Compare commits

...
12 Commits
34 changed files with 4477 additions and 619 deletions
+9
View File
@@ -61,6 +61,15 @@ $route['api/v1/organisation/[O|o]rganisationseinheit/(:any)'] = 'api/v1/organisa
$route['api/v1/ressource/[B|b]etriebsmittelperson/(:any)'] = 'api/v1/ressource/betriebsmittelperson2/$1'; $route['api/v1/ressource/[B|b]etriebsmittelperson/(:any)'] = 'api/v1/ressource/betriebsmittelperson2/$1';
$route['api/v1/system/[S|s]prache/(:any)'] = 'api/v1/system/sprache2/$1'; $route['api/v1/system/[S|s]prache/(:any)'] = 'api/v1/system/sprache2/$1';
// Legacy CMS news routes
$route['api/frontend/v1/Cms/getNewsItem/(:num)'] = 'api/frontend/v1/NewsAdministrationAPI/getNewsItem/$1';
$route['api/frontend/v1/Cms/storeNewsItem'] = 'api/frontend/v1/NewsAdministrationAPI/storeNewsItem';
$route['api/frontend/v1/Cms/updateNewsItem/(:num)'] = 'api/frontend/v1/NewsAdministrationAPI/updateNewsItem/$1';
$route['api/frontend/v1/Cms/deleteNewsItem/(:num)'] = 'api/frontend/v1/NewsAdministrationAPI/deleteNewsItem/$1';
// Keep legacy uppercase URLs refreshable while using the canonical controller method casing
$route['CisVue/Cms/NewsAdministration'] = 'CisVue/Cms/newsAdministration';
$route['Cis/LvPlan/.*'] = 'Cis/LvPlan/index/$1'; $route['Cis/LvPlan/.*'] = 'Cis/LvPlan/index/$1';
$route['Cis/MyLvPlan/.*'] = 'Cis/MyLvPlan/index/$1'; $route['Cis/MyLvPlan/.*'] = 'Cis/MyLvPlan/index/$1';
$route['Cis/MyLv/.*'] = 'Cis/MyLv/index/$1'; $route['Cis/MyLv/.*'] = 'Cis/MyLv/index/$1';
+24 -3
View File
@@ -17,14 +17,15 @@ class Cms extends Auth_Controller
array( array(
'content' => 'basis/cis:r', 'content' => 'basis/cis:r',
'getNews' => 'basis/cis:r', 'getNews' => 'basis/cis:r',
'getNewsRowCount' => 'basis/cis:r',
'getRoomInformation' => 'basis/cis:r', 'getRoomInformation' => 'basis/cis:r',
'news' => 'basis/cis:r' 'news' => 'basis/cis:r',
'newsAdministration' => ['basis/news:r'],
) )
); );
// Loads Libraries // Loads Libraries
$this->load->library('CmsLib'); $this->load->library('CmsLib');
$this->load->library('PermissionLib');
// Loads phrases system // Loads phrases system
$this->loadPhrases([ $this->loadPhrases([
@@ -84,9 +85,29 @@ class Cms extends Auth_Controller
public function news($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true) public function news($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true)
{ {
$viewData = array(); $viewData = array();
$this->load->view('CisRouterView/CisRouterView.php', ['viewData'=>$viewData, 'route' => 'News']); $this->load->view('CisRouterView/CisRouterView.php', [
'viewData'=>$viewData,
'route' => 'News',
'permissions' => [
'basis/news_r' => $this->permissionlib->isBerechtigt('basis/news', 's'),
'basis/news_w' => $this->permissionlib->isBerechtigt('basis/news', 'suid'),
],
]);
} }
public function newsAdministration()
{
$viewData = array();
$this->load->view('CisRouterView/CisRouterView.php', [
'viewData'=>$viewData,
'route' => 'NewsAdministration',
'permissions' => [
'basis/news_r' => $this->permissionlib->isBerechtigt('basis/news', 's'),
'basis/news_w' => $this->permissionlib->isBerechtigt('basis/news', 'suid'),
],
]);
}
public function getRoomInformation($ort_kurzbz) public function getRoomInformation($ort_kurzbz)
{ {
// Load Config // Load Config
+41 -27
View File
@@ -36,14 +36,15 @@ class Cms extends FHCAPI_Controller
'ContentID' => self::PERM_LOGGED, 'ContentID' => self::PERM_LOGGED,
'getOrtKurzbzContent' => self::PERM_LOGGED, 'getOrtKurzbzContent' => self::PERM_LOGGED,
'content' => self::PERM_LOGGED, 'content' => self::PERM_LOGGED,
'news' => self::PERM_LOGGED, 'newsForWidget' => self::PERM_LOGGED,
'getNewsRowCount' => self::PERM_LOGGED,
'getNews' => self::PERM_LOGGED, 'getNews' => self::PERM_LOGGED,
]); ]);
$this->load->model('content/News_model', 'NewsModel'); $this->load->model('content/News_model', 'NewsModel');
$this->load->model('crm/Student_model', 'StudentModel');
$this->load->model('crm/Prestudent_model', 'PrestudentModel');
// setting up the papgination_size // setting up the papgination_size
$this->page_size = 10; $this->page_size = 10;
@@ -59,7 +60,6 @@ class Cms extends FHCAPI_Controller
//------------------------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------------------------
// Private methods // Private methods
//------------------------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------------------------
// Public methods // Public methods
@@ -106,7 +106,7 @@ class Cms extends FHCAPI_Controller
$this->terminateWithSuccess($content_id); $this->terminateWithSuccess($content_id);
} }
public function news() public function newsForWidget()
{ {
// form validation // form validation
@@ -118,7 +118,7 @@ class Cms extends FHCAPI_Controller
$this->load->model('content/news_model', 'NewsModel'); $this->load->model('content/news_model', 'NewsModel');
$limit = $this->input->get('limit',TRUE); $limit = $this->input->get('limit',TRUE);
//query the news //query the news
$news = $this->NewsModel->getAll($limit); $news = $this->NewsModel->getAll($limit);
@@ -157,23 +157,7 @@ class Cms extends FHCAPI_Controller
} }
public function getNewsRowCount($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $fachbereich_kurzbz = null, $maxalter = 0, $edit = false, $sichtbar = true, $page = 1, $page_size = 10) public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = false, $titel = '', $edit = false, $sichtbar = true)
{
list($studiengang_kz, $semester) = $this->cmslib->getStgAndSem($studiengang_kz, $semester);
$all = $edit;
$this->load->model('content/News_model','NewsModel');
$num_rows = $this->NewsModel->countNewsWithContent(getSprache(), $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxalter, $page, $page_size, $all, $mischen);
$num_rows = $this->getDataOrTerminateWithError($num_rows);
$this->terminateWithSuccess($num_rows);
}
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true)
{ {
//form validation //form validation
$this->load->library('form_validation'); $this->load->library('form_validation');
@@ -193,15 +177,45 @@ class Cms extends FHCAPI_Controller
// default value for the page_size is 10 // default value for the page_size is 10
$page_size = $page_size ?? 10; $page_size = $page_size ?? 10;
$passedSichtbar = $this->input->get('published', true);
if($passedSichtbar !== null)
{
$sichtbar = $passedSichtbar;
}
$news = $this->cmslib->getNews($infoscreen, $studiengang_kz, $semester, $mischen, $titel, $edit, $sichtbar, $page, $page_size, $sprache); $filterForDegreePrograms = false;
$entitledDegreePrograms = [];
$student = $this->StudentModel->loadWhere(['student_uid' => getAuthUID()]);
if(isError($student))
{
$this->terminateWithError(getError($student));
}
if(hasData($student))
{
$student = current(getData($student));
$degreeProgramSemesterDataResult = $this->PrestudentModel->getStudiengaengAndAusbildungssemesterByStudentUid(getAuthUID());
$degreeProgramSemesterData = $this->getDataOrTerminateWithError($degreeProgramSemesterDataResult)[0];
if ($degreeProgramSemesterData) {
$studiengang_kz = isset($degreeProgramSemesterData->studiengang_kz) ? $degreeProgramSemesterData->studiengang_kz : null;
$semester = isset($degreeProgramSemesterData->ausbildungssemester) ? $degreeProgramSemesterData->ausbildungssemester : null;
}
$mischen = true;
} else {
$filterForDegreePrograms = true;
$entitledDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news");
array_push($entitledDegreePrograms, 0);
}
$news = $this->cmslib->getNews($infoscreen, $studiengang_kz, $semester, $mischen, $titel, $edit, $sichtbar, $page, $page_size, $sprache, $filterForDegreePrograms, $entitledDegreePrograms, true);
$news = $this->getDataOrTerminateWithError($news); $news = $this->getDataOrTerminateWithError($news);
$this->addMeta('phrases', json_decode($this->p->getJson())); $this->addMeta('row_count', $news["row_count"] ?? 0);
$this->terminateWithSuccess($news); $this->terminateWithSuccess($news["content"]);
} }
} }
@@ -0,0 +1,937 @@
<?php
/**
* Copyright (C) 2026 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
if (!defined('BASEPATH')) exit('No direct script access allowed');
class NewsAdministrationAPI extends FHCAPI_Controller
{
/**
* News API constructor.
*/
public function __construct()
{
parent::__construct([
'getNews' => 'basis/news:r',
'getNewsItem' => 'basis/news:r',
'storeNewsItem' => 'basis/news:w',
'updateNewsItem' => 'basis/news:w',
'deleteNewsItem' => 'basis/news:w'
]);
$this->load->model('content/Content_model', 'ContentModel');
$this->load->model('content/Contentsprache_model', 'ContentspracheModel');
$this->load->model('content/News_model', 'NewsModel');
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel');
$this->load->model('system/Sprache_model', 'SpracheModel');
$this->load->model('person/Benutzerfunktion_model', 'BenutzerfunktionModel');
$this->loadPhrases([
'global'
]);
$this->load->library('CmsLib');
$this->load->helper('hlp_sancho_helper');
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $titel = '', $active = true, $sichtbar = true)
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('page','Page','required|is_natural');
$this->form_validation->set_rules('page_size', 'PageSize', 'is_natural');
if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$page = intval($this->input->get('page', true));
$page_size = intval($this->input->get('page_size', true));
$sprache = $this->input->get('sprache', true);
if(!$sprache)
{
$sprache = getUserLanguage();
}
// default value for the page_size is 10
$page_size = $page_size ?? 10;
$passedSichtbar = $this->input->get('published', true);
if($passedSichtbar !== null)
{
$sichtbar = $passedSichtbar;
}
$passedIsActive = $this->input->get('isActive', true);
if ($passedIsActive !== null)
{
$isActive = filter_var(
$passedIsActive,
FILTER_VALIDATE_BOOLEAN,
FILTER_NULL_ON_FAILURE
);
if ($isActive !== null)
{
$active = $isActive;
}
}
$degreeProgramShortCode = $this->input->get('degreeProgramShortCode', true);
if ($degreeProgramShortCode !== null && $degreeProgramShortCode !== '')
{
if (!ctype_digit((string)$degreeProgramShortCode))
{
$this->terminateWithValidationErrors([
'degreeProgramShortCode' => 'A valid degree program is required'
]);
}
$studiengang_kz = $degreeProgramShortCode;
}
$passedSemester = $this->input->get('semester', true);
if ($passedSemester !== null && $passedSemester !== '')
{
if (
!ctype_digit((string)$passedSemester)
|| (int)$passedSemester < 1
|| (int)$passedSemester > 8
)
{
$this->terminateWithValidationErrors([
'semester' => 'A valid semester is required'
]);
}
$semester = filter_var($passedSemester, FILTER_VALIDATE_INT);
}
$entitledDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news");
$news = $this->cmslib->getNews($infoscreen, $studiengang_kz, $semester, false, $titel, true, $sichtbar, $page, $page_size, $sprache, true, $entitledDegreePrograms, $active);
$news = $this->getDataOrTerminateWithError($news);
$this->addMeta('row_count', $news["row_count"] ?? 0);
$this->terminateWithSuccess($news["content"]);
}
/**
* Returns one news entry with all available language versions for editing.
*
* @return void
*/
public function getNewsItem($newsId)
{
if (!is_numeric($newsId) || (int)$newsId < 1)
{
$this->terminateWithValidationErrors([
'newsId' => 'A valid news ID is required'
]);
}
$newsId = (int)$newsId;
$newsResult = $this->NewsModel->load($newsId);
if (isError($newsResult))
{
$this->terminateWithError(getError($newsResult), self::ERROR_TYPE_DB);
}
if (!hasData($newsResult))
{
$this->terminateWithValidationErrors([
'newsId' => 'The requested news entry does not exist'
]);
}
$news = current(getData($newsResult));
$entitledDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news");
if (!in_array($news->studiengang_kz, $entitledDegreePrograms)) {
$this->terminateWithError('You are not entitled to access this news entry', self::ERROR_TYPE_PERMISSION);
}
$translationsResult = $this->ContentspracheModel->loadWhere([
'content_id' => $news->content_id
]);
if (isError($translationsResult))
{
$this->terminateWithError(getError($translationsResult), self::ERROR_TYPE_DB);
}
$languages = [];
foreach ((array)getData($translationsResult) as $translation)
{
$languages[$translation->sprache] = true;
}
$translations = [];
foreach (array_keys($languages) as $language)
{
$contentResult = $this->ContentModel->getContent(
$news->content_id,
$language,
null,
null,
false
);
if (isError($contentResult))
{
$this->terminateWithError(getError($contentResult), self::ERROR_TYPE_DB);
}
$content = getData($contentResult);
$document = new DOMDocument('1.0', 'UTF-8');
if ($content->content !== '' && $document->loadXML($content->content) === false)
{
$this->terminateWithError(
'The stored news content is invalid',
self::ERROR_TYPE_GENERAL
);
}
$author = $document->getElementsByTagName('verfasser')->item(0);
$title = $document->getElementsByTagName('betreff')->item(0);
$text = $document->getElementsByTagName('text')->item(0);
$translations[] = [
'language' => $language,
'author' => $author ? $author->nodeValue : '',
'title' => $title ? $title->nodeValue : '',
'text' => $text ? str_replace('dms.php', APP_ROOT . 'cms/dms.php', $text->nodeValue) : '',
'isPublished' => (bool)$content->sichtbar
];
}
$this->terminateWithSuccess([
'newsId' => (int)$news->news_id,
'visibleFrom' => $news->datum,
'visibleTo' => $news->datum_bis,
'degreeProgramShortCode' => $news->studiengang_kz,
'semester' => $news->semester,
'translations' => $translations
]);
}
/**
* Creates a news entry and all supplied translations.
*
* @return void
*/
public function storeNewsItem()
{
$this->load->library('form_validation');
$postData = $this->input->post(null, true);
$this->form_validation->set_data(is_array($postData) ? $postData : []);
$this->form_validation->set_rules('visibleFrom', 'Visible from', 'required|is_valid_date');
$this->form_validation->set_rules('visibleTo', 'Visible to', 'is_valid_date');
$this->form_validation->set_rules(
'degreeProgramShortCode',
'Degree program',
'required|is_natural'
);
$this->form_validation->set_rules(
'semester',
'Semester',
'is_natural_no_zero|less_than_equal_to[8]'
);
$errors = $this->form_validation->run() === false
? $this->form_validation->error_array()
: [];
$visibleFrom = $this->input->post('visibleFrom', true);
$visibleTo = $this->input->post('visibleTo', true);
$translations = $this->input->post('translations', true);
if (
is_string($visibleFrom)
&& is_string($visibleTo)
&& $visibleTo !== ''
&& is_valid_date($visibleFrom)
&& is_valid_date($visibleTo)
&& new DateTime($visibleTo) < new DateTime($visibleFrom)
)
{
$errors['visibleTo'] = 'Visible to must not be before Visible from';
}
if (!is_array($translations) || count($translations) === 0)
{
$errors['translations'] = 'At least one translation is required';
}
else
{
foreach ($translations as $index => $translation)
{
if (!is_array($translation))
{
$errors['translations.' . $index] = 'Each translation must be an object';
}
}
}
if (count($errors) > 0)
{
$this->terminateWithValidationErrors($errors);
}
$this->_ensureUserIsEntitledForDegreeProgram($this->input->post('degreeProgramShortCode', true));
$degreeProgramShortCode = $this->input->post('degreeProgramShortCode', true);
$semester = $this->input->post('semester', true);
$semester = $semester === '' ? null : $semester;
$visibleTo = $visibleTo === '' ? null : $visibleTo;
$studiengang = $this->_getStudiengang($degreeProgramShortCode);
$data = [
'studiengang_kz' => $degreeProgramShortCode,
'semester' => $semester,
'datum' => $visibleFrom,
'datum_bis' => $visibleTo,
'translations' => []
];
foreach ($translations as $index => $translation)
{
$data['translations'][$index] = [
'sprache' => $translation['language'] ?? '',
'verfasser' => $translation['author'] ?? '',
'betreff' => $translation['title'] ?? '',
'text' => $translation['text'] ?? '',
'sichtbar' => isset($translation['isPublished']) ? (bool)$translation['isPublished'] : false
];
}
$this->_validateLanguages($data['translations']);
foreach ($data['translations'] as $index => $translation)
{
$data['translations'][$index]['content'] = $this->_buildNewsXml($translation);
}
$uid = getAuthUID();
$now = date('Y-m-d H:i:s');
if ($this->db->trans_begin() === false)
{
$this->terminateWithError($this->db->error(), self::ERROR_TYPE_DB);
}
$contentResult = $this->ContentModel->insert([
'template_kurzbz' => 'news',
'oe_kurzbz' => $studiengang->oe_kurzbz,
'aktiv' => true,
'menu_open' => false,
'insertamum' => $now,
'insertvon' => $uid
]);
$contentId = (int)$this->_getInsertedIdOrRollback($contentResult);
$newsResult = $this->NewsModel->insert([
'uid' => $uid,
'studiengang_kz' => $data['studiengang_kz'],
'semester' => $data['semester'],
'datum' => $data['datum'],
'datum_bis' => $data['datum_bis'],
'content_id' => $contentId,
'insertamum' => $now,
'insertvon' => $uid,
'updateamum' => $now,
'updatevon' => $uid
]);
$newsId = (int)$this->_getInsertedIdOrRollback($newsResult);
foreach ($data['translations'] as $translation)
{
$translation['content'] = $this->formatContentURLsToRelative($translation['content']);
$translationResult = $this->ContentspracheModel->insert([
'sprache' => $translation['sprache'],
'content_id' => $contentId,
'version' => 1,
'sichtbar' => $translation['sichtbar'],
'content' => $translation['content'],
'titel' => $translation['betreff'],
'insertamum' => $now,
'insertvon' => $uid,
'updateamum' => $now,
'updatevon' => $uid
]);
$this->_getInsertedIdOrRollback($translationResult);
}
$newsItemResult = $this->NewsModel->load($newsId);
$newsItem = $this->getDataOrTerminateWithError($newsItemResult);
$newsItem = $newsItem[0];
$this->_sendNewsItemTranslationRequiredMail(
$newsItem
);
if ($this->db->trans_status() === false)
{
$error = $this->db->error();
$this->db->trans_rollback();
$this->terminateWithError($error, self::ERROR_TYPE_DB);
}
if ($this->db->trans_commit() === false)
{
$error = $this->db->error();
$this->db->trans_rollback();
$this->terminateWithError($error, self::ERROR_TYPE_DB);
}
$this->terminateWithSuccess($newsId);
}
/**
* Updates a news entry and its supplied translations.
*
* @return void
*/
public function updateNewsItem($newsId)
{
if (!is_numeric($newsId) || (int)$newsId < 1)
{
$this->terminateWithValidationErrors([
'newsId' => 'A valid news ID is required'
]);
}
$newsId = (int)$newsId;
$existingNewsResult = $this->NewsModel->load($newsId);
if (isError($existingNewsResult))
{
$this->terminateWithError(getError($existingNewsResult), self::ERROR_TYPE_DB);
}
if (!hasData($existingNewsResult))
{
$this->terminateWithValidationErrors([
'newsId' => 'The requested news entry does not exist'
]);
}
$existingNews = current(getData($existingNewsResult));
$this->_ensureUserIsEntitledForDegreeProgram($existingNews->studiengang_kz);
$this->load->library('form_validation');
$postData = $this->input->post(null, true);
$this->form_validation->set_data(is_array($postData) ? $postData : []);
$this->form_validation->set_rules('visibleFrom', 'Visible from', 'required|is_valid_date');
$this->form_validation->set_rules('visibleTo', 'Visible to', 'is_valid_date');
$this->form_validation->set_rules(
'degreeProgramShortCode',
'Degree program',
'required|is_natural'
);
$this->form_validation->set_rules(
'semester',
'Semester',
'is_natural_no_zero|less_than_equal_to[8]'
);
$errors = $this->form_validation->run() === false
? $this->form_validation->error_array()
: [];
$visibleFrom = $this->input->post('visibleFrom', true);
$visibleTo = $this->input->post('visibleTo', true);
$translations = $this->input->post('translations', true);
if (
is_string($visibleFrom)
&& is_string($visibleTo)
&& $visibleTo !== ''
&& is_valid_date($visibleFrom)
&& is_valid_date($visibleTo)
&& new DateTime($visibleTo) < new DateTime($visibleFrom)
)
{
$errors['visibleTo'] = 'Visible to must not be before Visible from';
}
if (!is_array($translations) || count($translations) === 0)
{
$errors['translations'] = 'At least one translation is required';
}
else
{
foreach ($translations as $index => $translation)
{
if (!is_array($translation))
{
$errors['translations.' . $index] = 'Each translation must be an object';
}
}
}
if (count($errors) > 0)
{
$this->terminateWithValidationErrors($errors);
}
$degreeProgramShortCode = $this->input->post('degreeProgramShortCode', true);
$semester = $this->input->post('semester', true);
$semester = $semester === '' ? null : $semester;
$visibleTo = $visibleTo === '' ? null : $visibleTo;
$studiengang = $this->_getStudiengang($degreeProgramShortCode);
$data = [
'studiengang_kz' => $degreeProgramShortCode,
'semester' => $semester,
'datum' => $visibleFrom,
'datum_bis' => $visibleTo,
'translations' => []
];
foreach ($translations as $index => $translation)
{
$data['translations'][$index] = [
'sprache' => $translation['language'] ?? '',
'verfasser' => $translation['author'] ?? '',
'betreff' => $translation['title'] ?? '',
'text' => $translation['text'] ?? '',
'sichtbar' => isset($translation['isPublished']) ? (bool)$translation['isPublished'] : false
];
}
$this->_validateLanguages($data['translations']);
foreach ($data['translations'] as $index => $translation)
{
$data['translations'][$index]['content'] = $this->_buildNewsXml($translation);
}
$uid = getAuthUID();
$now = date('Y-m-d H:i:s');
if ($this->db->trans_begin() === false)
{
$this->terminateWithError($this->db->error(), self::ERROR_TYPE_DB);
}
$contentResult = $this->ContentModel->update($existingNews->content_id, [
'oe_kurzbz' => $studiengang->oe_kurzbz,
'updateamum' => $now,
'updatevon' => $uid
]);
if (isError($contentResult))
{
$this->db->trans_rollback();
$this->terminateWithError(getError($contentResult), self::ERROR_TYPE_DB);
}
$newsResult = $this->NewsModel->update($newsId, [
'studiengang_kz' => $data['studiengang_kz'],
'semester' => $data['semester'],
'datum' => $data['datum'],
'datum_bis' => $data['datum_bis'],
'updateamum' => $now,
'updatevon' => $uid
]);
if (isError($newsResult))
{
$this->db->trans_rollback();
$this->terminateWithError(getError($newsResult), self::ERROR_TYPE_DB);
}
$existingTranslationsResult = $this->ContentspracheModel->loadWhere([
'content_id' => $existingNews->content_id
]);
if (isError($existingTranslationsResult))
{
$this->db->trans_rollback();
$this->terminateWithError(getError($existingTranslationsResult), self::ERROR_TYPE_DB);
}
$existingTranslations = [];
foreach ((array)getData($existingTranslationsResult) as $existingTranslation)
{
$language = $existingTranslation->sprache;
if (
!isset($existingTranslations[$language])
|| (int)$existingTranslation->version > (int)$existingTranslations[$language]->version
)
{
$existingTranslations[$language] = $existingTranslation;
}
}
foreach ($data['translations'] as $translation)
{
$translation['content'] = $this->formatContentURLsToRelative($translation['content']);
$translationData = [
'sichtbar' => $translation['sichtbar'],
'content' => $translation['content'],
'titel' => $translation['betreff'],
'updateamum' => $now,
'updatevon' => $uid
];
$existingTranslation = $existingTranslations[$translation['sprache']] ?? null;
if ($existingTranslation)
{
$translationResult = $this->ContentspracheModel->update(
$existingTranslation->contentsprache_id,
$translationData
);
}
else
{
$translationResult = $this->ContentspracheModel->insert([
'sprache' => $translation['sprache'],
'content_id' => $existingNews->content_id,
'version' => 1,
'insertamum' => $now,
'insertvon' => $uid
] + $translationData);
}
$this->_getInsertedIdOrRollback($translationResult);
}
if ($this->db->trans_status() === false)
{
$error = $this->db->error();
$this->db->trans_rollback();
$this->terminateWithError($error, self::ERROR_TYPE_DB);
}
if ($this->db->trans_commit() === false)
{
$error = $this->db->error();
$this->db->trans_rollback();
$this->terminateWithError($error, self::ERROR_TYPE_DB);
}
$this->terminateWithSuccess($newsId);
}
/**
* Deletes one news entry and its linked content.
*
* @return void
*/
public function deleteNewsItem($newsId)
{
if (!is_numeric($newsId) || (int)$newsId < 1)
{
$this->terminateWithValidationErrors([
'newsId' => 'A valid news ID is required'
]);
}
$newsId = (int)$newsId;
$newsResult = $this->NewsModel->load($newsId);
if (isError($newsResult))
{
$this->terminateWithError(getError($newsResult), self::ERROR_TYPE_DB);
}
if (!hasData($newsResult))
{
$this->terminateWithValidationErrors([
'newsId' => 'The requested news entry does not exist'
]);
}
$news = current(getData($newsResult));
$this->_ensureUserIsEntitledForDegreeProgram($news->studiengang_kz);
if ($this->db->trans_begin() === false)
{
$this->terminateWithError($this->db->error(), self::ERROR_TYPE_DB);
}
$newsDeleteResult = $this->NewsModel->delete($newsId);
if (isError($newsDeleteResult))
{
$this->db->trans_rollback();
$this->terminateWithError(getError($newsDeleteResult), self::ERROR_TYPE_DB);
}
$this->db->where('content_id', $news->content_id);
if ($this->db->delete('campus.tbl_contentsprache') === false)
{
$error = $this->db->error();
$this->db->trans_rollback();
$this->terminateWithError($error, self::ERROR_TYPE_DB);
}
$contentDeleteResult = $this->ContentModel->delete($news->content_id);
if (isError($contentDeleteResult))
{
$this->db->trans_rollback();
$this->terminateWithError(getError($contentDeleteResult), self::ERROR_TYPE_DB);
}
if ($this->db->trans_status() === false)
{
$error = $this->db->error();
$this->db->trans_rollback();
$this->terminateWithError($error, self::ERROR_TYPE_DB);
}
if ($this->db->trans_commit() === false)
{
$error = $this->db->error();
$this->db->trans_rollback();
$this->terminateWithError($error, self::ERROR_TYPE_DB);
}
$this->terminateWithSuccess($newsId);
}
//------------------------------------------------------------------------------------------------------------------
// Private methods
/**
* Loads the requested Studiengang or reports a validation error.
*
* @param integer $studiengangKz
* @return stdClass
*/
private function _getStudiengang($studiengangKz)
{
$result = $this->StudiengangModel->load($studiengangKz);
if (isError($result))
{
$this->terminateWithError(getError($result), self::ERROR_TYPE_DB);
}
if (!hasData($result))
{
$this->terminateWithValidationErrors([
'studiengang_kz' => 'The requested Studiengang does not exist'
]);
}
$studiengaenge = getData($result);
return current($studiengaenge);
}
/**
* Ensures that all supplied languages exist in tbl_sprache.
*
* @param array $translations
* @return void
*/
private function _validateLanguages($translations)
{
$languages = [];
foreach ($translations as $translation)
{
$languages[] = $translation['sprache'];
}
$result = $this->SpracheModel->loadMultiple($languages);
if (isError($result))
{
$this->terminateWithError(getError($result), self::ERROR_TYPE_DB);
}
$existingLanguages = [];
foreach ((array)getData($result) as $language)
{
$existingLanguages[$language->sprache] = true;
}
$errors = [];
foreach ($translations as $index => $translation)
{
if (!isset($existingLanguages[$translation['sprache']]))
{
$errors['translations.' . $index . '.sprache'] = 'The requested language does not exist';
}
}
if (count($errors) > 0)
{
$this->terminateWithValidationErrors($errors);
}
}
private function _ensureUserIsEntitledForDegreeProgram($degreeProgramShortCode)
{
$entitledDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news:w");
if (!is_array($entitledDegreePrograms))
{
$this->terminateWithError('Failed to retrieve entitled degree programs for the user', self::ERROR_TYPE_GENERAL);
}
if (!in_array($degreeProgramShortCode, $entitledDegreePrograms)) {
$this->terminateWithError("User is not entitled for the specified degree program");
}
}
/**
* Returns an inserted identifier or rolls the transaction back on error.
*
* @param stdClass $result
* @return mixed
*/
private function _getInsertedIdOrRollback($result)
{
if (isError($result))
{
$this->db->trans_rollback();
$this->terminateWithError(getError($result), self::ERROR_TYPE_DB);
}
return getData($result);
}
/**
* Builds the XML format consumed by existing news readers.
*
* @param array $translation
* @return string
*/
private function _buildNewsXml($translation)
{
$document = new DOMDocument('1.0', 'UTF-8');
$news = $document->createElement('news');
$document->appendChild($news);
$this->_appendCdataElement($document, $news, 'verfasser', $translation['verfasser']);
$this->_appendCdataElement($document, $news, 'betreff', $translation['betreff']);
$this->_appendCdataElement($document, $news, 'text', $translation['text']);
return $document->saveXML($news);
}
/**
* Appends a CDATA element and safely preserves CDATA terminators.
*
* @param DOMDocument $document
* @param DOMElement $parent
* @param string $name
* @param string $value
* @return void
*/
private function _appendCdataElement($document, $parent, $name, $value)
{
$element = $document->createElement($name);
$parts = explode(']]>', $value);
$lastIndex = count($parts) - 1;
foreach ($parts as $index => $part)
{
if ($index > 0)
{
$part = '>' . $part;
}
if ($index < $lastIndex)
{
$part .= ']]';
}
$element->appendChild($document->createCDATASection($part));
}
$parent->appendChild($element);
}
private function _sendNewsItemTranslationRequiredMail($newsItem)
{
$translatorsResult = $this->BenutzerfunktionModel->getBenutzerFunktionen("translate", $newsItem->studiengang_kz);
$translators = $this->getDataOrTerminateWithError($translatorsResult);
$newsItemContentResult = $this->ContentspracheModel->loadWhere([
'content_id' => $newsItem->content_id,
'sprache' => 'German'
]);
$newsItemContentEntry = getData($newsItemContentResult)[0] ?? null;
if ($newsItemContentEntry) {
$newsItem->sichtbar = $newsItemContentEntry->sichtbar ?? false;
$newsItem->content = $newsItemContentEntry->content;
}
$content = getData(
$this->cmslib->createNewsContent(
$this->cmslib->createNewsWrapper($newsItem, true),
false,
""
)
);
foreach ($translators as $translator) {
sendSanchoMail(
"Sancho_Mail_News_Translation_Req",
[
"newsItemUrl" => APP_ROOT . "cis.php/CisVue/Cms/newsAdministration?newsId=" . $newsItem->news_id,
"newsItemAuthor" => $newsItem->verfasser,
"newsItemSubject" => $newsItem->betreff,
"newsItemContent" => $content
],
$translator->uid . "@" . DOMAIN,
"Übersetzung erforderlich / Translation required"
);
}
}
private function formatContentURLsToRelative($content)
{
$content = preg_replace(
'#https?://[^"\'>\s]*/(dms\.php(?:\?[^"\'>\s]*)?)#i',
'$1',
$content
);
$content = preg_replace(
'#(?:\.\./)+[^"\'>\s]*/(dms\.php(?:\?[^"\'>\s]*)?)#i',
'$1',
$content
);
$content = preg_replace(
'#/[^"\'>\s]*/(dms\.php(?:\?[^"\'>\s]*)?)#i',
'$1',
$content
);
return $content;
}
}
@@ -27,7 +27,7 @@ class Studgang extends FHCAPI_Controller
{ {
parent::__construct([ parent::__construct([
'getStudiengangInfo'=> self::PERM_LOGGED, 'getStudiengangInfo'=> self::PERM_LOGGED,
'getDegreePrograms'=> self::PERM_LOGGED,
]); ]);
$this->load->model('organisation/Studiengang_model', 'StudiengangModel'); $this->load->model('organisation/Studiengang_model', 'StudiengangModel');
@@ -43,6 +43,22 @@ class Studgang extends FHCAPI_Controller
//------------------------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------------------------
// Public methods // Public methods
public function getDegreePrograms() {
$entitledDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news");
if (!$entitledDegreePrograms) {
$this->terminateWithError("User is not entitled for any degree program");
}
$activeDegreePrograms = $this->StudiengangModel->getDegreePrograms();
$activeDegreePrograms= $this->getDataOrTerminateWithError($activeDegreePrograms);
$activeDegreePrograms = array_values(array_filter($activeDegreePrograms, function($degreeProgram) use ($entitledDegreePrograms) {
return in_array($degreeProgram->studiengang_kz, $entitledDegreePrograms);
}));
$this->terminateWithSuccess($activeDegreePrograms);
}
public function getStudiengangInfo(){ public function getStudiengangInfo(){
$isMitarbeiter = $this->MitarbeiterModel->isMitarbeiter(getAuthUID()); $isMitarbeiter = $this->MitarbeiterModel->isMitarbeiter(getAuthUID());
$isMitarbeiter = $this->getDataOrTerminateWithError($isMitarbeiter); $isMitarbeiter = $this->getDataOrTerminateWithError($isMitarbeiter);
+75 -93
View File
@@ -4,6 +4,7 @@ if (!defined('BASEPATH'))
exit('No direct script access allowed'); exit('No direct script access allowed');
use \DateTime as DateTime; use \DateTime as DateTime;
use \DateInterval as DateInterval;
use \DOMDocument as DOMDocument; use \DOMDocument as DOMDocument;
use \XSLTProcessor as XSLTProcessor; use \XSLTProcessor as XSLTProcessor;
@@ -141,63 +142,6 @@ class CmsLib
]); ]);
} }
/**
* @param stdClass $stg_obj
*
* @return stdClass
*/
protected function getNewsExtras($stg_obj, $semester)
{
$this->ci->load->model('person/Benutzerfunktion_model', 'BenutzerfunktionModel');
$stg_ltg = $this->ci->StudiengangModel->getLeitungDetailed($stg_obj->studiengang_kz);
if (isError($stg_ltg))
return $stg_ltg;
$stg_ltg = getData($stg_ltg) ?: [];
$gf_ltg = $this->ci->BenutzerfunktionModel->getBenutzerFunktionenDetailed('gLtg', $stg_obj->oe_kurzbz);
if (isError($gf_ltg))
return $gf_ltg;
$gf_ltg = getData($gf_ltg) ?: [];
$stv_ltg = $this->ci->BenutzerfunktionModel->getBenutzerFunktionenDetailed('stvLtg', $stg_obj->oe_kurzbz);
if (isError($stv_ltg))
return $stv_ltg;
$stv_ltg = getData($stv_ltg) ?: [];
$ass = $this->ci->BenutzerfunktionModel->getBenutzerFunktionenDetailed('ass', $stg_obj->oe_kurzbz);
if (isError($ass))
return $ass;
$ass = getData($ass) ?: [];
$hochschulvertr = $this->ci->BenutzerfunktionModel->getBenutzerFunktionenDetailed('hsv');
if (isError($hochschulvertr))
return $hochschulvertr;
$hochschulvertr = getData($hochschulvertr) ?: [];
$stdv = $this->ci->BenutzerfunktionModel->getBenutzerFunktionenDetailed('stdv', $stg_obj->oe_kurzbz);
if (isError($stdv))
return $stdv;
$stdv = getData($stdv) ?: [];
$jahrgangsvertr = $this->ci->BenutzerfunktionModel->getBenutzerFunktionenDetailed('jgv', $stg_obj->oe_kurzbz, $semester);
if (isError($jahrgangsvertr))
return $jahrgangsvertr;
$jahrgangsvertr = getData($jahrgangsvertr) ?: [];
return success($this->ci->load->view('Cis/Cms/News/Xml/NewsExtras', [
'studiengang' => $stg_obj,
'semester' => $semester,
'stg_ltg' => $stg_ltg,
'gf_ltg' => $gf_ltg,
'stv_ltg' => $stv_ltg,
'ass' => $ass,
'hochschulvertr' => $hochschulvertr,
'stdv' => $stdv,
'jahrgangsvertr' => $jahrgangsvertr
], true));
}
/** /**
* @param string $studiengang_kz * @param string $studiengang_kz
* @param string $semester * @param string $semester
@@ -233,64 +177,102 @@ class CmsLib
* *
* @return void * @return void
*/ */
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true, $page = 1, $page_size = 10, $sprache) public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true, $page = 1, $page_size = 10, $sprache, $filterForDegreePrograms = false, $allowedDegreePrograms = [], $active = true)
{ {
$this->ci->load->model('organisation/Studiengang_model', 'StudiengangModel'); $this->ci->load->model('organisation/Studiengang_model', 'StudiengangModel');
list($studiengang_kz, $semester) = $this->getStgAndSem($studiengang_kz, $semester); list($studiengang_kz, $semester) = $this->getStgAndSem($studiengang_kz, $semester);
$all = $edit;
$xml = '<?xml version="1.0" encoding="UTF-8"?><content>';
$this->ci->load->model('content/News_model', 'NewsModel'); $this->ci->load->model('content/News_model', 'NewsModel');
$news = $this->ci->NewsModel->getNewsWithContent($sprache, $studiengang_kz, $semester, null, $sichtbar, 0, $page, $page_size, $all, $mischen);
if (isError($news))
return $news;
$news = getData($news);
$news = $this->ci->NewsModel->getNewsWithContent($sprache, $studiengang_kz, $semester, null, $sichtbar, $page, $page_size, $active, $mischen, $filterForDegreePrograms, $allowedDegreePrograms);
$news = getData($news) ?? [];
$newsWrappers = [];
foreach ($news as $newsobj) { foreach ($news as $newsobj) {
if ($studiengang_kz && $edit && !$newsobj->studiengang_kz) if ($studiengang_kz && $edit && !$newsobj->studiengang_kz)
continue; continue;
$date = new DateTime($newsobj->datum);
$datum = '<datum><![CDATA[' . $date->format('d.m.Y') . ']]></datum>'; $newsWrappers[] = $this->createNewsWrapper($newsobj, $edit);
$datum .= '<datumdetail><![CDATA[' . $date->format('Y-m-d H:i') . ']]></datumdetail>';
$id = $edit ? '<news_id><![CDATA[' . $newsobj->news_id . ']]></news_id>' : '';
$xml .= "<newswrapper>" . $newsobj->content . $datum . $id . "</newswrapper>";
} }
/* if ($studiengang_kz != 0) { $content = $this->createNewsContent($newsWrappers, $infoscreen, $titel);
$stg_obj = $this->ci->StudiengangModel->load($studiengang_kz); if (isError($content))
if (isError($stg_obj)) return $content;
return $stg_obj;
$stg_obj = current(getData($stg_obj) ?: []);
if ($stg_obj) { return success([
if (!$edit && !$infoscreen) { "content" => getData($content),
$extras = $this->getNewsExtras($stg_obj, $semester); "row_count" => $news[0]->row_count ?? 0
if (isError($extras)) ]);
return $extras; }
$xml .= getData($extras);
} /**
$xml .= '<studiengang_bezeichnung><![CDATA[' . $stg_obj->bezeichnung . ']]></studiengang_bezeichnung>'; * Creates a newswrapper XML element from a news item.
} *
} */ * @param stdClass $newsobj
* @param boolean $edit
*
* @return DOMElement
*/
public function createNewsWrapper($newsobj, $edit)
{
$id = $edit ? '<news_id><![CDATA[' . $newsobj->news_id . ']]></news_id>' : '';
$isPublished = $edit ? '<is_published><![CDATA[' . ($newsobj->sichtbar ? 'true' : 'false') . ']]></is_published>' : '';
$date = new DateTime($newsobj->datum);
$datum = '<datum><![CDATA[' . $date->format('d.m.Y') . ']]></datum>';
$datum .= '<datumdetail><![CDATA[' . $date->format('Y-m-d H:i') . ']]></datumdetail>';
$hasVisibleToDate = isset($newsobj->datum_bis) && $newsobj->datum_bis !== '';
$datumTo = $hasVisibleToDate
? new DateTime($newsobj->datum_bis)
: (clone $date)->add(new DateInterval('P' . MAXNEWSALTER . 'D'));
if ($hasVisibleToDate)
$datumTo->setTime(23, 59, 59);
$now = new DateTime();
$isActive = '<is_active><![CDATA[' . ($datumTo >= $now ? 'true' : 'false') . ']]></is_active>';
$newsWrapper = new DOMDocument();
$newsWrapper->loadXML('<newswrapper>' . $newsobj->content . $datum . $id . $isPublished . $isActive . '</newswrapper>');
return $newsWrapper->documentElement;
}
/**
* Creates rendered news content from one or more newswrapper XML elements.
*
* @param DOMElement|array $newsWrappers
* @param boolean $infoscreen
* @param string $titel
*
* @return stdClass
*/
public function createNewsContent($newsWrappers, $infoscreen, $titel)
{
if (!is_array($newsWrappers))
$newsWrappers = [$newsWrappers];
$XML = new DOMDocument('1.0', 'UTF-8');
$contentElement = $XML->createElement('content');
$XML->appendChild($contentElement);
foreach ($newsWrappers as $newsWrapper)
$contentElement->appendChild($XML->importNode($newsWrapper, true));
if ($titel != '') { if ($titel != '') {
$xml .= '<news_titel>' . $titel . '</news_titel>'; $titleElement = $XML->createElement('news_titel');
$titleElement->appendChild($XML->createTextNode($titel));
$contentElement->appendChild($titleElement);
} }
$xml .= '</content>';
//XSLT Vorlage laden //XSLT Vorlage laden
$template = $this->ci->TemplateModel->load($infoscreen ? 'news_infoscreen' : 'news'); $template = $this->ci->TemplateModel->load($infoscreen ? 'news_infoscreen' : 'news');
if (isError($template)) if (isError($template))
return $template; return $template;
$template = current(getData($template)); $template = current(getData($template));
$XML = new DOMDocument();
$XML->loadXML($xml);
$xsltemplate = new DOMDocument(); $xsltemplate = new DOMDocument();
$xsltemplate->loadXML($template->xslt_xhtml_c4); $xsltemplate->loadXML($template->xslt_xhtml_c4);
+52 -35
View File
@@ -20,14 +20,16 @@ class News_model extends DB_Model
public function getAll($limit = null) public function getAll($limit = null)
{ {
$this->addJoin("campus.tbl_content","content_id"); $this->addJoin("campus.tbl_content","content_id");
return $this->execReadOnlyQuery(' return $this->execReadOnlyQuery('
SELECT *, TO_CHAR(campus.tbl_news.datum, ?) as datumformatted SELECT *, TO_CHAR(campus.tbl_news.datum, ?) as datumformatted
FROM campus.tbl_news FROM campus.tbl_news
JOIN campus.tbl_content content ON content.content_id = campus.tbl_news.content_id JOIN campus.tbl_content content ON content.content_id = campus.tbl_news.content_id
WHERE WHERE
--text IS NOT NULL AND --text IS NOT NULL AND
datum <= NOW() AND (datum_bis IS NULL OR datum_bis >= now()::date) datum <= NOW() AND (datum_bis >= NOW()::date OR datum_bis IS NULL) AND
ORDER BY datum DESC (NOW()-datum) < interval \'' . MAXNEWSALTER . ' days\'
ORDER BY datum DESC ORDER BY campus.tbl_news.updateamum DESC
LIMIT ' . $this->escape($limit) LIMIT ' . $this->escape($limit)
, ['DD/MM/YYYY']); , ['DD/MM/YYYY']);
} }
@@ -48,31 +50,28 @@ class News_model extends DB_Model
* @param integer | null $semester * @param integer | null $semester
* @param string $fachbereich_kurzbz * @param string $fachbereich_kurzbz
* @param boolean $sichtbar * @param boolean $sichtbar
* @param integer $maxalter
* @param integer $page * @param integer $page
* @param integer $page_size * @param integer $page_size
* @param boolean $all * @param boolean $active
* @param boolean $mischen * @param boolean $mischen
* *
* TODO(chris): this is not a good function -> the params are all over the place * TODO(chris): this is not a good function -> the params are all over the place
* *
*/ */
protected function prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz = null, $sichtbar = true, $maxalter = 0, $page = 1, $page_size = 10, $all = false, $mischen = true) protected function prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz = null, $sichtbar = true, $page = 1, $page_size = 10, $active = true, $mischen = true, $filterForDegreePrograms = false, $allowedDegreePrograms = [])
{ {
$this->addSelect(["*", "COUNT(*) OVER() AS row_count"]);
$this->addOrder('datum', 'DESC'); $this->addOrder('datum', 'DESC');
$this->addOrder('tbl_news.updateamum', 'DESC');
$studiengang_kz = trim($studiengang_kz); $studiengang_kz = trim($studiengang_kz);
$fachbereich_kurzbz = trim($fachbereich_kurzbz); $fachbereich_kurzbz = trim($fachbereich_kurzbz);
$where = []; $where = [];
if (trim($maxalter) != '0') {
$where[] = "(now()-datum) < interval " . $this->db->escape($maxalter) . " days"; if ($active) {
$where[] = "(datum <= NOW()::date AND (datum_bis >= NOW()::date OR (datum_bis IS NULL AND (NOW()-datum) < interval '" . MAXNEWSALTER . " days')))";
}
if (!$all) {
$where[] = "datum <= now()";
$where[] = "(datum_bis >= now()::date OR datum_bis IS NULL)";
} }
if ($fachbereich_kurzbz != '*') { if ($fachbereich_kurzbz != '*') {
if ($fachbereich_kurzbz == '') { if ($fachbereich_kurzbz == '') {
@@ -82,27 +81,54 @@ class News_model extends DB_Model
} }
} }
if ($studiengang_kz == '0') {
$where[] = "studiengang_kz = " . $this->db->escape($studiengang_kz);
if ($semester === NULL) $sgAndSemesterCondition = [];
$where[] = "semester IS NULL";
elseif ($semester === 0)
$where[] = "semester = 0";
} elseif ($studiengang_kz != '') {
$add = $mischen === true ? " OR (studiengang_kz = 0 AND semester IS NULL)" : "";
$where[] = "((studiengang_kz = " . $this->db->escape($studiengang_kz) . " AND semester = " . $this->db->escape($semester) . ") OR (studiengang_kz = " . $this->db->escape($studiengang_kz) . " AND semester = 0) OR (studiengang_kz = 0 AND semester = " . $this->db->escape($semester) . ")" . $add . ")";
if ($studiengang_kz !== '') {
$sgAndSemesterCondition[] = 'studiengang_kz = ' . $this->db->escape($studiengang_kz);
} }
if ($semester !== null && $semester !== 0) {
$sgAndSemesterCondition[] = 'semester = ' . $this->db->escape($semester);
}
$targetedSgAndSemesterCondition = [];
if (!empty($sgAndSemesterCondition)) {
$targetedSgAndSemesterCondition[] = '(' . implode(' AND ', $sgAndSemesterCondition) . ')';
}
if ($mischen === true) {
$targetedSgAndSemesterCondition[] = '(studiengang_kz = 0 AND (semester = 0 OR semester IS NULL))';
if ($semester !== null && $semester !== 0) {
$targetedSgAndSemesterCondition[] = '(studiengang_kz = 0 AND semester = ' . $this->db->escape($semester) . ')';
}
}
$finalSgAndSemesterCondition = implode(' OR ', $targetedSgAndSemesterCondition);
if ($finalSgAndSemesterCondition !== '') {
$where[] = "(" . $finalSgAndSemesterCondition . ")";
}
$this->addJoin('campus.tbl_contentsprache cs', 'content_id'); $this->addJoin('campus.tbl_contentsprache cs', 'content_id');
$where[] = "cs.sichtbar = " . ($sichtbar ? "true" : "false"); if (is_bool($sichtbar) || $sichtbar === 'true' || $sichtbar === 'false') {
$isSichtbar = filter_var($sichtbar, FILTER_VALIDATE_BOOLEAN);
$where[] = "cs.sichtbar = " . ($isSichtbar ? "true" : "false");
}
$where[] = "cs.sprache = (CASE WHEN EXISTS(SELECT 1 FROM campus.tbl_contentsprache cs2 WHERE cs2.content_id=" . $this->dbTable . ".content_id AND sprache=" . $this->db->escape($sprache) . ") THEN " . $this->db->escape($sprache) . " ELSE " . $this->db->escape(DEFAULT_LANGUAGE) . " END)"; $where[] = "cs.sprache = (CASE WHEN EXISTS(SELECT 1 FROM campus.tbl_contentsprache cs2 WHERE cs2.content_id=" . $this->dbTable . ".content_id AND sprache=" . $this->db->escape($sprache) . ") THEN " . $this->db->escape($sprache) . " ELSE " . $this->db->escape(DEFAULT_LANGUAGE) . " END)";
$where[] = "cs.version = (SELECT MAX(version) FROM campus.tbl_contentsprache cs3 WHERE cs3.content_id=" . $this->dbTable . ".content_id AND cs3.sprache = (CASE WHEN EXISTS(SELECT 1 FROM campus.tbl_contentsprache cs2 WHERE cs2.content_id=" . $this->dbTable . ".content_id AND sprache=" . $this->db->escape($sprache) . ") THEN " . $this->db->escape($sprache) . " ELSE " . $this->db->escape(DEFAULT_LANGUAGE) . " END))"; $where[] = "cs.version = (SELECT MAX(version) FROM campus.tbl_contentsprache cs3 WHERE cs3.content_id=" . $this->dbTable . ".content_id AND cs3.sprache = (CASE WHEN EXISTS(SELECT 1 FROM campus.tbl_contentsprache cs2 WHERE cs2.content_id=" . $this->dbTable . ".content_id AND sprache=" . $this->db->escape($sprache) . ") THEN " . $this->db->escape($sprache) . " ELSE " . $this->db->escape(DEFAULT_LANGUAGE) . " END))";
if ($filterForDegreePrograms) {
if (empty($allowedDegreePrograms)) {
$where[] = "1=0";
} else {
$where[] = "studiengang_kz IN (" . implode(',', array_map([$this->db, 'escape'], $allowedDegreePrograms)) . ")";
}
}
$where = implode(" AND ", $where); $where = implode(" AND ", $where);
@@ -110,25 +136,16 @@ class News_model extends DB_Model
} }
public function getNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz = null, $sichtbar = true, $maxalter = 0, $page = 1, $page_size = 10, $all = false, $mischen = true) public function getNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz = null, $sichtbar = true, $page = 1, $page_size = 10, $active = true, $mischen = true, $filterForDegreePrograms = false, $allowedDegreePrograms = [])
{ {
$this->prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxalter, $page, $page_size, $all, $mischen); $this->prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $page, $page_size, $active, $mischen, $filterForDegreePrograms, $allowedDegreePrograms);
// getting the number of rows of the query and adding pagination to the query result // getting the number of rows of the query and adding pagination to the query result
$num_rows = $this->getNumRows(true); $num_rows = $this->getNumRows(true);
$this->addPagination($page, $page_size, $num_rows); $this->addPagination($page, $page_size, $num_rows);
// preparing the query again because every call to get_compiled_select or cour_all_results will add the from clause to the query // preparing the query again because every call to get_compiled_select or cour_all_results will add the from clause to the query
$this->prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxalter, $page, $page_size, $all, $mischen); $this->prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $page, $page_size, $active, $mischen, $filterForDegreePrograms, $allowedDegreePrograms);
return $this->load(); return $this->load();
} }
public function countNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz = null, $sichtbar = true, $maxalter = 0, $page = 1, $page_size = 10, $all = false, $mischen = true)
{
$this->prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxalter, $page, $page_size, $all, $mischen);
return $this->getNumRows();
}
} }
@@ -88,6 +88,30 @@ class Prestudent_model extends DB_Model
return $this->execQuery($query, $parametersArray); return $this->execQuery($query, $parametersArray);
} }
/**
* Gets the Studiengang and current Ausbildungssemester for every
* Prestudent belonging to the same person as the student.
*
* @param string $student_uid
* @return stdClass
*/
public function getStudiengaengAndAusbildungssemesterByStudentUid($student_uid)
{
$query = 'SELECT p.studiengang_kz,
(
SELECT ps.ausbildungssemester
FROM public.tbl_prestudentstatus ps
WHERE ps.prestudent_id = p.prestudent_id
ORDER BY ps.datum DESC, ps.insertamum DESC, ps.ext_id DESC
LIMIT 1
) AS ausbildungssemester
FROM public.tbl_student s
JOIN public.tbl_prestudent p USING (prestudent_id)
WHERE s.student_uid = ?';
return $this->execQuery($query, array($student_uid));
}
/** /**
* updateAufnahmegruppe * updateAufnahmegruppe
*/ */
@@ -885,6 +885,14 @@ class Studiengang_model extends DB_Model
return success($ass); return success($ass);
} }
public function getDegreePrograms() {
$query = "SELECT *
FROM public.tbl_studiengang
ORDER BY public.tbl_studiengang.kurzbzlang";
return $this->execReadOnlyQuery($query);
}
private function addEmailProperty(&$benutzerfunktionen) { private function addEmailProperty(&$benutzerfunktionen) {
if(count($benutzerfunktionen) && defined('DOMAIN')) if(count($benutzerfunktionen) && defined('DOMAIN'))
{ {
@@ -10,6 +10,7 @@ $includesArray = array(
'primevue3' => true, 'primevue3' => true,
'skipID' => '#fhccontent', 'skipID' => '#fhccontent',
'vuedatepicker11' => true, 'vuedatepicker11' => true,
'tinymce5' => true,
'customCSSs' => array( 'customCSSs' => array(
'public/css/components/verticalsplit.css', 'public/css/components/verticalsplit.css',
'public/css/components/searchbar/searchbar.css', 'public/css/components/searchbar/searchbar.css',
@@ -23,7 +24,8 @@ $includesArray = array(
'public/css/components/FormUnderline.css', 'public/css/components/FormUnderline.css',
'public/css/components/abgabetool/abgabe.css', 'public/css/components/abgabetool/abgabe.css',
'public/css/Cis4/Cms.css', 'public/css/Cis4/Cms.css',
'public/css/Cis4/Studium.css' 'public/css/Cis4/Studium.css',
'public/css/components/vue-datepicker.css',
), ),
'customJSs' => array( 'customJSs' => array(
'vendor/npm-asset/primevue/accordion/accordion.min.js', 'vendor/npm-asset/primevue/accordion/accordion.min.js',
@@ -49,6 +51,8 @@ $includesArray = array(
$this->load->view('templates/CISVUE-Header', $includesArray); $this->load->view('templates/CISVUE-Header', $includesArray);
?> ?>
<div id="fhccontent" class="h-100" route=<?php echo $route ?>> <div id="fhccontent" class="h-100" route=<?php echo $route ?>>
<router-view></router-view> <router-view
:permissions="<?= htmlspecialchars(json_encode($permissions ?? [])); ?>"
></router-view>
</div> </div>
<?php $this->load->view('templates/CISVUE-Footer', $includesArray); ?> <?php $this->load->view('templates/CISVUE-Footer', $includesArray); ?>
+5 -5
View File
@@ -86,11 +86,11 @@ echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
<link rel="stylesheet" href="../skin/styles/jquery.css" type="text/css"> <link rel="stylesheet" href="../skin/styles/jquery.css" type="text/css">
<link rel="stylesheet" href="../vendor/components/jqueryui/themes/base/jquery-ui.min.css" type="text/css"> <link rel="stylesheet" href="../vendor/components/jqueryui/themes/base/jquery-ui.min.css" type="text/css">
<link rel="stylesheet" type="text/css" href="../skin/jquery-ui-1.9.2.custom.min.css"> <link rel="stylesheet" type="text/css" href="../skin/jquery-ui-1.9.2.custom.min.css">
<script type="text/javascript" src="../vendor/jquery/jquery1/jquery-1.12.4.min.js"></script> <script type="text/javascript" src="../vendor/jquery/jquery1/jquery-1.12.4.min.js"></script>
<script type="text/javascript" src="../vendor/christianbach/tablesorter/jquery.tablesorter.min.js"></script> <script type="text/javascript" src="../vendor/christianbach/tablesorter/jquery.tablesorter.min.js"></script>
<script type="text/javascript" src="../vendor/components/jqueryui/jquery-ui.min.js"></script> <script type="text/javascript" src="../vendor/components/jqueryui/jquery-ui.min.js"></script>
<script type="text/javascript" src="../include/js/jquery.ui.datepicker.translation.js"></script> <script type="text/javascript" src="../include/js/jquery.ui.datepicker.translation.js"></script>
<script type="text/javascript" src="../vendor/jquery/sizzle/sizzle.js"></script> <script type="text/javascript" src="../vendor/jquery/sizzle/sizzle.js"></script>
<script src="../vendor/components/jqueryui/jquery-ui.min.js" type="text/javascript"></script> <script src="../vendor/components/jqueryui/jquery-ui.min.js" type="text/javascript"></script>
<script type="text/javascript" src="../include/tiny_mce/tiny_mce.js"></script> <script type="text/javascript" src="../include/tiny_mce/tiny_mce.js"></script>
+23
View File
@@ -88,6 +88,29 @@ if (! $rechte->isberechtigt('basis/dms', null, 's', null))
init: function(){ init: function(){
}, },
mySubmit : function (id) { mySubmit : function (id) {
var messageTarget = window.opener;
if (!messageTarget && window.parent !== window)
messageTarget = window.parent;
if (messageTarget && typeof(messageTarget.postMessage) == "function")
{
messageTarget.postMessage(
{
type: "fhcomplete:dms:selected",
dmsId: id
},
window.location.protocol + "//" + window.location.host
);
}
if (
typeof(tinyMCEPopup) == "undefined" ||
!tinyMCEPopup.params ||
typeof(tinyMCEPopup.getWindowArg) != "function"
)
return;
var URL = "dms.php?id="+id; var URL = "dms.php?id="+id;
var win = tinyMCEPopup.getWindowArg("window"); var win = tinyMCEPopup.getWindowArg("window");
+17 -38
View File
@@ -1,45 +1,24 @@
export default { export default {
content(content_id, version=null, sprache=null, sichtbar=null) { content(content_id, version = null, sprache = null, sichtbar = null) {
return this.$fhcApi.get( return this.$fhcApi.get('/api/frontend/v1/Cms/content', {
"/api/frontend/v1/Cms/content", content_id: content_id,
{ ...(version ? { version } : {}),
content_id: content_id, ...(sprache ? { sprache } : {}),
...(version?{version}:{}), ...(sichtbar ? { sichtbar } : {}),
...(sprache?{sprache}:{}), });
...(sichtbar?{sichtbar}:{}), },
}
);
},
//api function used for the news View that renders the html //api function used for the news View that renders the html
getNews(page = 1, page_size = 10, sprache) { getNews(page = 1, page_size = 10, sprache) {
return this.$fhcApi.get( return this.$fhcApi.get('/api/frontend/v1/Cms/getNews', {
"/api/frontend/v1/Cms/getNews", page,
{ page_size,
page, sprache,
page_size, });
sprache,
},
);
}, },
//api function used for the widget component //api function used for the widget component
news(limit) { news(limit) {
return this.$fhcApi.get( return this.$fhcApi.get('/api/frontend/v1/Cms/news', {
"/api/frontend/v1/Cms/news", limit: limit,
{ });
limit: limit
}
);
}, },
getNewsRowCount: function () { };
return this.$fhcApi.get(
"/api/frontend/v1/Cms/getNewsRowCount",
{}
);
},
getNewsExtra: function(){
return this.$fhcApi.get(
"/api/frontend/v1/Cms/getStudiengangInfoForNews",
{}
);
}
}
+10 -22
View File
@@ -16,7 +16,7 @@
*/ */
export default { export default {
content(content_id, version=null, sprache=null, sichtbar=null) { content(content_id, version = null, sprache = null, sichtbar = null) {
return { return {
method: 'get', method: 'get',
url: '/api/frontend/v1/Cms/content', url: '/api/frontend/v1/Cms/content',
@@ -24,41 +24,29 @@ export default {
content_id, content_id,
...(version ? { version } : {}), ...(version ? { version } : {}),
...(sprache ? { sprache } : {}), ...(sprache ? { sprache } : {}),
...(sichtbar ? { sichtbar } : {}) ...(sichtbar ? { sichtbar } : {}),
} },
}; };
}, },
//api function used for the news View that renders the html //api function used for the news View that renders the html
getNews(page = 1, page_size = 10, sprache) { getNews(page = 1, page_size = 10, sprache, filterPublished = true) {
return { return {
method: 'get', method: 'get',
url: '/api/frontend/v1/Cms/getNews', url: '/api/frontend/v1/Cms/getNews',
params: { params: {
page, page,
page_size, page_size,
sprache sprache,
published: filterPublished,
}, },
}; };
}, },
//api function used for the widget component //api function used for the widget component
news(limit) { newsForWidget(limit) {
return { return {
method: 'get', method: 'get',
url: '/api/frontend/v1/Cms/news', url: '/api/frontend/v1/Cms/newsForWidget',
params: { limit } params: { limit },
}; };
}, },
getNewsRowCount() { };
return {
method: 'get',
url: '/api/frontend/v1/Cms/getNewsRowCount'
};
},
getNewsExtra() {
// TODO(chris): seems to be called from nowhere?
return {
method: 'get',
url: '/api/frontend/v1/Cms/getStudiengangInfoForNews'
};
}
};
@@ -0,0 +1,68 @@
/**
* 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 {
getNews(
page = 1,
page_size = 10,
sprache,
publishedFilter = true,
isActive = true,
degreeProgramShortCode = null,
semester = null,
) {
return {
method: 'get',
url: '/api/frontend/v1/NewsAdministrationAPI/getNews',
params: {
page,
page_size,
sprache,
published: publishedFilter,
isActive,
degreeProgramShortCode,
semester,
},
};
},
getNewsItem(newsId) {
return {
method: 'get',
url: `/api/frontend/v1/NewsAdministrationAPI/getNewsItem/${newsId}`,
};
},
storeNewsItem(data) {
return {
method: 'post',
url: '/api/frontend/v1/NewsAdministrationAPI/storeNewsItem',
params: data,
};
},
updateNewsItem(newsId, data) {
return {
method: 'post',
url: `/api/frontend/v1/NewsAdministrationAPI/updateNewsItem/${newsId}`,
params: data,
};
},
deleteNewsItem(newsId) {
return {
method: 'post',
url: `/api/frontend/v1/NewsAdministrationAPI/deleteNewsItem/${newsId}`,
};
},
};
+10 -4
View File
@@ -16,17 +16,23 @@
*/ */
export default { export default {
getDegreePrograms() {
return {
method: 'get',
url: '/api/frontend/v1/Studgang/getDegreePrograms',
};
},
studiengangInformation() { studiengangInformation() {
return { return {
method: 'get', method: 'get',
url: '/api/frontend/v1/Studgang/getStudiengangInfo' url: '/api/frontend/v1/Studgang/getStudiengangInfo',
}; };
}, },
getStudiengangByKz(studiengang_kz) { getStudiengangByKz(studiengang_kz) {
return { return {
method: 'get', method: 'get',
url: '/api/frontend/v1/organisation/StudiengangEP/getStudiengangByKz', url: '/api/frontend/v1/organisation/StudiengangEP/getStudiengangByKz',
params: { studiengang_kz } params: { studiengang_kz },
}; };
} },
}; };
+135 -106
View File
@@ -2,29 +2,35 @@ import FhcDashboard from '../../components/Dashboard/Dashboard.js';
import PluginsPhrasen from '../../plugins/Phrasen.js'; import PluginsPhrasen from '../../plugins/Phrasen.js';
import Theme from '../../plugins/Theme.js'; import Theme from '../../plugins/Theme.js';
import contrast from '../../directives/contrast.js'; import contrast from '../../directives/contrast.js';
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js"; import { setScrollbarWidth } from '../../helpers/CssVarCalcHelpers.js';
import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js"; import LvPlan from '../../components/Cis/LvPlan/Lehrveranstaltung.js';
import MyLvPlan from "../../components/Cis/LvPlan/MyLvPlan.js"; import MyLvPlan from '../../components/Cis/LvPlan/MyLvPlan.js';
import MylvStudent from "../../components/Cis/Mylv/Student.js"; import MylvStudent from '../../components/Cis/Mylv/Student.js';
import Profil from "../../components/Cis/Profil/Profil.js"; import Profil from '../../components/Cis/Profil/Profil.js';
import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js"; import Raumsuche from '../../components/Cis/Raumsuche/Raumsuche.js';
import CmsNews from "../../components/Cis/Cms/News.js"; import CmsNews from '../../components/Cis/Cms/News.js';
import CmsContent from "../../components/Cis/Cms/Content.js"; import NewsAdministration from '../../components/Cis/Cms/NewsAdministration/NewsAdministration.js';
import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js"; import CmsContent from '../../components/Cis/Cms/Content.js';
import RoomInformation, {DEFAULT_MODE_RAUMINFO_DESKTOP, DEFAULT_MODE_RAUMINFO_MOBILE} from "../../components/Cis/Mylv/RoomInformation.js"; import Info from '../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js';
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js"; import RoomInformation, {
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js"; DEFAULT_MODE_RAUMINFO_DESKTOP,
import AbgabetoolAssistenz from "../../components/Cis/Abgabetool/AbgabetoolAssistenz.js"; DEFAULT_MODE_RAUMINFO_MOBILE,
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js"; } from '../../components/Cis/Mylv/RoomInformation.js';
import Studium from "../../components/Cis/Studium/Studium.js"; import AbgabetoolStudent from '../../components/Cis/Abgabetool/AbgabetoolStudent.js';
import StgOrgLvPlan from "../../components/Cis/LvPlan/StgOrg.js"; import AbgabetoolMitarbeiter from '../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js';
import OtherLvPlan from "../../components/Cis/LvPlan/OtherLvPlan.js"; import AbgabetoolAssistenz from '../../components/Cis/Abgabetool/AbgabetoolAssistenz.js';
import DeadlineOverview from '../../components/Cis/Abgabetool/DeadlineOverview.js';
import Studium from '../../components/Cis/Studium/Studium.js';
import StgOrgLvPlan from '../../components/Cis/LvPlan/StgOrg.js';
import OtherLvPlan from '../../components/Cis/LvPlan/OtherLvPlan.js';
import ApiRouteInfo from '../../api/factory/routeinfo.js'; import ApiRouteInfo from '../../api/factory/routeinfo.js';
import {capitalize} from "../../helpers/StringHelpers.js"; import { capitalize } from '../../helpers/StringHelpers.js';
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router; const ciPath =
const isMobile = window.matchMedia("(max-width: 767px)").matches; FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') +
FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const isMobile = window.matchMedia('(max-width: 767px)').matches;
const router = VueRouter.createRouter({ const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(`/${ciPath}`), history: VueRouter.createWebHistory(`/${ciPath}`),
@@ -33,62 +39,66 @@ const router = VueRouter.createRouter({
path: `/Cis/Studium`, path: `/Cis/Studium`,
name: 'Studium', name: 'Studium',
component: Studium, component: Studium,
props: true props: true,
}, },
{ {
path: `/Cis/Profil/View/:uid`, path: `/Cis/Profil/View/:uid`,
name: 'ProfilView', name: 'ProfilView',
component: Profil, component: Profil,
props: true props: true,
}, },
{ {
path: `/Cis/Profil`, path: `/Cis/Profil`,
name: 'Profil', name: 'Profil',
component: Profil, component: Profil,
props: true props: true,
}, },
{ {
path: `/Cis/Abgabetool/Student/:student_uid_prop?`, path: `/Cis/Abgabetool/Student/:student_uid_prop?`,
name: 'AbgabetoolStudent', name: 'AbgabetoolStudent',
component: AbgabetoolStudent, component: AbgabetoolStudent,
props: true props: true,
}, },
{ {
path: `/Cis/Abgabetool/Mitarbeiter`, path: `/Cis/Abgabetool/Mitarbeiter`,
name: 'AbgabetoolMitarbeiter', name: 'AbgabetoolMitarbeiter',
component: AbgabetoolMitarbeiter, component: AbgabetoolMitarbeiter,
props: true props: true,
}, },
{ {
path: `/Cis/Abgabetool/Assistenz/:stg_kz_prop?`, path: `/Cis/Abgabetool/Assistenz/:stg_kz_prop?`,
name: 'AbgabetoolAssistenz', name: 'AbgabetoolAssistenz',
component: AbgabetoolAssistenz, component: AbgabetoolAssistenz,
props: true props: true,
}, },
{ {
path: `/Cis/Abgabetool/Deadlines/:person_uid_prop?`, path: `/Cis/Abgabetool/Deadlines/:person_uid_prop?`,
name: 'DeadlineOverview', name: 'DeadlineOverview',
component: DeadlineOverview, component: DeadlineOverview,
props: true props: true,
}, },
{ {
path: `/Cis/Raumsuche`, path: `/Cis/Raumsuche`,
name: 'Raumsuche', name: 'Raumsuche',
component: Raumsuche, component: Raumsuche,
props: true props: true,
}, },
// Redirect old links to new format // Redirect old links to new format
{ {
path: "/CisVue/Cms/getRoomInformation/:ort_kurzbz", path: '/CisVue/Cms/getRoomInformation/:ort_kurzbz',
name: "RoomInformationOld", name: 'RoomInformationOld',
component: RoomInformation, component: RoomInformation,
redirect: (to) => { redirect: (to) => {
return { // redirect to longer Rauminfo url and map params return {
name: "RoomInformation", // redirect to longer Rauminfo url and map params
params: { // in this case always populate other params since they are not optional name: 'RoomInformation',
params: {
// in this case always populate other params since they are not optional
ort_kurzbz: to.params.ort_kurzbz, ort_kurzbz: to.params.ort_kurzbz,
mode: isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP, mode: isMobile
focus_date: new Date().toISOString().split("T")[0] ? DEFAULT_MODE_RAUMINFO_MOBILE
: DEFAULT_MODE_RAUMINFO_DESKTOP,
focus_date: new Date().toISOString().split('T')[0],
}, },
}; };
}, },
@@ -97,18 +107,28 @@ const router = VueRouter.createRouter({
path: `/CisVue/Cms/getRoomInformation/:mode/:focus_date/:ort_kurzbz`, path: `/CisVue/Cms/getRoomInformation/:mode/:focus_date/:ort_kurzbz`,
name: 'RoomInformation', name: 'RoomInformation',
component: RoomInformation, component: RoomInformation,
props: (route) => { // validate and set mode/focus date if for some reason missing props: (route) => {
const validModes = ["Month", "Week", "Day"]; // validate and set mode/focus date if for some reason missing
const validModes = ['Month', 'Week', 'Day'];
// check mode string // check mode string
const mode = route.params.mode && const mode =
validModes.includes(route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()) route.params.mode &&
? route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase() validModes.includes(
: (isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP); route.params.mode.charAt(0).toUpperCase() +
route.params.mode.slice(1).toLowerCase(),
)
? route.params.mode.charAt(0).toUpperCase() +
route.params.mode.slice(1).toLowerCase()
: isMobile
? DEFAULT_MODE_RAUMINFO_MOBILE
: DEFAULT_MODE_RAUMINFO_DESKTOP;
// default to today date if not provided // default to today date if not provided
const d = new Date(route.params.focus_date) const d = new Date(route.params.focus_date);
const focus_date = !isNaN(d) ? route.params.focus_date : new Date().toISOString().split("T")[0]; const focus_date = !isNaN(d)
? route.params.focus_date
: new Date().toISOString().split('T')[0];
// for consistency reasons format the props into one object but actually use a new name to we dont collide with // for consistency reasons format the props into one object but actually use a new name to we dont collide with
// existing viewData declaration written from codeigniter 3 into routerview tag // existing viewData declaration written from codeigniter 3 into routerview tag
@@ -116,37 +136,44 @@ const router = VueRouter.createRouter({
propsViewData: { propsViewData: {
mode, mode,
focus_date, focus_date,
ort_kurzbz: route.params.ort_kurzbz ort_kurzbz: route.params.ort_kurzbz,
} },
}; };
}, },
beforeEnter: (to, from, next) => { beforeEnter: (to, from, next) => {
// missing mode or focus_date -> set defaults // missing mode or focus_date -> set defaults
if (!to.params.mode || !to.params.focus_date) { if (!to.params.mode || !to.params.focus_date) {
next({ next({
name: "RoomInformation", name: 'RoomInformation',
params: { params: {
mode: to.params.mode || DEFAULT_MODE_RAUMINFO, mode: to.params.mode || DEFAULT_MODE_RAUMINFO,
focus_date: to.params.focus_date || new Date().toISOString().split("T")[0], focus_date:
ort_kurzbz: route.params.ort_kurzbz to.params.focus_date || new Date().toISOString().split('T')[0],
} ort_kurzbz: route.params.ort_kurzbz,
},
}); });
} else { } else {
next(); next();
} }
} },
}, },
{ {
path: `/CisVue/Cms/Content/:content_id`, path: `/CisVue/Cms/Content/:content_id`,
name: 'Content', name: 'Content',
component: CmsContent, component: CmsContent,
props: true props: true,
}, },
{ {
path: `/CisVue/Cms/News`, path: `/CisVue/Cms/News`,
name: 'News', name: 'News',
component: CmsNews, component: CmsNews,
props: true props: true,
},
{
path: `/CisVue/Cms/newsAdministration`,
name: 'NewsAdministration',
component: NewsAdministration,
props: true,
}, },
{ {
path: `/Cis/MyLv/:studiensemester?`, path: `/Cis/MyLv/:studiensemester?`,
@@ -158,23 +185,24 @@ const router = VueRouter.createRouter({
path: `/Cis/MyLv/Info/:studien_semester/:lehrveranstaltung_id`, path: `/Cis/MyLv/Info/:studien_semester/:lehrveranstaltung_id`,
name: 'LvInfo', name: 'LvInfo',
component: Info, component: Info,
props: true props: true,
}, },
// Redirect old links to new format // Redirect old links to new format
{ {
// only trigger on first param being numeric to avoid paths like "LvPlan/Month" entering here // only trigger on first param being numeric to avoid paths like "LvPlan/Month" entering here
path: "/Cis/LvPlan/:lv_id(\\d+)", path: '/Cis/LvPlan/:lv_id(\\d+)',
name: "LvPlanOld", name: 'LvPlanOld',
component: LvPlan, component: LvPlan,
redirect(to) { redirect(to) {
const route = Vue.unref(router.currentRoute); const route = Vue.unref(router.currentRoute);
const { mode, focus_date } = route.params; // keep mode and focus_date if available const { mode, focus_date } = route.params; // keep mode and focus_date if available
return { // redirect to longer LvPlan url and map params return {
name: "LvPlan", // redirect to longer LvPlan url and map params
name: 'LvPlan',
params: { params: {
mode, mode,
focus_date, focus_date,
lv_id: to.params.lv_id lv_id: to.params.lv_id,
}, },
}; };
}, },
@@ -185,9 +213,9 @@ const router = VueRouter.createRouter({
component: LvPlan, component: LvPlan,
props(route) { props(route) {
return { return {
propsViewData: route.params propsViewData: route.params,
}; };
} },
}, },
{ {
path: `/Cis/MyLvPlan/:mode?/:focus_date?`, path: `/Cis/MyLvPlan/:mode?/:focus_date?`,
@@ -195,9 +223,9 @@ const router = VueRouter.createRouter({
component: MyLvPlan, component: MyLvPlan,
props(route) { props(route) {
return { return {
propsViewData: route.params propsViewData: route.params,
}; };
} },
}, },
{ {
path: `/Cis/StgOrgLvPlan/:mode?/:focus_date?/:stgkz?/:sem?/:verband?/:gruppe?`, path: `/Cis/StgOrgLvPlan/:mode?/:focus_date?/:stgkz?/:sem?/:verband?/:gruppe?`,
@@ -205,48 +233,48 @@ const router = VueRouter.createRouter({
component: StgOrgLvPlan, component: StgOrgLvPlan,
props(route) { props(route) {
return { return {
propsViewData: route.params propsViewData: route.params,
}; };
} },
}, },
{ {
path: `/Cis/OtherLvPlan/:otherUid/:mode?/:focus_date?`, path: `/Cis/OtherLvPlan/:otherUid/:mode?/:focus_date?`,
name: "OtherLvPlan", name: 'OtherLvPlan',
component: OtherLvPlan, component: OtherLvPlan,
props(route) { props(route) {
return { return {
propsViewData: route.params propsViewData: route.params,
}; };
} },
}, },
{ {
path: `/Cis4`, path: `/Cis4`,
name: 'Cis4', name: 'Cis4',
component: FhcDashboard, component: FhcDashboard,
props: {dashboard: 'CIS'}, props: { dashboard: 'CIS' },
}, },
{ {
path: `/`, path: `/`,
name: 'FhcDashboard', name: 'FhcDashboard',
component: FhcDashboard, component: FhcDashboard,
props: {dashboard: 'CIS'}, props: { dashboard: 'CIS' },
}, },
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'Fallback', name: 'Fallback',
component: FhcDashboard, component: FhcDashboard,
props: {dashboard: 'CIS'}, props: { dashboard: 'CIS' },
redirect: () => { redirect: () => {
return { return {
name: "Cis4", name: 'Cis4',
params: { params: {
dashboard: 'CIS' dashboard: 'CIS',
}, },
}; };
}, },
}, },
] ],
}) });
const app = Vue.createApp({ const app = Vue.createApp({
name: 'CisApp', name: 'CisApp',
@@ -255,48 +283,49 @@ const app = Vue.createApp({
windowWidth: 0, windowWidth: 0,
}), }),
provide() { provide() {
return { // provide injectable & watchable language property return {
// provide injectable & watchable language property
language: Vue.computed(() => this.$p.user_language), language: Vue.computed(() => this.$p.user_language),
isMobile: Vue.computed(() => this.isMobile), isMobile: Vue.computed(() => this.isMobile),
} };
}, },
computed: { computed: {
isMobile: function() { isMobile: function () {
return (this.windowWidth < 767); return this.windowWidth < 767;
} },
}, },
methods: { methods: {
isInternalRoute(href) { isInternalRoute(href) {
const internalBase = window.location.origin const internalBase = window.location.origin;
return href.startsWith(internalBase); return href.startsWith(internalBase);
}, },
handleClick(event) { handleClick(event) {
const target = event.target.closest('a'); const target = event.target.closest('a');
if(target?.id == 'skiplink') return if (target?.id == 'skiplink') return;
if (target && this.isInternalRoute(target.href)) { if (target && this.isInternalRoute(target.href)) {
const url = new URL(target.href) const url = new URL(target.href);
const path = url.pathname const path = url.pathname;
const base = this.$router.options.history.base const base = this.$router.options.history.base;
const route = path.replace(base, '') || '/' const route = path.replace(base, '') || '/';
// let click event propagate normally if we dont route internally // let click event propagate normally if we dont route internally
const res = this.$router.resolve(route) const res = this.$router.resolve(route);
if(!res?.matched?.length || res.name === 'Fallback') return if (!res?.matched?.length || res.name === 'Fallback') return;
event.preventDefault(); // Prevent browser navigation event.preventDefault(); // Prevent browser navigation
if(this.isMobile) { // toggle the menu if (this.isMobile) {
// toggle the menu
const navMain = document.getElementById('nav-main'); const navMain = document.getElementById('nav-main');
// fix unwanted toggle from off to on for some links on mobile // fix unwanted toggle from off to on for some links on mobile
if(navMain.classList.contains('show')){ if (navMain.classList.contains('show')) {
document.getElementById('nav-main-btn').click(); document.getElementById('nav-main-btn').click();
} }
} }
this.$router.push(route); this.$router.push(route);
} }
}, },
handleWindowResize() { handleWindowResize() {
@@ -308,11 +337,11 @@ const app = Vue.createApp({
}, },
async mounted() { async mounted() {
document.addEventListener('click', this.handleClick); document.addEventListener('click', this.handleClick);
window.addEventListener("resize", this.handleWindowResize); window.addEventListener('resize', this.handleWindowResize);
}, },
beforeUnmount() { beforeUnmount() {
document.removeEventListener('click', this.handleClick); document.removeEventListener('click', this.handleClick);
window.removeEventListener("resize", this.handleWindowResize); window.removeEventListener('resize', this.handleWindowResize);
}, },
}); });
@@ -327,9 +356,9 @@ app.use(router);
app.use(primevue.config.default, { app.use(primevue.config.default, {
zIndex: { zIndex: {
overlay: 9000, overlay: 9000,
tooltip: 8000 tooltip: 8000,
} },
}) });
app.directive('tooltip', primevue.tooltip); app.directive('tooltip', primevue.tooltip);
app.use(PluginsPhrasen); app.use(PluginsPhrasen);
app.use(Theme); app.use(Theme);
+34 -32
View File
@@ -1,4 +1,4 @@
import FhcCalendar from "./Base.js"; import FhcCalendar from './Base.js';
import ApiLvPlan from '../../api/factory/lvPlan.js'; import ApiLvPlan from '../../api/factory/lvPlan.js';
@@ -11,50 +11,48 @@ import ModeMonth from './Mode/Month.js';
import ModeList from './Mode/List.js'; import ModeList from './Mode/List.js';
export default { export default {
name: "CalendarLvPlan", name: 'CalendarLvPlan',
components: { components: {
FhcCalendar FhcCalendar,
},
inject: {
isMobile: {
default: false,
},
}, },
inject: ["isMobile"],
props: { props: {
date: { date: {
type: [Date, String, Number, luxon.DateTime], type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local() default: luxon.DateTime.local(),
}, },
mode: { mode: {
type: String, type: String,
default: 'Week' default: 'Week',
}, },
getPromiseFunc: { getPromiseFunc: {
type: Function, type: Function,
required: true required: true,
} },
}, },
provide() { provide() {
return { return {
shouldCompactEvents: Vue.computed( shouldCompactEvents: Vue.computed(
() => this.$props.mode === "Month" && this.isMobile, () => this.$props.mode === 'Month' && this.isMobile,
),
compactibleEventTypes: Vue.computed(
() => this.compactibleEventTypes,
), ),
compactibleEventTypes: Vue.computed(() => this.compactibleEventTypes),
}; };
}, },
emits: [ emits: ['update:date', 'update:mode', 'update:range'],
"update:date",
"update:mode",
"update:range"
],
data() { data() {
return { return {
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone, timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
modeOptions: { modeOptions: {
day: { day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')), emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')) emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')),
}, },
week: { week: {
collapseEmptyDays: false collapseEmptyDays: false,
}, },
list: { list: {
length: 7, length: 7,
@@ -72,16 +70,18 @@ export default {
return [ return [
{ {
class: 'background-past', class: 'background-past',
end: now.startOf('day') end: now.startOf('day'),
} },
]; ];
return [ return [
{ {
class: 'background-past', class: 'background-past',
end: now, end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false }) label: now
} .startOf('minute')
.toISOTime({ suppressSeconds: true, includeOffset: false }),
},
]; ];
}, },
modes() { modes() {
@@ -100,8 +100,7 @@ export default {
}, },
methods: { methods: {
eventStyle(event) { eventStyle(event) {
if (!event.farbe) if (!event.farbe) return undefined;
return undefined;
return '--event-bg:#' + event.farbe; return '--event-bg:#' + event.farbe;
}, },
updateRange(rangeInterval) { updateRange(rangeInterval) {
@@ -128,10 +127,13 @@ export default {
}, },
setup(props, context) { setup(props, context) {
const rangeInterval = Vue.ref(null); const rangeInterval = Vue.ref(null);
const { events, lv, reset } = useEventLoader(rangeInterval, props.getPromiseFunc);
Vue.watch(lv, newValue => { const { events, lv, reset } = useEventLoader(
rangeInterval,
props.getPromiseFunc,
);
Vue.watch(lv, (newValue) => {
context.emit('update:lv', newValue); context.emit('update:lv', newValue);
}); });
@@ -142,14 +144,14 @@ export default {
events, events,
lv, lv,
reset, reset,
renderers renderers,
}; };
}, },
async created() { async created() {
await this.getStunden(); await this.getStunden();
await this.getCompactibleEventTypes(); await this.getCompactibleEventTypes();
}, },
template: /* html */` template: /* html */ `
<fhc-calendar <fhc-calendar
ref="calendar" ref="calendar"
class="fhc-calendar-lvplan" class="fhc-calendar-lvplan"
@@ -203,5 +205,5 @@ export default {
<template #actions> <template #actions>
<slot /> <slot />
</template> </template>
</fhc-calendar>` </fhc-calendar>`,
} };
+38 -38
View File
@@ -1,30 +1,36 @@
import Pagination from "../../Pagination/Pagination.js"; import Pagination from '../../Pagination/Pagination.js';
import StudiengangInformation from "./StudiengangInformation/StudiengangInformation.js"; import StudiengangInformation from './StudiengangInformation/StudiengangInformation.js';
import BsConfirm from "../../Bootstrap/Confirm.js"; import BsConfirm from '../../Bootstrap/Confirm.js';
import ApiCms from "../../../api/factory/cms.js"; import ApiCms from '../../../api/factory/cms.js';
export default { export default {
name: "NewsComponent", name: 'NewsComponent',
components: { components: {
Pagination, Pagination,
StudiengangInformation, StudiengangInformation,
}, },
inject: ["isMobile"], inject: ['isMobile'],
props: {
permissions: Object,
},
data() { data() {
return { return {
content: null, content: null,
maxPageCount: 0, maxPageCount: 0,
page_size: 10, page_size: 100,
page: 1, page: 1,
}; };
}, },
watch: { watch: {
"$p.user_language.value": function (sprache) { '$p.user_language.value': function (sprache) {
this.fetchNews(); this.fetchNews();
}, },
}, },
computed: { computed: {
hasBasisNewsTypRPermission() {
return this.permissions['basis/news_r'] || false;
},
sprache: function () { sprache: function () {
return this.$p.user_language.value; return this.$p.user_language.value;
}, },
@@ -35,9 +41,10 @@ export default {
ApiCms.getNews(this.page, this.page_size, this.sprache), ApiCms.getNews(this.page, this.page_size, this.sprache),
); );
this.content = newsResponse.data; this.content = newsResponse.data;
this.maxPageCount = newsResponse.meta.row_count;
document.querySelectorAll("#cms [data-confirm]").forEach((el) => { document.querySelectorAll('#cms [data-confirm]').forEach((el) => {
el.addEventListener("click", (evt) => { el.addEventListener('click', (evt) => {
evt.preventDefault(); evt.preventDefault();
BsConfirm.popup(el.dataset.confirm) BsConfirm.popup(el.dataset.confirm)
.then(() => { .then(() => {
@@ -46,12 +53,12 @@ export default {
// TODO(chris): check for success then show message and/or reload // TODO(chris): check for success then show message and/or reload
location = location; location = location;
}) })
.catch((err) => console.error("ERROR:", err)); .catch((err) => console.error('ERROR:', err));
}) })
.catch(() => {}); .catch(() => {});
}); });
}); });
document.querySelectorAll("#cms [data-href]").forEach((el) => { document.querySelectorAll('#cms [data-href]').forEach((el) => {
el.href = el.dataset.href.replace( el.href = el.dataset.href.replace(
/^ROOT\//, /^ROOT\//,
FHC_JS_DATA_STORAGE_OBJECT.app_root, FHC_JS_DATA_STORAGE_OBJECT.app_root,
@@ -73,48 +80,41 @@ export default {
}, },
formatExternalHtml() { formatExternalHtml() {
document document
.querySelectorAll(".news-list-item .card-header") .querySelectorAll('.news-list-item .card-header')
.forEach((el) => { .forEach((el) => {
el.classList.add("fhc-primary"); el.classList.add('fhc-primary');
}); });
document.querySelectorAll(".news-list-item .row").forEach((el) => { document.querySelectorAll('.news-list-item .row').forEach((el) => {
el.classList.add("w-100"); el.classList.add('w-100');
el.classList.add("align-items-center"); el.classList.add('align-items-center');
});
document.querySelectorAll('.news-list-item .row h2').forEach((el) => {
el.classList.add('mb-0');
}); });
document
.querySelectorAll(".news-list-item .row h2")
.forEach((el) => {
el.classList.add("mb-0");
});
}, },
afterPageUpdated(event) { afterPageUpdated(event) {
this.page = event.page; this.page = event.page;
this.page_size = event.rows; this.page_size = event.rows;
this.$refs.newsPageHeading.scrollIntoView({block: 'end'}); this.$refs.newsPageHeading.scrollIntoView({ block: 'end' });
this.loadNewPageContent(event); this.loadNewPageContent(event);
}, },
}, },
created() { created() {
this.fetchNews(); this.fetchNews();
this.$api
.call(ApiCms.getNewsRowCount())
.then((res) => res.data)
.then((result) => {
this.maxPageCount = result;
});
}, },
template: /*html*/ ` template: /*html*/ `
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden"> <div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
<h2 ref="newsPageHeading" class="fhc-primary-color">News</h2> <div class="d-flex justify-content-between align-items-center">
<h2 ref="newsPageHeading" class="fhc-primary-color">News</h2>
<router-link
v-if="hasBasisNewsTypRPermission"
class="btn btn-primary mb-3"
:to="{ name: 'NewsAdministration' }"
>
{{ $p.t('ui', 'newsAdministration') }} <i class="fas fa-cog m-0"></i>
</router-link>
</div>
<hr/> <hr/>
<pagination
v-if="content?true:false"
:page="page"
:page_size="page_size"
@pageUpdated="afterPageUpdated($event)"
:maxPageCount="maxPageCount"
></pagination>
<div class="container-fluid mt-4"> <div class="container-fluid mt-4">
<div class="row"> <div class="row">
<div class="col" v-html="content"> <div class="col" v-html="content">
@@ -0,0 +1,208 @@
import NewsItemForm from './NewsItemForm.js';
import NewsItemPreview from './NewsItemPreview.js';
import NewsList from './NewsList.js';
import ApiNewsAdministration from '../../../../api/factory/newsAdministration.js';
export default {
name: 'NewsAdministration',
components: {
NewsItemForm,
NewsItemPreview,
NewsList,
},
props: {
permissions: Object,
},
data() {
return {
isNewsFormShown: false,
isNewsPreviewShown: false,
editableNewsItem: null,
newsPreview: null,
newsFormCollapse: null,
};
},
watch: {
isNewsFormShown: async function (isShown) {
await this.$nextTick();
this.newsFormCollapse?.[isShown ? 'show' : 'hide']();
},
'$route.query.newsId': {
immediate: true,
handler(newNewsId, oldNewsId) {
if (!newNewsId) {
this.isNewsFormShown = false;
this.isNewsPreviewShown = false;
this.editableNewsItem = null;
this.newsPreview = null;
return;
}
this.editNewsItem({ newsId: this.$route.query.newsId });
},
},
},
computed: {
hasBasisNewsTypRPermission() {
return this.permissions['basis/news_r'] || false;
},
hasBasisNewsTypWPermission() {
return this.permissions['basis/news_w'] || false;
},
},
methods: {
async scrollToNewsForm() {
await this.$nextTick();
this.$refs.newsItemForm?.$el?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
},
async reloadNews() {
await this.$refs.newsList.fetchNews();
},
async showCreateNewsForm() {
this.editableNewsItem = null;
await this.$refs.newsItemForm?.fillFormData();
await this.$nextTick();
this.isNewsPreviewShown = false;
this.isNewsFormShown = true;
this.scrollToNewsForm();
},
async hideNewsForm() {
this.isNewsFormShown = false;
this.isNewsPreviewShown = false;
this.editableNewsItem = null;
await this.$refs.newsItemForm?.fillFormData();
this.newsPreview = null;
},
handleNewsItemSaved() {
this.isNewsFormShown = false;
this.isNewsPreviewShown = false;
this.editableNewsItem = null;
this.newsPreview = null;
this.reloadNews();
this.$refs.newsItemForm?.fillFormData();
},
async handleNewsItemUpdated() {
this.isNewsFormShown = false;
this.isNewsPreviewShown = false;
this.editableNewsItem = null;
this.newsPreview = null;
this.reloadNews();
this.$refs.newsItemForm?.fillFormData();
},
async loadNewsItem(newsId) {
if (!newsId) {
return;
}
try {
const response = await this.$api.call(
ApiNewsAdministration.getNewsItem(newsId),
);
const newsItem = response.data;
if (!newsItem) {
return null;
}
return newsItem;
} catch (error) {
this.$fhcAlert.handleSystemError(error);
}
},
async editNewsItem(newsItem) {
try {
this.editableNewsItem = await this.loadNewsItem(newsItem.newsId);
this.isNewsPreviewShown = false;
await this.$nextTick();
setTimeout(() => {
this.isNewsFormShown = true;
this.scrollToNewsForm();
}, 100);
} catch (error) {
this.$fhcAlert.handleSystemError(error);
}
},
toggleNewsPreview() {
this.isNewsPreviewShown = !this.isNewsPreviewShown;
},
handlePreviewChange(preview) {
this.newsPreview = preview;
},
},
created() {
this.$p.loadCategory('ui').then(() => {
this.phrasesLoaded = true;
});
},
mounted() {
this.newsFormCollapse = new bootstrap.Collapse(
this.$refs.newsFormCollapse,
{ toggle: false },
);
if (this.isNewsFormShown) {
this.newsFormCollapse.show();
}
},
beforeUnmount() {
this.newsFormCollapse?.dispose();
},
template: /*html*/ `
<div class="pb-3 overflow-x-hidden">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 ref="newsPageHeading" class="fhc-primary-color mb-0">{{ $capitalize($p.t('ui', 'newsAdministration')) }}</h2>
<button
v-if="!isNewsFormShown && hasBasisNewsTypWPermission"
@click="showCreateNewsForm"
type="button"
class="btn btn-primary rounded-circle d-inline-flex align-items-center justify-content-center p-0"
style="width: 2.5rem; height: 2.5rem"
:title="$capitalize($p.t('ui', 'createNews'))"
:aria-label="$capitalize($p.t('ui', 'createNews'))"
aria-controls="news-item-form-collapse"
>
<i class="fa-solid fa-plus" aria-hidden="true"></i>
</button>
</div>
<div ref="newsFormCollapse" id="news-item-form-collapse" class="collapse">
<div class="row g-4 align-items-start">
<div :class="isNewsPreviewShown ? 'col-12 col-xl-6' : 'col-12'">
<news-item-form
ref="newsItemForm"
:key="editableNewsItem?.newsId || 'new-news-item'"
id="news-item-form"
:news-item="editableNewsItem"
:is-preview-shown="isNewsPreviewShown"
@created="handleNewsItemSaved"
@update="handleNewsItemUpdated"
@cancel="hideNewsForm"
@preview-change="handlePreviewChange"
@toggle-preview="toggleNewsPreview"
></news-item-form>
</div>
<div v-if="isNewsPreviewShown" class="col-12 col-xl-6">
<news-item-preview
:form-data="newsPreview?.formData"
:active-language="newsPreview?.activeLanguage"
></news-item-preview>
</div>
</div>
</div>
<news-list
v-if="hasBasisNewsTypRPermission"
ref="newsList"
@edit="editNewsItem"
></news-list>
</div>
`,
};
@@ -0,0 +1,31 @@
export default {
name: 'NewsItemAddLanguageTab',
emits: ['tab-action'],
props: {
config: {
type: Object,
default: () => ({}),
},
},
created() {
this.$p.loadCategory('ui').then(() => {
this.phrasesLoaded = true;
});
},
template: /*html*/ `
<div class="py-4 text-center text-muted">
<div v-if="config?.languages?.length" class="d-flex justify-content-center gap-2">
<button
v-for="language in config.languages"
:key="language.value"
type="button"
class="btn btn-outline-primary text-start"
@click="$emit('tab-action', { action: 'add-language', language: language.value })"
>
{{ language.label }}
</button>
</div>
<p v-else class="mb-0 text-muted">{{ $capitalize($p.t('ui', 'allLanguagesAdded')) }}</p>
</div>
`,
};
@@ -0,0 +1,239 @@
import FormInput from '../../../Form/Input.js';
export default {
name: 'NewsItemContentForm',
components: {
FormInput,
},
emits: ['update:modelValue', 'tab-action'],
props: {
config: {
type: Object,
default: () => ({}),
},
modelValue: {
type: Array,
default: () => [],
},
},
data() {
const translation = this.modelValue.find(
(item) => item.language === this.config.language,
);
return {
editor: null,
dmsFileBrowser: null,
dmsFileBrowserClosePollId: null,
dmsFileBrowserMessageHandler: null,
contentData: {
language: this.config.language,
author: '',
title: '',
text: '',
isPublished: false,
...translation,
},
};
},
watch: {
contentData: {
deep: true,
handler(contentData) {
const translations = this.modelValue.map((item) => ({ ...item }));
const index = translations.findIndex(
(item) => item.language === this.config.language,
);
const translation = {
...contentData,
language: this.config.language,
};
if (index === -1) {
translations.push(translation);
} else {
translations[index] = translation;
}
this.$emit('update:modelValue', translations);
},
},
},
methods: {
cleanupDmsFileBrowser(closePopup = false) {
if (this.dmsFileBrowserClosePollId !== null) {
window.clearInterval(this.dmsFileBrowserClosePollId);
this.dmsFileBrowserClosePollId = null;
}
if (this.dmsFileBrowserMessageHandler) {
window.removeEventListener(
'message',
this.dmsFileBrowserMessageHandler,
);
this.dmsFileBrowserMessageHandler = null;
}
if (closePopup && this.dmsFileBrowser && !this.dmsFileBrowser.closed) {
this.dmsFileBrowser.close();
}
this.dmsFileBrowser = null;
},
openDmsFileBrowser(callback, meta) {
this.cleanupDmsFileBrowser(true);
const appRoot =
FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/\/+$/, '') + '/';
const fileType = meta && meta.filetype ? meta.filetype : 'file';
const fileBrowserUrl =
appRoot + 'cms/tinymce_dms.php?type=' + encodeURIComponent(fileType);
const popupName = this.editor
? 'FHCFileBrowser_' + this.editor.id
: 'FHCFileBrowser';
const popup = window.open(
fileBrowserUrl,
popupName,
'width=750,height=550,resizable=yes,scrollbars=yes',
);
if (!popup) {
return;
}
this.dmsFileBrowser = popup;
this.dmsFileBrowserMessageHandler = (event) => {
const message = event.data;
if (
event.origin !== window.location.origin ||
event.source !== popup ||
this.dmsFileBrowser !== popup ||
!message ||
message.type !== 'fhcomplete:dms:selected' ||
!Number.isInteger(message.dmsId) ||
message.dmsId <= 0
) {
return;
}
try {
callback(
appRoot + 'cms/dms.php?id=' + encodeURIComponent(message.dmsId),
);
} finally {
this.cleanupDmsFileBrowser(true);
}
};
window.addEventListener('message', this.dmsFileBrowserMessageHandler);
this.dmsFileBrowserClosePollId = window.setInterval(() => {
if (this.dmsFileBrowser === popup && popup.closed) {
this.cleanupDmsFileBrowser();
}
}, 250);
popup.focus();
},
initTinyMCE() {
const vm = this;
tinymce.init({
target: this.$refs.editor.$refs.input, // Important: enables multiple component instances
height: 500,
toolbar:
'styleselect fontsizeselect | bold italic underline | alignleft aligncenter alignright alignjustify | link unlink image | bullist | pastetext',
plugins: 'lists link image paste',
paste_as_text: true,
statusbar: true,
resize: true,
file_picker_types: 'file image',
file_picker_callback: (callback, _value, meta) => {
vm.openDmsFileBrowser(callback, meta);
},
style_formats: [
{ title: vm.$capitalize(vm.$p.t('ui', 'blocks')), block: 'div' },
{ title: vm.$capitalize(vm.$p.t('ui', 'paragraph')), block: 'p' },
{ title: vm.$capitalize(vm.$p.t('ui', 'heading1')), block: 'h1' },
{ title: vm.$capitalize(vm.$p.t('ui', 'heading2')), block: 'h2' },
{ title: vm.$capitalize(vm.$p.t('ui', 'heading3')), block: 'h3' },
{ title: vm.$capitalize(vm.$p.t('ui', 'heading4')), block: 'h4' },
{ title: vm.$capitalize(vm.$p.t('ui', 'heading5')), block: 'h5' },
{ title: vm.$capitalize(vm.$p.t('ui', 'heading6')), block: 'h6' },
],
setup: (editor) => {
vm.editor = editor;
editor.on('input change undo redo', () => {
vm.contentData.text = editor.getContent();
});
},
});
},
},
async mounted() {
await this.$p.loadCategory(['global', 'ui', 'notiz']);
setTimeout(() => {
if (this.$refs.editor?.$refs.input) {
this.initTinyMCE();
}
}, 100);
},
beforeUnmount() {
this.cleanupDmsFileBrowser(true);
if (this.editor) {
this.editor.destroy();
}
},
template: /*html*/ `
<div class="position-relative overflow-x-hidden">
<button
v-if="config.language !== 'German'"
type="button"
class="btn btn-sm btn-outline-danger position-absolute top-0 end-0"
:title="$capitalize($p.t('ui', 'removeLanguage'))"
:aria-label="$capitalize($p.t('ui', 'removeLanguage'))"
@click="$emit('tab-action', { action: 'remove-language', language: config.language })"
>
<i class="fa-solid fa-trash-can" aria-hidden="true"></i>
</button>
<div class="d-flex flex-column flex-md-row align-items-md-end gap-3">
<form-input
type="text"
:label="$capitalize($p.t('notiz', 'verfasser'))"
name="author"
v-model="contentData.author"
>
</form-input>
<form-input
type="text"
:label="$capitalize($p.t('global', 'betreff'))"
name="title"
v-model="contentData.title"
>
</form-input>
<form-input
type="checkbox"
:label="$capitalize($p.t('ui', 'publish'))"
name="isPublished"
v-model="contentData.isPublished"
>
</form-input>
</div>
<form-input
ref="editor"
:label="$capitalize($p.t('global', 'text') + ' *')"
type="textarea"
name="text"
v-model="contentData.text"
rows="5"
cols="75"
style="min-height: 500px"
>
</form-input>
</div>
`,
};
@@ -0,0 +1,117 @@
import BootstrapModal from '../../../Bootstrap/Modal.js';
export default {
name: 'NewsItemCopyTranslationModal',
components: {
BootstrapModal,
},
emits: ['copy', 'hidden'],
props: {
sourceLanguages: {
type: Array,
default: () => [],
},
targetLanguageLabel: {
type: String,
default: '',
},
targetHasContent: Boolean,
},
data() {
return {
selectedSourceLanguage: null,
};
},
methods: {
show() {
this.selectedSourceLanguage = null;
this.$refs.modal.show();
},
hide() {
this.$refs.modal.hide();
},
selectSourceLanguage(language) {
if (this.targetHasContent) {
this.selectedSourceLanguage = language;
return;
}
this.copyTranslation(language);
},
copyTranslation(language = this.selectedSourceLanguage) {
const sourceLanguage = language?.value ?? language;
if (!sourceLanguage) {
return;
}
this.$emit('copy', sourceLanguage);
this.hide();
},
resetSelection() {
this.selectedSourceLanguage = null;
this.$emit('hidden');
},
getCopyingTranslationText() {
return this.$capitalize(
this.$p.t('ui', 'copyingTranslation', {
source: this.selectedSourceLanguage?.label ?? '',
target: this.targetLanguageLabel,
}),
);
},
getChooseLanguageText() {
return this.$capitalize(
this.$p.t('ui', 'chooseLanguageToCopyContent', {
target: this.targetLanguageLabel,
}),
);
},
},
created() {
this.$p.loadCategory('ui').then(() => {
this.phrasesLoaded = true;
});
},
template: /*html*/ `
<bootstrap-modal
ref="modal"
dialog-class="modal-dialog-centered"
body-class="p-4"
@hidden-bs-modal="resetSelection"
>
<template #title>
{{ $capitalize(selectedSourceLanguage ? $p.t('ui', 'overwriteTranslationContent') : $p.t('ui', 'copyTranslationContent')) }}
</template>
<template v-if="selectedSourceLanguage">
<p class="mb-3">
{{ getCopyingTranslationText() }}
</p>
<p class="text-muted small">{{ $capitalize($p.t('ui', 'existingContentWillBeLost')) }}</p>
<div class="d-flex justify-content-end gap-2 mt-4">
<button type="button" class="btn btn-outline-secondary" @click="selectedSourceLanguage = null">
{{ $capitalize($p.t('ui', 'chooseAnotherLanguage')) }}
</button>
<button type="button" class="btn btn-primary" @click="copyTranslation()">
{{ $capitalize($p.t('ui', 'overwriteAndCopy')) }}
</button>
</div>
</template>
<template v-else>
<p class="text-muted">{{ getChooseLanguageText() }}</p>
<div v-if="sourceLanguages.length" class="d-grid gap-2">
<button
v-for="language in sourceLanguages"
:key="language.value"
type="button"
class="btn btn-outline-primary text-start"
@click="selectSourceLanguage(language)"
>
{{ language.label }}
</button>
</div>
<p v-else class="mb-0 text-muted">{{ $capitalize($p.t('ui', 'addLanguageBeforeCopying')) }}</p>
</template>
</bootstrap-modal>
`,
};
@@ -0,0 +1,614 @@
import CoreForm from '../../../Form/Form.js';
import FormInput from '../../../Form/Input.js';
import FhcTabs from '../../../Tabs.js';
import NewsItemCopyTranslationModal from './NewsItemCopyTranslationModal.js';
import ApiStudiengang from '../../../../api/factory/studiengang.js';
import ApiNewsAdministration from '../../../../api/factory/newsAdministration.js';
const BASE_COMPONENT_URL =
FHC_JS_DATA_STORAGE_OBJECT.app_root +
'public/js/components/Cis/Cms/NewsAdministration/';
export default {
name: 'NewsItemForm',
components: {
CoreForm,
FormInput,
FhcTabs,
NewsItemCopyTranslationModal,
},
emits: [
'created',
'update',
'cancel',
'translation-copied',
'preview-change',
'toggle-preview',
],
props: {
newsItem: {
type: Object,
default: null,
},
isPreviewShown: Boolean,
},
data() {
return {
degreePrograms: [],
filteredDegreePrograms: [],
isSaving: false,
activeContentFormKey: 'germanContentForm',
contentFormRenderKey: 0,
formData: {
visibleFrom: null,
visibleTo: null,
degreeProgram: null,
semester: null,
translations: [],
},
contentFormItems: {},
};
},
watch: {
'$p.user_language.value': function () {
Object.values(this.contentFormItems).forEach((item) => {
if (item.config?.language) {
item.title = this.getContentFormTitle(item.config.language);
}
});
},
formData: {
deep: true,
immediate: true,
handler() {
this.emitPreviewChange();
},
},
activeContentLanguage() {
this.emitPreviewChange();
},
},
computed: {
sprache: function () {
return this.$p.user_language.value;
},
availableLanguages() {
this.sprache;
return FHC_JS_DATA_STORAGE_OBJECT?.server_languages?.map(
(languageObject) => ({
label: this.getLanguageLabel(languageObject.sprache),
value: languageObject.sprache,
}),
);
},
semesters() {
this.sprache;
const semesterLabel = this.$capitalize(this.$p.t('lehre', 'semester'));
return [
{
label: this.$capitalize(this.$p.t('ui', 'all_semester')),
value: null,
},
...Array.from({ length: 8 }, (_, index) => ({
label: `${index + 1}. ${semesterLabel}`,
value: String(index + 1),
})),
];
},
dropdownParsedDegreePrograms() {
return this.degreePrograms
.map((degreeProgram) => {
let labelFragment = degreeProgram.typ + degreeProgram.kurzbz;
labelFragment = labelFragment.trim().toUpperCase();
return {
label: `${labelFragment} (${degreeProgram.bezeichnung})`,
value: degreeProgram.studiengang_kz,
};
})
.sort((a, b) => a.label.localeCompare(b.label));
},
visibleContentLanguages() {
return Object.values(this.contentFormItems)
.map((item) => item.config?.language)
.filter(Boolean);
},
languagesToAdd() {
return this.availableLanguages.filter(
(language) => !this.visibleContentLanguages.includes(language.value),
);
},
activeContentLanguage() {
return this.contentFormItems[this.activeContentFormKey]?.config?.language;
},
copySourceLanguages() {
return this.visibleContentLanguages
.filter((language) => language !== this.activeContentLanguage)
.map((language) => ({
label: this.getLanguageLabel(language),
value: language,
}));
},
activeTranslationHasContent() {
return this.hasTranslationContent(
this.getTranslation(this.activeContentLanguage),
);
},
},
methods: {
emitPreviewChange() {
this.$emit('preview-change', {
formData: this.formData,
activeLanguage: this.activeContentLanguage,
});
},
getContentFormKey(language) {
return `${language.toLowerCase()}ContentForm`;
},
createContentFormItem(language) {
const key = this.getContentFormKey(language);
return {
title: this.getContentFormTitle(language),
component: BASE_COMPONENT_URL + 'NewsItemContentForm.js?124',
config: {
language,
type: 'news',
},
key,
};
},
createAddLanguageTab() {
return {
title: '+',
component: BASE_COMPONENT_URL + 'NewsItemAddLanguageTab.js',
config: { languages: [] },
key: 'addLanguage',
};
},
showCopyTranslationModal() {
this.$refs.copyTranslationModal.show();
},
getLanguageLabel(language) {
const languagePhrases = {
German: ['global', 'deutsch'],
English: ['global', 'englisch'],
French: ['ui', 'franzoesisch'],
Spanish: ['ui', 'spanisch'],
};
const phrase = languagePhrases[language];
return phrase
? this.$capitalize(this.$p.t(phrase[0], phrase[1]))
: language;
},
getContentFormTitle(language) {
return this.$capitalize(
this.$p.t('ui', 'contentFormTitle', {
language: this.getLanguageLabel(language),
}),
);
},
getTranslation(language) {
return this.formData.translations.find(
(translation) => translation.language === language,
);
},
hasTranslationContent(translation) {
return ['author', 'title', 'text'].some(
(field) => String(translation?.[field] ?? '').trim() !== '',
);
},
copyTranslation(sourceLanguage) {
const sourceTranslation = this.getTranslation(sourceLanguage);
const targetTranslation = this.getTranslation(this.activeContentLanguage);
if (!sourceTranslation || !targetTranslation) {
return;
}
Object.assign(targetTranslation, {
author: sourceTranslation.author,
title: sourceTranslation.title,
text: sourceTranslation.text,
});
this.contentFormRenderKey += 1;
this.$emit('translation-copied', {
from: sourceLanguage,
to: this.activeContentLanguage,
});
},
handleContentTabChange(key) {
this.activeContentFormKey = key;
},
handleContentTabAction({ key, payload }) {
if (payload?.action === 'add-language') {
this.addLanguage(payload.language);
} else if (payload?.action === 'remove-language') {
this.removeLanguage(payload.language);
}
},
removeLanguage(language) {
const key = this.getContentFormKey(language);
if (!this.contentFormItems[key]) {
return;
}
const contentFormItems = { ...this.contentFormItems };
const addLanguageTab = contentFormItems.addLanguage;
delete contentFormItems[key];
delete contentFormItems.addLanguage;
const remainingLanguages = Object.values(contentFormItems)
.map((item) => item.config?.language)
.filter(Boolean);
contentFormItems.addLanguage = {
...addLanguageTab,
config: {
hasAvailableLanguages: this.availableLanguages.some(
(availableLanguage) =>
!remainingLanguages.includes(availableLanguage.value),
),
},
};
this.contentFormItems = contentFormItems;
this.formData.translations = this.formData.translations.filter(
(translation) => translation.language !== language,
);
const nextContentFormKey = Object.keys(contentFormItems).find(
(itemKey) => itemKey !== 'addLanguage',
);
this.activeContentFormKey = nextContentFormKey ?? 'addLanguage';
if (nextContentFormKey) {
this.$nextTick(() => this.$refs.tabs.change(nextContentFormKey));
}
},
addLanguage(language) {
const key = this.getContentFormKey(language);
if (this.contentFormItems[key]) {
return;
}
const addLanguageTab = this.contentFormItems.addLanguage;
const contentFormItems = { ...this.contentFormItems };
delete contentFormItems.addLanguage;
contentFormItems[key] = this.createContentFormItem(language);
contentFormItems.addLanguage = {
...addLanguageTab,
config: {
hasAvailableLanguages: this.availableLanguages.some(
(availableLanguage) =>
availableLanguage.value !== language &&
!this.visibleContentLanguages.includes(availableLanguage.value),
),
},
};
this.contentFormItems = contentFormItems;
if (
!this.formData.translations.some((item) => item.language === language)
) {
this.formData.translations.push({
language,
author: '',
title: '',
text: '',
isPublished: false,
});
}
this.activeContentFormKey = key;
this.$nextTick(() => this.$refs.tabs.change(key));
},
restoreActiveContentTab() {
this.$refs.tabs.change(this.activeContentFormKey);
},
fillFormData(newsItem) {
this.formData.visibleFrom =
newsItem?.visibleFrom ?? newsItem?.dateTime?.slice(0, 10) ?? null;
this.formData.visibleTo = newsItem?.visibleTo ?? null;
const degreeProgramShortCode =
newsItem?.degreeProgramShortCode ??
newsItem?.degreeProgram?.value ??
null;
this.formData.degreeProgram =
this.dropdownParsedDegreePrograms.find(
(degreeProgram) =>
String(degreeProgram.value) === String(degreeProgramShortCode),
) ??
(degreeProgramShortCode === null
? null
: {
label: String(degreeProgramShortCode),
value: degreeProgramShortCode,
});
this.formData.semester = newsItem?.semester ?? null;
const translations = newsItem?.translations ?? [
{
language: newsItem?.language ?? 'German',
author: newsItem?.author ?? '',
title: newsItem?.title ?? '',
text: newsItem?.content ?? '',
isPublished: Boolean(newsItem?.isPublished),
},
];
this.formData.translations = translations.map((sourceTranslation) => ({
language: sourceTranslation.language,
author: sourceTranslation.author ?? '',
title: sourceTranslation.title ?? '',
text: sourceTranslation.text ?? '',
isPublished: Boolean(sourceTranslation.isPublished),
}));
const contentFormItems = {};
this.formData.translations.forEach((translation) => {
if (translation.language) {
contentFormItems[this.getContentFormKey(translation.language)] =
this.createContentFormItem(translation.language);
}
});
contentFormItems.addLanguage = this.createAddLanguageTab();
this.contentFormItems = contentFormItems;
this.activeContentFormKey =
Object.keys(contentFormItems).find((key) => key !== 'addLanguage') ??
'addLanguage';
},
filterDegreePrograms(event) {
const query = event.query.toLowerCase();
return (this.filteredDegreePrograms =
this.dropdownParsedDegreePrograms.filter((unit) => {
return unit.label.toLowerCase().includes(query);
}));
},
async storeNewsItem() {
if (this.isSaving) {
return;
}
let parsedFormData = JSON.parse(JSON.stringify(this.formData));
parsedFormData.degreeProgramShortCode =
parsedFormData.degreeProgram?.value;
delete parsedFormData.degreeProgram;
this.isSaving = true;
try {
const response = await this.$refs.form.call(
ApiNewsAdministration.storeNewsItem(parsedFormData),
);
if (response.meta.status !== 'success') {
this.$fhcAlert.alertError(
this.$capitalize(this.$p.t('ui', 'fehlerBeimSpeichern')),
);
return;
}
this.$fhcAlert.alertSuccess(
this.$capitalize(this.$p.t('ui', 'gespeichert')),
);
this.$emit('created');
} catch (error) {
if (!error?.handled) {
this.$fhcAlert.handleSystemError(error);
}
} finally {
this.isSaving = false;
}
},
async updateNewsItem() {
if (!this.newsItem || this.isSaving) {
return;
}
let parsedFormData = JSON.parse(JSON.stringify(this.formData));
parsedFormData.degreeProgramShortCode =
parsedFormData.degreeProgram?.value;
delete parsedFormData.degreeProgram;
this.isSaving = true;
try {
const response = await this.$refs.form.call(
ApiNewsAdministration.updateNewsItem(
this.newsItem.newsId,
parsedFormData,
),
);
if (response.meta.status !== 'success') {
this.$fhcAlert.alertError(
this.$capitalize(this.$p.t('ui', 'fehlerBeimSpeichern')),
);
return;
}
this.$fhcAlert.alertSuccess(
this.$capitalize(this.$p.t('ui', 'gespeichert')),
);
this.$emit('update');
} catch (error) {
if (!error?.handled) {
this.$fhcAlert.handleSystemError(error);
}
} finally {
this.isSaving = false;
}
},
},
async created() {
await this.$p.loadCategory(['global', 'ui', 'lehre']);
let getAllDegreePrograms = await this.$api.call(
ApiStudiengang.getDegreePrograms(),
);
if (getAllDegreePrograms.meta.status === 'success') {
this.degreePrograms = getAllDegreePrograms.data.sort((a, b) =>
a.bezeichnung.localeCompare(b.bezeichnung),
);
} else {
this.$fhcAlert.alertError(
this.$capitalize(this.$p.t('ui', 'fehlerBeimLesen')),
);
}
this.fillFormData(this.newsItem);
if (!this.$props.newsItem) {
this.formData.degreeProgram =
this.dropdownParsedDegreePrograms.find(
(degreeProgram) => degreeProgram.value === 0,
) ?? null;
}
const addLanguageTab = this.contentFormItems.addLanguage;
if (addLanguageTab?.config) {
addLanguageTab.config.languages = this.languagesToAdd;
}
},
template: /*html*/ `
<section
class="card bg-white shadow-sm border-0 mb-4 overflow-x-hidden"
aria-labelledby="news-item-form-heading"
>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center gap-3 mb-3">
<h4 id="news-item-form-heading" ref="newsPageHeading" class="card-title fhc-primary-color mb-0">{{ $capitalize($p.t('ui', 'newsForm')) }}</h4>
<button
@click="$emit('toggle-preview')"
type="button"
class="btn btn-sm btn-outline-primary d-inline-flex align-items-center justify-content-center"
:title="$capitalize(isPreviewShown ? $p.t('ui', 'hidePreview') : $p.t('ui', 'showPreview'))"
:aria-label="$capitalize(isPreviewShown ? $p.t('ui', 'hidePreview') : $p.t('ui', 'showPreview'))"
:aria-expanded="isPreviewShown"
aria-controls="news-item-preview"
>
<i class="fa-solid" :class="isPreviewShown ? 'fa-eye-slash' : 'fa-eye'" aria-hidden="true"></i>
</button>
</div>
<core-form ref="form">
<div class="d-flex flex-row flex-md-row align-items-md-end gap-3">
<div class="d-flex flex-row flex-md-row align-items-md-end gap-3">
<form-input
:label="$capitalize($p.t('ui', 'visible') + ' ' + $p.t('ui', 'von'))"
:teleport="true"
:enable-time-picker="false"
type="datePicker"
name="visibleFrom"
v-model="formData.visibleFrom"
model-type="yyyy-MM-dd"
format="dd.MM.yyyy"
auto-apply
/>
<form-input
:label="$capitalize($p.t('ui', 'visible') + ' ' + $p.t('global', 'bis'))"
:teleport="true"
:enable-time-picker="false"
type="datePicker"
name="visibleTo"
v-model="formData.visibleTo"
model-type="yyyy-MM-dd"
format="dd.MM.yyyy"
auto-apply
/>
</div>
<div class="d-flex flex-row flex-md-row align-items-md-end gap-3">
<form-input
:label="$capitalize($p.t('lehre/studiengang'))"
:suggestions="filteredDegreePrograms"
:optionValue="(option) => option.value"
:optionLabel="(option) => option.label"
@complete="filterDegreePrograms($event)"
type="autocomplete"
name="degreeProgramShortCode"
v-model="formData.degreeProgram"
dropdown
forceSelection
>
</form-input>
<form-input
v-model="formData.semester"
:label="$capitalize($p.t('lehre/studiensemester'))"
type="select"
name="semester"
>
<option
v-for="semester in semesters"
:key="semester.value"
:value="semester.value"
>
{{ semester.label }}
</option>
</form-input>
</div>
</div>
<fhc-tabs
:key="contentFormRenderKey"
ref="tabs"
:useprimevue="true"
:config="contentFormItems"
:default="activeContentFormKey"
v-model="formData.translations"
@change="handleContentTabChange"
@tab-action="handleContentTabAction"
style="flex: 1 1 0%; height: 0%"
class="mt-3"
>
</fhc-tabs>
<div class="d-flex justify-content-end mt-2">
<button
v-if="activeContentLanguage && copySourceLanguages.length"
@click="showCopyTranslationModal"
type="button"
class="btn btn-sm btn-outline-secondary"
:title="$capitalize($p.t('ui', 'copyContentFromAnotherLanguage'))"
aria-haspopup="dialog"
>
<i class="fa-solid fa-copy me-1" aria-hidden="true"></i>
{{ $capitalize($p.t('ui', 'copyContentFromAnotherLanguage')) }}
</button>
</div>
<news-item-copy-translation-modal
ref="copyTranslationModal"
:source-languages="copySourceLanguages"
:target-language-label="getLanguageLabel(activeContentLanguage)"
:target-has-content="activeTranslationHasContent"
@copy="copyTranslation"
></news-item-copy-translation-modal>
<div v-if="!isSaving" class="d-flex justify-content-end gap-2 mt-4 pt-3 border-top">
<button
@click="$emit('cancel')"
type="button"
class="btn btn-secondary"
>
{{ $capitalize($p.t('ui', 'abbrechen')) }}
</button>
<button
@click="newsItem ? updateNewsItem() : storeNewsItem()"
type="button"
class="btn btn-primary"
>
{{ $capitalize($p.t('global', 'speichern')) }}
</button>
</div>
<div v-else class="d-flex justify-content-end mt-4 pt-3 border-top" role="status">
<span class="spinner-border" aria-hidden="true"></span>
</div>
</core-form>
</div>
</section>
`,
};
@@ -0,0 +1,137 @@
import FormInput from '../../../Form/Input.js';
export default {
name: 'NewsItemPreview',
components: {
FormInput,
},
props: {
formData: {
type: Object,
default: null,
},
activeLanguage: {
type: String,
default: '',
},
},
data() {
return {
selectedLanguage: '',
};
},
computed: {
sprache() {
return this.$p.user_language.value;
},
availableLanguages() {
return (this.formData?.translations ?? [])
.map((translation) => translation.language)
.filter(Boolean);
},
displayedLanguage() {
return this.selectedLanguage || this.activeLanguage;
},
translation() {
return this.formData?.translations?.find(
(item) => item.language === this.displayedLanguage,
);
},
parsedTranslationText() {
const text = this.translation?.text ?? '';
if (!text) {
return '';
}
const appRoot = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/\/+$/, '');
return text.replace(
/(?:https?:\/\/|\/|\.\.\/)?[^"'<>\s]*?dms\.php(\?[^"'<>\s]*)/gi,
`${appRoot}/cms/dms.php$1`,
);
},
},
watch: {
activeLanguage: {
immediate: true,
handler(language) {
if (this.availableLanguages.includes(language)) {
this.selectedLanguage = language;
}
},
},
availableLanguages(languages) {
if (languages.includes(this.selectedLanguage)) {
return;
}
this.selectedLanguage = languages.includes(this.activeLanguage)
? this.activeLanguage
: (languages[0] ?? '');
},
},
created() {
this.$p.loadCategory(['global', 'ui']).then(() => {
this.phrasesLoaded = true;
});
},
methods: {
getLanguageLabel(language) {
this.sprache;
const languagePhrases = {
German: ['global', 'deutsch'],
English: ['global', 'englisch'],
French: ['ui', 'franzoesisch'],
Spanish: ['ui', 'spanisch'],
};
const phrase = languagePhrases[language];
return phrase
? this.$capitalize(this.$p.t(phrase[0], phrase[1]))
: language;
},
},
template: /*html*/ `
<section
id="news-item-preview"
class="card bg-white shadow-sm border-0 mb-4 position-sticky"
style="top: 1rem"
aria-labelledby="news-item-preview-heading"
>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center gap-3 mb-3">
<h4 id="news-item-preview-heading" class="card-title fhc-primary-color mb-0">{{ $capitalize($p.t('ui', 'newsPreview')) }}</h4>
<form-input
v-if="availableLanguages.length > 1"
v-model="selectedLanguage"
type="select"
name="previewLanguage"
class="m-0"
>
<option
v-for="language in availableLanguages"
:key="language"
:value="language"
>
{{ getLanguageLabel(language) }}
</option>
</form-input>
</div>
<article v-if="translation" class="card border">
<header class="card-header" :class="translation.isPublished ? 'fhc-primary' : 'bg-secondary bg-opacity-25 text-secondary'">
<div class="d-flex justify-content-between align-items-start gap-3">
<h5 class="mb-0">{{ translation.title || $capitalize($p.t('ui', 'untitledNews')) }}</h5>
<span v-if="!translation.isPublished" class="badge text-bg-secondary">{{ $capitalize($p.t('ui', 'notPublishedYet')) }}</span>
</div>
<address v-if="translation.author" class="small mb-0 mt-2">{{ translation.author }}</address>
</header>
<div class="card-body">
<div v-if="parsedTranslationText" class="card-text" v-html="parsedTranslationText"></div>
<p v-else class="text-muted mb-0">{{ $capitalize($p.t('ui', 'newsContentPreviewHint')) }}</p>
</div>
</article>
<p v-else class="text-muted mb-0">{{ $capitalize($p.t('ui', 'selectLanguageTabToPreview')) }}</p>
</div>
</section>
`,
};
@@ -0,0 +1,171 @@
import Pagination from '../../../Pagination/Pagination.js';
import ApiNewsAdministration from '../../../../api/factory/newsAdministration.js';
import NewsListItem from './NewsListItem.js';
import NewsListFilter from './NewsListFilter.js';
export default {
name: 'NewsList',
emits: ['edit'],
components: {
NewsListItem,
NewsListFilter,
Pagination,
},
data() {
return {
newsItems: [],
isLoading: false,
loadError: null,
maxPageCount: 0,
page: 1,
pageSize: 10,
isPublishedFilter: 'all',
isActiveFilter: true,
degreeProgramShortCodeFilter: null,
semesterFilter: null,
};
},
watch: {
'$p.user_language.value': function () {
this.page = 1;
this.fetchNews();
},
},
computed: {
sprache() {
return this.$p.user_language.value;
},
},
methods: {
async getNewsItem(newsId) {
await this.fetchNews();
return this.newsItems.find(
(news) => String(news.newsId) === String(newsId),
);
},
async fetchNews() {
this.isLoading = true;
this.loadError = null;
try {
const response = await this.$api.call(
ApiNewsAdministration.getNews(
this.page,
this.pageSize,
this.sprache,
this.isPublishedFilter,
this.isActiveFilter,
this.degreeProgramShortCodeFilter,
this.semesterFilter,
),
);
this.newsItems = this.parseNewsHtml(response.data);
this.maxPageCount = response.meta.row_count;
} catch (error) {
this.newsItems = [];
this.loadError = this.$capitalize(this.$p.t('ui', 'fehlerBeimLesen'));
} finally {
this.isLoading = false;
}
},
parseNewsHtml(html) {
if (!html) {
return [];
}
const document = new DOMParser().parseFromString(html, 'text/html');
return Array.from(document.querySelectorAll('.news-list-item')).map(
(article) => this.parseNewsItem(article),
);
},
parseNewsItem(article) {
const newsId = article
.querySelector('[news-id]')
?.getAttribute('news-id');
const title = article.querySelector('.card-header h2');
const author = article.querySelector('address');
const time = article.querySelector('time');
const isPublished = article.querySelector(
'.card-header span[is-published]',
);
const isActive =
article
.querySelector('.card-header span[is-active]')
?.getAttribute('is-active') === 'true';
const content = article.querySelector('.card-text');
return {
newsId: newsId ? Number(newsId) : null,
language: this.sprache,
title: title ? title.textContent.trim() : '',
author: author ? author.textContent.trim() : '',
date: time ? time.textContent.trim() : '',
dateTime: time ? time.getAttribute('datetime') || '' : '',
content: content ? content.innerHTML : '',
isPublished: isPublished
? isPublished.getAttribute('is-published') === 'true'
: null,
isActive,
};
},
afterPageUpdated(event) {
this.page = event.page;
this.pageSize = event.rows;
this.fetchNews();
this.$refs.newsListHeading.scrollIntoView({ block: 'end' });
},
afterFilterChanged(filters) {
this.isPublishedFilter = filters.isPublished;
this.isActiveFilter = filters.isActive;
this.degreeProgramShortCodeFilter = filters.degreeProgramShortCode;
this.semesterFilter = filters.semester;
this.page = 1;
this.fetchNews();
},
},
created() {
this.$p.loadCategory(['filter', 'global', 'lehre', 'ui']).then(() => {
this.phrasesLoaded = true;
});
this.fetchNews();
},
template: /*html*/ `
<section class="mt-4" aria-labelledby="news-administration-list-heading">
<h3 id="news-administration-list-heading" ref="newsListHeading" class="fhc-primary-color">
{{ $capitalize($p.t('ui', 'news')) }}
</h3>
<hr>
<news-list-filter @filter-changed="afterFilterChanged"></news-list-filter>
<div v-if="isLoading" class="d-flex justify-content-center py-4" role="status">
<span class="spinner-border" aria-hidden="true"></span>
<span class="visually-hidden">{{ $capitalize($p.t('ui', 'loadingNews')) }}</span>
</div>
<div v-else-if="loadError" class="alert alert-danger" role="alert">
{{ loadError }}
</div>
<div v-else-if="!newsItems.length" class="alert alert-info" role="status">
{{ $capitalize($p.t('ui', 'noNewsAvailable')) }}
</div>
<div v-else class="pt-3">
<news-list-item
v-for="(news, index) in newsItems"
:key="news.newsId || page + '-' + index"
:news="news"
@edit="$emit('edit', $event)"
@deleted="fetchNews()"
></news-list-item>
<pagination
v-if="maxPageCount > pageSize"
:page="page"
:page_size="pageSize"
:maxPageCount="maxPageCount"
@pageUpdated="afterPageUpdated"
></pagination>
</div>
</section>
`,
};
@@ -0,0 +1,153 @@
import FormInput from '../../../Form/Input.js';
import ApiStudiengang from '../../../../api/factory/studiengang.js';
export default {
name: 'NewsListFilter',
emits: ['filter-changed'],
components: {
FormInput,
},
data() {
return {
isPublished: 'all',
isActive: true,
degreeProgram: null,
semester: null,
degreePrograms: [],
filteredDegreePrograms: [],
};
},
computed: {
semesters() {
this.$p.user_language.value;
const semesterLabel = this.$capitalize(this.$p.t('lehre', 'semester'));
return [
{
label: this.$capitalize(this.$p.t('ui', 'all_semester')),
value: null,
},
...Array.from({ length: 8 }, (_, index) => ({
label: `${index + 1}. ${semesterLabel}`,
value: String(index + 1),
})),
];
},
dropdownParsedDegreePrograms() {
const degreePrograms = this.degreePrograms
.map((degreeProgram) => {
let labelFragment = degreeProgram.typ + degreeProgram.kurzbz;
labelFragment = labelFragment.trim().toUpperCase();
return {
label: `${labelFragment} (${degreeProgram.bezeichnung})`,
value: degreeProgram.studiengang_kz,
};
})
.sort((a, b) => a.label.localeCompare(b.label));
return [
{
label: this.$capitalize(this.$p.t('ui', 'dropdownEmptyOption')),
value: null,
},
...degreePrograms,
];
},
},
watch: {
isPublished() {
this.emitFilterChanged();
},
isActive() {
this.emitFilterChanged();
},
degreeProgram() {
this.emitFilterChanged();
},
semester() {
this.emitFilterChanged();
},
},
methods: {
emitFilterChanged() {
this.$emit('filter-changed', {
isPublished: this.isPublished,
isActive: this.isActive,
degreeProgramShortCode: this.degreeProgram?.value ?? null,
semester: this.semester,
});
},
filterDegreePrograms(event) {
const query = event.query.toLowerCase();
this.filteredDegreePrograms = this.dropdownParsedDegreePrograms.filter(
(degreeProgram) => degreeProgram.label.toLowerCase().includes(query),
);
},
},
async created() {
try {
const response = await this.$api.call(ApiStudiengang.getDegreePrograms());
if (response.meta.status === 'success') {
this.degreePrograms = response.data.sort((a, b) =>
a.bezeichnung.localeCompare(b.bezeichnung),
);
}
} catch (error) {
this.degreePrograms = [];
}
},
template: /*html*/ `
<div class="mb-3">
<div class="border rounded p-3 mt-2 d-flex flex-wrap align-items-center gap-3">
<form-input
v-model="isPublished"
type="select"
name="is-published-filter"
:label="$capitalize($p.t('ui', 'visible'))"
container-class="mb-0"
class="w-auto"
>
<option value="all">{{ $capitalize($p.t('global', 'alle')) }}</option>
<option :value="true">{{ $capitalize($p.t('ui', 'visible')) }}</option>
<option :value="false">{{ $capitalize($p.t('ui', 'notPublishedYet')) }}</option>
</form-input>
<form-input
v-model="degreeProgram"
:label="$capitalize($p.t('lehre', 'studiengang'))"
:suggestions="filteredDegreePrograms"
:optionValue="(option) => option.value"
:optionLabel="(option) => option.label"
@complete="filterDegreePrograms($event)"
type="autocomplete"
name="degree-program-filter"
dropdown
clear
></form-input>
<form-input
v-model="semester"
:label="$capitalize($p.t('lehre', 'studiensemester'))"
type="select"
name="semester-filter"
container-class="mb-0"
>
<option
v-for="semesterOption in semesters"
:key="semesterOption.value"
:value="semesterOption.value"
>
{{ semesterOption.label }}
</option>
</form-input>
<form-input
v-model="isActive"
type="checkbox"
name="is-active-filter"
:label="$capitalize($p.t('global', 'aktiv'))"
container-class="mb-0 align-self-end"
></form-input>
</div>
</div>
`,
};
@@ -0,0 +1,144 @@
import ApiNewsAdministration from '../../../../api/factory/newsAdministration.js';
const createCollapseId = () => {
const randomNumber = Math.floor(
1000000000000000 + Math.random() * 8000000000000000,
);
return `news-administration-item-${randomNumber}`;
};
export default {
name: 'NewsListItem',
emits: ['edit', 'deleted'],
props: {
news: {
type: Object,
required: true,
},
},
data() {
return {
isExpanded: false,
isDeleting: false,
collapseId: createCollapseId(),
};
},
methods: {
toggleContent() {
this.isExpanded = !this.isExpanded;
},
editNews(news) {
this.$emit('edit', news);
},
async deleteNews(news) {
if (this.isDeleting) {
return;
}
if (
(await this.$fhcAlert.confirm({
message: this.$capitalize(this.$p.t('ui', 'frageSicherLoeschen')),
acceptClass: 'btn btn-danger',
})) === false
) {
return;
}
this.isDeleting = true;
try {
const response = await this.$api.call(
ApiNewsAdministration.deleteNewsItem(news.newsId),
);
if (response.meta.status !== 'success') {
return;
}
this.$emit('deleted', news);
} catch (error) {
this.$fhcAlert.handleSystemError(error);
} finally {
this.isDeleting = false;
}
},
},
created() {
this.$p.loadCategory(['global', 'ui']).then(() => {
this.phrasesLoaded = true;
});
},
template: /*html*/ `
<article
class="card mb-3"
:class="{'border-secondary': !news.isPublished || !news.isActive}"
>
<header
class="card-header d-flex justify-content-between align-items-center gap-3"
:class="{
'fhc-primary': news.isPublished && news.isActive,
'bg-secondary bg-opacity-25 text-secondary': !news.isPublished || !news.isActive,
}"
>
<button
type="button"
class="btn btn-link text-reset text-decoration-none text-start d-flex align-items-center gap-2 flex-grow-1 p-0"
:aria-expanded="isExpanded"
:aria-controls="collapseId"
@click="toggleContent"
>
<i
class="fa-solid"
:class="isExpanded ? 'fa-chevron-down' : 'fa-chevron-right'"
aria-hidden="true"
></i>
<span class="h5 mb-0">{{ news.title }}</span>
<span v-if="!news.isPublished" class="badge text-bg-secondary">
{{ $capitalize($p.t('ui', 'notPublishedYet')) }}
</span>
<span v-if="!news.isActive" class="badge text-bg-secondary">
{{ $capitalize($p.t('ui', 'noLongerActive')) }}
</span>
</button>
<div class="d-flex align-items-center gap-2 flex-shrink-0">
<div class="text-end">
<address v-if="news.author" class="fw-bold small mb-0">
{{ news.author }}
</address>
<time v-if="news.date" class="small" :datetime="news.dateTime">
{{ news.date }}
</time>
</div>
<div class="d-flex gap-1" :aria-label="$capitalize($p.t('global', 'actions'))">
<button
type="button"
class="btn btn-sm rounded-circle shadow-sm d-inline-flex align-items-center justify-content-center"
:class="news.isPublished ? 'btn-light text-primary' : 'btn-outline-secondary bg-white'"
style="width: 1.75rem; height: 1.75rem"
:title="$capitalize($p.t('ui', 'bearbeiten'))"
:aria-label="$capitalize($p.t('ui', 'bearbeiten'))"
@click="editNews(news)"
>
<i class="fa-solid fa-pen" aria-hidden="true"></i>
</button>
<button
type="button"
class="btn btn-sm rounded-circle shadow-sm d-inline-flex align-items-center justify-content-center"
:class="news.isPublished ? 'btn-light text-danger' : 'btn-outline-danger bg-white'"
style="width: 1.75rem; height: 1.75rem"
:title="$capitalize($p.t('ui', 'loeschen'))"
:aria-label="$capitalize($p.t('ui', 'loeschen'))"
:disabled="isDeleting"
@click="deleteNews(news)"
>
<i class="fa-solid fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</header>
<div v-show="isExpanded" :id="collapseId" class="card-body">
<div class="card-text" v-html="news.content"></div>
</div>
</article>
`,
};
+89 -46
View File
@@ -1,4 +1,4 @@
import FhcCalendar from "../../Calendar/LvPlan.js"; import FhcCalendar from '../../Calendar/LvPlan.js';
import ApiLvPlan from '../../../api/factory/lvPlan.js'; import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js'; import ApiAuthinfo from '../../../api/factory/authinfo.js';
@@ -9,10 +9,10 @@ export const DEFAULT_MODE_LVPLAN_DESKTOP = 'Week';
export default { export default {
name: 'LvPlan', name: 'LvPlan',
components: { components: {
FhcCalendar FhcCalendar,
}, },
props: { props: {
propsViewData: Object propsViewData: Object,
}, },
data() { data() {
return { return {
@@ -24,65 +24,102 @@ export default {
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone, timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
}; };
}, },
inject: ["isMobile"], inject: {
computed:{ isMobile: {
default: false,
},
},
computed: {
currentDay() { currentDay() {
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.timezone).toISODate(); return (
this.propsViewData?.focus_date ||
luxon.DateTime.now().setZone(this.timezone).toISODate()
);
}, },
currentMode() { currentMode() {
const defaultMode = this.isMobile ? DEFAULT_MODE_LVPLAN_MOBILE : DEFAULT_MODE_LVPLAN_DESKTOP; const defaultMode = this.isMobile
? DEFAULT_MODE_LVPLAN_MOBILE
: DEFAULT_MODE_LVPLAN_DESKTOP;
return this.propsViewData?.mode || defaultMode; return this.propsViewData?.mode || defaultMode;
}, },
downloadLinks() { downloadLinks() {
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid) if (
!this.studiensemester_start ||
!this.studiensemester_ende ||
!this.uid
)
return false; return false;
const opts = { zone: this.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 const opts = { zone: this.timezone };
+ 'cis/private/lvplan/stpl_kalender.php' const start = luxon.DateTime.fromISO(
+ '?type=student' this.studiensemester_start,
+ '&pers_uid=' + this.uid opts,
+ '&begin=' + start ).toUnixInteger();
+ '&ende=' + ende; 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=student' +
'&pers_uid=' +
this.uid +
'&begin=' +
start +
'&ende=' +
ende;
return [ 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: 'excel',
{ title: "ical1", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=1&target=ical' }, icon: 'fa-solid fa-file-excel',
{ title: "ical2", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=2&target=ical' } 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: { methods: {
handleChangeDate(day, newMode) { handleChangeDate(day, newMode) {
return this.handleChangeMode(newMode, day); return this.handleChangeMode(newMode, day);
}, },
handleChangeMode(newMode, day) { handleChangeMode(newMode, day) {
const mode = newMode[0].toUpperCase() + newMode.slice(1) const mode = newMode[0].toUpperCase() + newMode.slice(1);
const focus_date = day.toISODate(); const focus_date = day.toISODate();
this.$router.push({ this.$router.push({
name: "LvPlan", name: 'LvPlan',
params: { params: {
mode, mode,
focus_date, focus_date,
lv_id: this.propsViewData?.lv_id ?? null lv_id: this.propsViewData?.lv_id ?? null,
} },
}); });
}, },
updateRange(rangeInterval) { updateRange(rangeInterval) {
this.$api this.$api
.call(ApiLvPlan.studiensemesterDateInterval( .call(
rangeInterval.end.startOf('week').toISODate() ApiLvPlan.studiensemesterDateInterval(
)) rangeInterval.end.startOf('week').toISODate(),
.then(res => { ),
)
.then((res) => {
this.studiensemester_kurzbz = res.data.studiensemester_kurzbz; this.studiensemester_kurzbz = res.data.studiensemester_kurzbz;
this.studiensemester_start = res.data.start; this.studiensemester_start = res.data.start;
this.studiensemester_ende = res.data.ende; this.studiensemester_ende = res.data.ende;
@@ -90,19 +127,25 @@ export default {
}, },
getPromiseFunc(start, end) { getPromiseFunc(start, end) {
return [ return [
this.$api.call(ApiLvPlan.LvPlanEvents(start.toISODate(), end.toISODate(), this.propsViewData.lv_id)), this.$api.call(
this.$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate())) ApiLvPlan.LvPlanEvents(
start.toISODate(),
end.toISODate(),
this.propsViewData.lv_id,
),
),
this.$api.call(
ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()),
),
]; ];
} },
}, },
created() { created() {
this.$api this.$api.call(ApiAuthinfo.getAuthUID()).then((res) => {
.call(ApiAuthinfo.getAuthUID()) this.uid = res.data.uid;
.then(res => { });
this.uid = res.data.uid;
});
}, },
template: /*html*/` template: /*html*/ `
<div class="cis-lvplan-personal d-flex flex-column h-100"> <div class="cis-lvplan-personal d-flex flex-column h-100">
<h2> <h2>
{{ $p.t('lehre/stundenplan') }} {{ $p.t('lehre/stundenplan') }}
@@ -144,5 +187,5 @@ export default {
</div> </div>
</div> </div>
</fhc-calendar> </fhc-calendar>
</div>` </div>`,
}; };
+84 -60
View File
@@ -1,72 +1,85 @@
import CisMenuEntry from "./Menu/Entry.js"; import CisMenuEntry from './Menu/Entry.js';
import FhcSearchbar from "../searchbar/searchbar.js"; import FhcSearchbar from '../searchbar/searchbar.js';
import CisSprachen from "./Sprachen.js" import CisSprachen from './Sprachen.js';
import ThemeSwitch from "./ThemeSwitch.js"; import ThemeSwitch from './ThemeSwitch.js';
import ApiCisMenu from '../../api/factory/cis/menu.js'; import ApiCisMenu from '../../api/factory/cis/menu.js';
export default { export default {
components: { components: {
CisMenuEntry, CisMenuEntry,
FhcSearchbar, FhcSearchbar,
CisSprachen, CisSprachen,
ThemeSwitch, ThemeSwitch,
}, },
props: { props: {
rootUrl: String, rootUrl: String,
logoUrl: String, logoUrl: String,
avatarUrl: String, avatarUrl: String,
logoutUrl: String, logoutUrl: String,
selectedtypes: Array, selectedtypes: Array,
searchbaroptions: Object, searchbaroptions: Object,
searchfunction: Function searchfunction: Function,
}, },
data: () => { data: () => {
return { return {
entries: [], entries: [],
activeEntry:null, activeEntry: null,
url:null, url: null,
urlMatchRankings:[], urlMatchRankings: [],
navUserDropdown:null, navUserDropdown: null,
menuOpen:true, menuOpen: true,
}; };
}, },
inject: ["isNarrow", "isMobile"], inject: {
provide(){ isNarrow: {
return{ default: false,
},
isMobile: {
default: false,
},
},
provide() {
return {
setActiveEntry: this.setActiveEntry, setActiveEntry: this.setActiveEntry,
addUrlCount: this.addUrlCount, addUrlCount: this.addUrlCount,
makeParentContentActive: this.makeParentContentActive, makeParentContentActive: this.makeParentContentActive,
} };
}, },
computed:{ computed: {
menuCollapseAriaLabel(){ menuCollapseAriaLabel() {
if(this.menuOpen){ if (this.menuOpen) {
return this.$p.t('global', 'collapseMenu'); return this.$p.t('global', 'collapseMenu');
}else{ } else {
return this.$p.t('global', 'extendMenu'); return this.$p.t('global', 'extendMenu');
} }
}, },
highestMatchingUrlCount(){ highestMatchingUrlCount() {
// gets the hightest ranking inside the array // gets the hightest ranking inside the array
let highestMatch = Math.max(...this.urlMatchRankings); let highestMatch = Math.max(...this.urlMatchRankings);
if(this.urlMatchRankings.length > 0){ if (this.urlMatchRankings.length > 0) {
// if more than one entry has the same ranking, none should be active // if more than one entry has the same ranking, none should be active
return this.urlMatchRankings.filter((value)=>value == highestMatch).length > 1 ? null : highestMatch; return this.urlMatchRankings.filter((value) => value == highestMatch)
.length > 1
? null
: highestMatch;
} }
return null; return null;
}, },
site_url(){ site_url() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router; return (
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router
);
}, },
}, },
methods: { methods: {
fetchMenu() { fetchMenu() {
return this.$api return this.$api
.call(ApiCisMenu.getMenu()) .call(ApiCisMenu.getMenu())
.then(res => res.data) .then((res) => res.data)
.then(menu => { .then((menu) => {
this.entries = menu; this.entries = menu;
}); });
}, },
@@ -76,43 +89,54 @@ export default {
this.navUserDropdown.hide(); this.navUserDropdown.hide();
} }
}, },
handleShowNavUser(){ handleShowNavUser() {
document.addEventListener("click", this.checkSettingsVisibility); document.addEventListener('click', this.checkSettingsVisibility);
}, },
handleHideNavUser(){ handleHideNavUser() {
document.removeEventListener("click", this.checkSettingsVisibility); document.removeEventListener('click', this.checkSettingsVisibility);
}, },
makeParentContentActive(content_id, collection=this.entries, parent=null){ makeParentContentActive(
if(!collection) return; content_id,
if (typeof collection == 'object' && !Array.isArray(collection) && Object.entries(collection).length > 0) { collection = this.entries,
parent = null,
) {
if (!collection) return;
if (
typeof collection == 'object' &&
!Array.isArray(collection) &&
Object.entries(collection).length > 0
) {
collection = Object.values(collection); collection = Object.values(collection);
} }
for(let entry of collection){ for (let entry of collection) {
if(entry.content_id == content_id){ if (entry.content_id == content_id) {
this.activeEntry = parent; this.activeEntry = parent;
} }
this.makeParentContentActive(content_id, entry.childs, entry.content_id); this.makeParentContentActive(
content_id,
entry.childs,
entry.content_id,
);
} }
}, },
addUrlCount(count){ addUrlCount(count) {
this.urlMatchRankings.push(count); this.urlMatchRankings.push(count);
}, },
setActiveEntry(content_id){ setActiveEntry(content_id) {
this.activeEntry = content_id; this.activeEntry = content_id;
}, },
}, },
created(){ created() {
this.fetchMenu(); this.fetchMenu();
}, },
mounted(){ mounted() {
this.$p.loadCategory(['ui', 'global', 'profilUpdate']) this.$p.loadCategory(['ui', 'global', 'profilUpdate']);
this.navUserDropdown = new bootstrap.Collapse(this.$refs.navUserDropdown,{ this.navUserDropdown = new bootstrap.Collapse(this.$refs.navUserDropdown, {
toggle: false toggle: false,
}); });
}, },
template: /*html*/` template: /*html*/ `
<div id="cis-header-bar" class="d-flex flex-row flex-grow-1"> <div id="cis-header-bar" class="d-flex flex-row flex-grow-1">
<div id="nav-logo" class="d-none d-lg-block"> <div id="nav-logo" class="d-none d-lg-block">
<div class="d-flex h-100 justify-content-between"> <div class="d-flex h-100 justify-content-between">
@@ -212,5 +236,5 @@ export default {
</div> </div>
</div> </div>
</div> </div>
</nav>` </nav>`,
}; };
+123 -104
View File
@@ -6,18 +6,17 @@ import ApiCms from '../../api/factory/cms.js';
const MAX_LOADED_NEWS = 30; const MAX_LOADED_NEWS = 30;
export default { export default {
name: "WidgetsNews", name: 'WidgetsNews',
components: { components: {
BsModal BsModal,
}, },
data: () => ({ data: () => ({
allNewsList: [], allNewsList: [],
singleNews: {}, singleNews: {},
selected: null, selected: null,
size:0, size: 0,
}), }),
props: ['width', 'height'] props: ['width', 'height'],
,
mixins: [AbstractWidget], mixins: [AbstractWidget],
computed: { computed: {
sizeClass() { sizeClass() {
@@ -28,7 +27,6 @@ export default {
// let quantity = this.width; // let quantity = this.width;
let quantity = MAX_LOADED_NEWS; let quantity = MAX_LOADED_NEWS;
if (this.width === 1) { if (this.width === 1) {
quantity = this.height === 1 ? 4 : MAX_LOADED_NEWS; quantity = this.height === 1 ? 4 : MAX_LOADED_NEWS;
} }
@@ -39,107 +37,129 @@ export default {
}, },
carouselItems() { carouselItems() {
return this.allNewsList.reduce((acc, cur) => { return this.allNewsList.reduce((acc, cur) => {
const el = document.getElementById('card-'+cur.news_id) const el = document.getElementById('card-' + cur.news_id);
acc.push(el); acc.push(el);
return acc return acc;
}, []) }, []);
} },
}, },
methods: { methods: {
updateNewsContentClasses:function(){ updateNewsContentClasses: function () {
Vue.nextTick(() => { Vue.nextTick(() => {
document.querySelectorAll(".fhc-news-card-item .card-body, .fhc-news-card-item .card, .fhc-news-card-item .card-header").forEach((el) => { document
if (!el.classList.contains("border-0")) { .querySelectorAll(
el.classList.add("border-0"); '.fhc-news-card-item .card-body, .fhc-news-card-item .card, .fhc-news-card-item .card-header',
} )
}); .forEach((el) => {
document.querySelectorAll(".fhc-news-card-item .card-header").forEach((el) => { if (!el.classList.contains('border-0')) {
if (!el.classList.contains("px-5")) { el.classList.add('border-0');
el.classList.add("px-5"); }
} });
if (!el.classList.contains("fhc-primary")) { document
el.classList.add("fhc-primary"); .querySelectorAll('.fhc-news-card-item .card-header')
} .forEach((el) => {
if (!el.classList.contains("position-sticky")) { if (!el.classList.contains('px-5')) {
el.classList.add("position-sticky"); el.classList.add('px-5');
} }
if (!el.classList.contains("top-0")) { if (!el.classList.contains('fhc-primary')) {
el.classList.add("top-0"); el.classList.add('fhc-primary');
} }
if (!el.classList.contains('position-sticky')) {
el.classList.add('position-sticky');
}); }
document.querySelectorAll(".fhc-news-card-item .card-header .row").forEach((el) => { if (!el.classList.contains('top-0')) {
if (!el.classList.contains("w-100")) { el.classList.add('top-0');
el.classList.add("w-100"); }
} });
if (!el.classList.contains("align-items-center")) { document
el.classList.add("align-items-center"); .querySelectorAll('.fhc-news-card-item .card-header .row')
} .forEach((el) => {
}); if (!el.classList.contains('w-100')) {
document.querySelectorAll(".fhc-news-card-item .card-header .row h2").forEach((el) => { el.classList.add('w-100');
if (!el.classList.contains("mb-0")) { }
el.classList.add("mb-0"); if (!el.classList.contains('align-items-center')) {
} el.classList.add('align-items-center');
}); }
}) });
document
.querySelectorAll('.fhc-news-card-item .card-header .row h2')
.forEach((el) => {
if (!el.classList.contains('mb-0')) {
el.classList.add('mb-0');
}
});
});
}, },
formatDate: function (dateTime) { formatDate: function (dateTime) {
const dt = new Date(dateTime); const dt = new Date(dateTime);
return numberPadding(dt.getDate()) + '.' + numberPadding((dt.getMonth() + 1)) + '.' + dt.getFullYear(); return (
numberPadding(dt.getDate()) +
'.' +
numberPadding(dt.getMonth() + 1) +
'.' +
dt.getFullYear()
);
}, },
formatTime: function (dateTime) { formatTime: function (dateTime) {
const dt = new Date(dateTime); const dt = new Date(dateTime);
return numberPadding(dt.getHours()) + ':' + numberPadding(dt.getMinutes()); return (
numberPadding(dt.getHours()) + ':' + numberPadding(dt.getMinutes())
);
}, },
isString(value){ isString(value) {
return Object.prototype.toString.call(value) === '[object String]'; return Object.prototype.toString.call(value) === '[object String]';
}, },
setNext(){ setNext() {
const thisIndex = this.allNewsList.findIndex(n=>n.news_id == this.selected.news_id) const thisIndex = this.allNewsList.findIndex(
const nextIndex = thisIndex == (this.allNewsList.length - 1) ? 0 : thisIndex + 1 (n) => n.news_id == this.selected.news_id,
);
const nextIndex =
thisIndex == this.allNewsList.length - 1 ? 0 : thisIndex + 1;
this.setSelected(this.allNewsList[nextIndex]) this.setSelected(this.allNewsList[nextIndex]);
this.updateNewsContentClasses(); this.updateNewsContentClasses();
}, },
setPrev() { setPrev() {
const thisIndex = this.allNewsList.findIndex(n=>n.news_id == this.selected.news_id) const thisIndex = this.allNewsList.findIndex(
const prevIndex = thisIndex ? thisIndex - 1 : this.allNewsList.length - 1 (n) => n.news_id == this.selected.news_id,
);
const prevIndex = thisIndex ? thisIndex - 1 : this.allNewsList.length - 1;
this.setSelected(this.allNewsList[prevIndex]) this.setSelected(this.allNewsList[prevIndex]);
this.updateNewsContentClasses(); this.updateNewsContentClasses();
}, },
getMenuItemClass(news) { getMenuItemClass(news) {
let classString = '' let classString = '';
if(this.selected && this.selected.news_id === news.news_id) { if (this.selected && this.selected.news_id === news.news_id) {
classString += 'selected' classString += 'selected';
} }
return classString return classString;
}, },
async setSelected(news) { async setSelected(news) {
const clickedElement = document.getElementById('card-'+news.news_id); const clickedElement = document.getElementById('card-' + news.news_id);
const clickedElementIndex = this.allNewsList.indexOf(news); const clickedElementIndex = this.allNewsList.indexOf(news);
const oldElementIndex = this.allNewsList.indexOf(this.selected); const oldElementIndex = this.allNewsList.indexOf(this.selected);
//if the clicked element is already active, do nothing //if the clicked element is already active, do nothing
if(clickedElementIndex === oldElementIndex) return; if (clickedElementIndex === oldElementIndex) return;
//add prev/next class to the clicked element //add prev/next class to the clicked element
if(clickedElementIndex > oldElementIndex) { if (clickedElementIndex > oldElementIndex) {
clickedElement.classList.add('carousel-item-next'); clickedElement.classList.add('carousel-item-next');
}else{ } else {
clickedElement.classList.add('carousel-item-prev'); clickedElement.classList.add('carousel-item-prev');
} }
// move to clicked element // move to clicked element
await Vue.nextTick(() => { this.carouselInstance.to(clickedElementIndex); }) await Vue.nextTick(() => {
this.carouselInstance.to(clickedElementIndex);
});
this.selected = news; this.selected = news;
}, },
contentURI: function (content_id) { contentURI: function (content_id) {
return ( return (
FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router + FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/CisVue/Cms/content/" + '/CisVue/Cms/content/' +
content_id content_id
); );
}, },
@@ -147,7 +167,7 @@ export default {
return ( return (
FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router + FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/CisVue/Cms/news" '/CisVue/Cms/news'
); );
}, },
setSingleNews(singleNews) { setSingleNews(singleNews) {
@@ -155,73 +175,72 @@ export default {
this.$refs.newsModal.show(); this.$refs.newsModal.show();
}, },
initCarouselInstance() { initCarouselInstance() {
Vue.nextTick(()=> { Vue.nextTick(() => {
if(this.$refs.carousel) { // carousel ref might not exist in every widget width/height if (this.$refs.carousel) {
// carousel ref might not exist in every widget width/height
this.carouselInstance = new bootstrap.Carousel(this.$refs.carousel, { this.carouselInstance = new bootstrap.Carousel(this.$refs.carousel, {
wrap: false, // keep this off even though it actually wraps wrap: false, // keep this off even though it actually wraps
interval: false interval: false,
}); });
} }
}) });
}, },
initActiveItem() { initActiveItem() {
Vue.nextTick(()=> { Vue.nextTick(() => {
if (Array.isArray(this.$refs.carouselItems) && this.$refs.carouselItems.length >0) { if (
this.$refs.carouselItems[0].classList.add("active") Array.isArray(this.$refs.carouselItems) &&
this.$refs.carouselItems.length > 0
) {
this.$refs.carouselItems[0].classList.add('active');
} }
}) });
} },
}, },
watch: { watch: {
width(newVal, oldVal) { width(newVal, oldVal) {
if(oldVal == 1 && newVal > 1) { // carousel instance will have been disposed if (oldVal == 1 && newVal > 1) {
this.initCarouselInstance() // carousel instance will have been disposed
this.initActiveItem() this.initCarouselInstance();
this.initActiveItem();
} }
} },
}, },
created() { created() {
this.$emit("setConfig", false); this.$emit('setConfig', false);
this.$api this.$api
.call(ApiCms.news(MAX_LOADED_NEWS)) .call(ApiCms.newsForWidget(MAX_LOADED_NEWS))
.then(res => res.data) .then((res) => res.data)
.then((news) => { .then((news) => {
this.allNewsList = Array.from(Object.values(news)); this.allNewsList = Array.from(Object.values(news));
this.allNewsList.sort((a, b) => { this.allNewsList.sort((a, b) => {
return new Date(b.datum) - new Date(a.datum); return new Date(b.datum) - new Date(a.datum);
}); });
this.selected = this.allNewsList.length ? this.allNewsList[0] : null this.selected = this.allNewsList.length ? this.allNewsList[0] : null;
this.initActiveItem() this.initActiveItem();
this.updateNewsContentClasses(); this.updateNewsContentClasses();
}) })
.catch((err) => { .catch((err) => {
console.error("ERROR: ", err); console.error('ERROR: ', err);
}); });
}, },
mounted() { mounted() {
if (this.$refs.container) { if (this.$refs.container) {
new ResizeObserver(entries => { new ResizeObserver((entries) => {
for (const entry of entries) { for (const entry of entries) {
let w = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width; let w = entry.contentBoxSize
? entry.contentBoxSize[0].inlineSize
: entry.contentRect.width;
// TODO(chris): rework sizing // TODO(chris): rework sizing
if (w > 600) if (w > 600) this.size = 3;
this.size = 3; else if (w > 350) this.size = 2;
else if (w > 350) else if (w > 250) this.size = 1;
this.size = 2; else this.size = 0;
else if (w > 250) }
this.size = 1;
else
this.size = 0;
}
}).observe(this.$refs.container); }).observe(this.$refs.container);
} }
this.initCarouselInstance()
this.initCarouselInstance();
}, },
template: /*html*/ ` template: /*html*/ `
<div ref="container" class="widgets-news h-100" :class="sizeClass" > <div ref="container" class="widgets-news h-100" :class="sizeClass" >
+4 -2
View File
@@ -14,7 +14,8 @@ export default {
emits: [ emits: [
'update:modelValue', 'update:modelValue',
'change', 'change',
'changed' 'changed',
'tab-action'
], ],
props: { props: {
config: { config: {
@@ -234,6 +235,7 @@ export default {
v-model="value" v-model="value"
:config="tab.config" :config="tab.config"
@update:suffix="updateSuffix($event)" @update:suffix="updateSuffix($event)"
@tab-action="$emit('tab-action', { key: tab.key, payload: $event })"
></component> ></component>
</keep-alive> </keep-alive>
</tabpanel> </tabpanel>
@@ -265,6 +267,7 @@ export default {
v-model="value" v-model="value"
:config="currentTab.config" :config="currentTab.config"
@update:suffix="updateSuffix($event)" @update:suffix="updateSuffix($event)"
@tab-action="$emit('tab-action', { key: currentTab.key, payload: $event })"
></component> ></component>
</keep-alive> </keep-alive>
</div> </div>
@@ -272,4 +275,3 @@ export default {
</template>` </template>`
}; };
+823 -1
View File
@@ -25,7 +25,6 @@
//flag for at least one new phrase //flag for at least one new phrase
$new = false; $new = false;
$phrases = array( $phrases = array(
//******************* CORE/global //******************* CORE/global
array( array(
@@ -60359,10 +60358,833 @@ I have been informed that I am under no obligation to consent to the transmissio
) )
) )
), ),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'visible',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'sichtbar',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'visible',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'publish',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'veroeffentlichen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'publish',
'description' => '',
'insertvon' => 'system'
)
)
),
// NEWS ADMINISTRATION UI PHRASES ----------------------------------------------
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'newsAdministration',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'News-Verwaltung',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'News Administration',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'createNews',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'News erstellen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Create news',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'news',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'News',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'News',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'loadingNews',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'News werden geladen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Loading news',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'noNewsAvailable',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Keine News verfügbar.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'No news available.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'addTranslationLanguage',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Übersetzungssprache hinzufügen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Add translation language',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'allLanguagesAdded',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Alle verfügbaren Sprachen wurden hinzugefügt.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'All available languages have been added.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'selectLanguageForTranslation',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Wählen Sie eine Sprache aus, um eine Übersetzung hinzuzufügen.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Select a language to add a translation.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'removeLanguage',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Sprache entfernen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Remove language',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'copyTranslationContent',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Übersetzungsinhalt kopieren',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Copy translation content',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'overwriteTranslationContent',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Übersetzungsinhalt überschreiben?',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Overwrite translation content?',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'copyingTranslation',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Das Kopieren von {source} ersetzt Verfasser*in, Titel und Text in {target}.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Copying from {source} will replace the author, title, and text in {target}.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'existingContentWillBeLost',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Der bestehende Inhalt der aktuellen Sprache geht verloren.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'The existing content in the current language will be lost.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'chooseAnotherLanguage',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Andere Sprache auswählen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Choose another language',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'overwriteAndCopy',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Überschreiben und kopieren',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Overwrite and copy',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'chooseLanguageToCopyContent',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Wählen Sie die Sprache aus, deren Inhalt in {target} kopiert werden soll.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Choose the language whose content should be copied into {target}.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'addLanguageBeforeCopying',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Fügen Sie vor dem Kopieren eine weitere Sprache hinzu.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Add another language before copying content.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'newsPreview',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'News-Vorschau',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'News preview',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'untitledNews',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'News ohne Titel',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Untitled news',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'notPublishedYet',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Noch nicht veröffentlicht',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Not published yet',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'noLongerActive',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Nicht mehr aktiv',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'No longer active',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'newsContentPreviewHint',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Text eingeben, um die News-Inhaltsvorschau zu sehen.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Enter text to see the news content preview.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'selectLanguageTabToPreview',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Wählen Sie einen Sprachreiter aus, um dessen Inhalt in der Vorschau zu sehen.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Select a language tab to preview its content.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'newsForm',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'News-Formular',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'News form',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'contentFormTitle',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => '{language} Inhaltsformular',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => '{language} Content form',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'showPreview',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Vorschau anzeigen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Show preview',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'hidePreview',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Vorschau ausblenden',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Hide preview',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'copyContentFromAnotherLanguage',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Inhalt aus einer anderen Sprache kopieren',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Copy content from another language',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'franzoesisch',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Französisch',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'French',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'spanisch',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Spanisch',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Spanish',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'blocks',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Blöcke',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Blocks',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'paragraph',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Absatz',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Paragraph',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'heading1',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Überschrift 1',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Heading 1',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'heading2',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Überschrift 2',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Heading 2',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'heading3',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Überschrift 3',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Heading 3',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'heading4',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Überschrift 4',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Heading 4',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'heading5',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Überschrift 5',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Heading 5',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'heading6',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Überschrift 6',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Heading 6',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'dropdownEmptyOption',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => '--keine Auswahl--',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => '--no selection--',
'description' => '',
'insertvon' => 'system'
)
)
),
// generic tabulator phrases END // generic tabulator phrases END
); );
//***** CHECK PHRASES & PHRASENTEXTE in German and English. //***** CHECK PHRASES & PHRASENTEXTE in German and English.
//***** INSERT into phrase_tbl if new app + category + phrase found in phrasen-array. //***** INSERT into phrase_tbl if new app + category + phrase found in phrasen-array.
//***** INSERT into phrasentext_tbl if new text found in phrasen-phrases-array, conciders every language apart. //***** INSERT into phrasentext_tbl if new text found in phrasen-phrases-array, conciders every language apart.
+7
View File
@@ -0,0 +1,7 @@
node_modules
.env
.env.*
!.env.example
/cypress/screenshots
/cypress/videos
/results