This commit is contained in:
Ivymaster
2026-08-20 17:05:38 +02:00
parent ab854067eb
commit 708ffac47e
32 changed files with 2925 additions and 689 deletions
+6
View File
@@ -61,6 +61,12 @@ $route['api/v1/organisation/[O|o]rganisationseinheit/(:any)'] = 'api/v1/organisa
$route['api/v1/ressource/[B|b]etriebsmittelperson/(:any)'] = 'api/v1/ressource/betriebsmittelperson2/$1';
$route['api/v1/system/[S|s]prache/(:any)'] = 'api/v1/system/sprache2/$1';
// 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';
$route['Cis/LvPlan/.*'] = 'Cis/LvPlan/index/$1';
$route['Cis/MyLvPlan/.*'] = 'Cis/MyLvPlan/index/$1';
$route['Cis/MyLv/.*'] = 'Cis/MyLv/index/$1';
+8 -2
View File
@@ -17,9 +17,9 @@ class Cms extends Auth_Controller
array(
'content' => 'basis/cis:r',
'getNews' => 'basis/cis:r',
'getNewsRowCount' => 'basis/cis:r',
'getRoomInformation' => 'basis/cis:r',
'news' => 'basis/cis:r'
'news' => 'basis/cis:r',
'newsAdministration' => ['basis/news:r', 'basis/cis:r'],
)
);
@@ -87,6 +87,12 @@ class Cms extends Auth_Controller
$this->load->view('CisRouterView/CisRouterView.php', ['viewData'=>$viewData, 'route' => 'News']);
}
public function newsAdministration()
{
$viewData = array();
$this->load->view('CisRouterView/CisRouterView.php', ['viewData'=>$viewData, 'route' => 'NewsAdministration']);
}
public function getRoomInformation($ort_kurzbz)
{
// Load Config
+26 -30
View File
@@ -36,13 +36,13 @@ class Cms extends FHCAPI_Controller
'ContentID' => self::PERM_LOGGED,
'getOrtKurzbzContent' => self::PERM_LOGGED,
'content' => self::PERM_LOGGED,
'news' => self::PERM_LOGGED,
'getNewsRowCount' => self::PERM_LOGGED,
'newsForWidget' => self::PERM_LOGGED,
'getNews' => self::PERM_LOGGED,
]);
$this->load->model('content/News_model', 'NewsModel');
$this->load->model('crm/Student_model', 'StudentModel');
// setting up the papgination_size
$this->page_size = 10;
@@ -59,7 +59,6 @@ class Cms extends FHCAPI_Controller
//------------------------------------------------------------------------------------------------------------------
// Private methods
//------------------------------------------------------------------------------------------------------------------
// Public methods
@@ -106,7 +105,7 @@ class Cms extends FHCAPI_Controller
$this->terminateWithSuccess($content_id);
}
public function news()
public function newsForWidget()
{
// form validation
@@ -162,28 +161,6 @@ 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)
{
list($studiengang_kz, $semester) = $this->cmslib->getStgAndSem($studiengang_kz, $semester);
$all = $edit;
$this->load->model('content/News_model','NewsModel');
$passedInMaxAlter = $this->input->get('maxAlter', true);
if($passedInMaxAlter !== null)
{
$maxAlter = intval($passedInMaxAlter);
}
$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, $maxAlter = 0)
{
//form validation
@@ -210,15 +187,34 @@ class Cms extends FHCAPI_Controller
{
$maxAlter = intval($passedInMaxAlter);
}
$passedSichtbar = $this->input->get('published', true);
if($passedSichtbar !== null)
{
$sichtbar = $passedSichtbar;
}
$student = $this->StudentModel->loadWhere(['student_uid' => getAuthUID()]);
if(isError($student))
{
$this->terminateWithError(getError($student));
}
if(hasData($student))
{
$student = current(getData($student));
$studiengang_kz = $student->studiengang_kz;
$semester = $student->semester;
$this->addMeta('semester', $semester);
}
$news = $this->cmslib->getNews($infoscreen, $studiengang_kz, $semester, $mischen, $titel, $edit, $sichtbar, $page, $page_size, $sprache, $maxAlter);
$news = $this->getDataOrTerminateWithError($news);
$this->addMeta('phrases', json_decode($this->p->getJson()));
$this->terminateWithSuccess($news);
$this->addMeta('row_count', $news["full_count"] ?? 0);
$this->terminateWithSuccess($news["content"]);
}
}
@@ -0,0 +1,815 @@
<?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
{
const GENERAL_DEGREE_PROGRAM_ID = 0;
/**
* 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->loadPhrases([
'global'
]);
$this->load->library('CmsLib');
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = true, $sichtbar = true, $maxAlter = 0)
{
$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;
$passedInMaxAlter = $this->input->get('maxAlter', true);
if($passedInMaxAlter !== null)
{
$maxAlter = intval($passedInMaxAlter);
}
$passedSichtbar = $this->input->get('published', true);
if($passedSichtbar !== null)
{
$sichtbar = $passedSichtbar;
}
$allowedDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news");
array_push($allowedDegreePrograms, self::GENERAL_DEGREE_PROGRAM_ID);
$news = $this->cmslib->getNews($infoscreen, $studiengang_kz, $semester, $mischen, $titel, $edit, $sichtbar, $page, $page_size, $sprache, $maxAlter, true, $allowedDegreePrograms);
$news = $this->getDataOrTerminateWithError($news);
$this->addMeta('phrases', json_decode($this->p->getJson()));
$this->addMeta('row_count', $news["full_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));
$allowedDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news");
array_push($allowedDegreePrograms, self::GENERAL_DEGREE_PROGRAM_ID);
if (!in_array($news->studiengang_kz, $allowedDegreePrograms)) {
$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 ? $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)
{
$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);
}
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 creates new versions of 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);
}
$latestVersions = [];
foreach ((array)getData($existingTranslationsResult) as $existingTranslation)
{
$language = $existingTranslation->sprache;
$version = (int)$existingTranslation->version;
$latestVersions[$language] = max($latestVersions[$language] ?? 0, $version);
}
foreach ($data['translations'] as $translation)
{
$translationResult = $this->ContentspracheModel->insert([
'sprache' => $translation['sprache'],
'content_id' => $existingNews->content_id,
'version' => ($latestVersions[$translation['sprache']] ?? 0) + 1,
'sichtbar' => $translation['sichtbar'],
'content' => $translation['content'],
'titel' => $translation['betreff'],
'insertamum' => $now,
'insertvon' => $uid,
'updateamum' => $now,
'updatevon' => $uid
]);
$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);
}
}
@@ -27,7 +27,7 @@ class Studgang extends FHCAPI_Controller
{
parent::__construct([
'getStudiengangInfo'=> self::PERM_LOGGED,
'getDegreePrograms'=> self::PERM_LOGGED,
]);
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
@@ -43,6 +43,22 @@ class Studgang extends FHCAPI_Controller
//------------------------------------------------------------------------------------------------------------------
// 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(){
$isMitarbeiter = $this->MitarbeiterModel->isMitarbeiter(getAuthUID());
$isMitarbeiter = $this->getDataOrTerminateWithError($isMitarbeiter);
+13 -83
View File
@@ -141,63 +141,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 $semester
@@ -233,22 +176,22 @@ class CmsLib
*
* @return void
*/
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true, $page = 1, $page_size = 10, $sprache, $maxAlter = 0)
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true, $page = 1, $page_size = 10, $sprache, $maxAlter = 0, $filterForDegreePrograms = false, $allowedDegreePrograms = [])
{
$this->ci->load->model('organisation/Studiengang_model', 'StudiengangModel');
list($studiengang_kz, $semester) = $this->getStgAndSem($studiengang_kz, $semester);
log_message('error', 'cmslib getNews Studiengang: ' . $studiengang_kz . ', Semester: ' . $semester);
$all = $edit;
$xml = '<?xml version="1.0" encoding="UTF-8"?><content>';
$this->ci->load->model('content/News_model', 'NewsModel');
$news = $this->ci->NewsModel->getNewsWithContent($sprache, $studiengang_kz, $semester, null, $sichtbar, $maxAlter, $page, $page_size, $all, $mischen);
if (isError($news))
return $news;
$news = getData($news);
$news = $this->ci->NewsModel->getNewsWithContent($sprache, $studiengang_kz, $semester, null, $sichtbar, $maxAlter, $page, $page_size, $all, $mischen, $filterForDegreePrograms, $allowedDegreePrograms);
$news = getData($news) ?? [];
foreach ($news as $newsobj) {
if ($studiengang_kz && $edit && !$newsobj->studiengang_kz)
continue;
@@ -256,26 +199,10 @@ class CmsLib
$datum = '<datum><![CDATA[' . $date->format('d.m.Y') . ']]></datum>';
$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>";
$isPublished = $edit ? '<is_published><![CDATA[' . ($newsobj->sichtbar ? 'true' : 'false') . ']]></is_published>' : '';
$xml .= "<newswrapper>" . $newsobj->content . $datum . $id . $isPublished . "</newswrapper>";
}
/* if ($studiengang_kz != 0) {
$stg_obj = $this->ci->StudiengangModel->load($studiengang_kz);
if (isError($stg_obj))
return $stg_obj;
$stg_obj = current(getData($stg_obj) ?: []);
if ($stg_obj) {
if (!$edit && !$infoscreen) {
$extras = $this->getNewsExtras($stg_obj, $semester);
if (isError($extras))
return $extras;
$xml .= getData($extras);
}
$xml .= '<studiengang_bezeichnung><![CDATA[' . $stg_obj->bezeichnung . ']]></studiengang_bezeichnung>';
}
} */
if ($titel != '') {
$xml .= '<news_titel>' . $titel . '</news_titel>';
}
@@ -304,6 +231,9 @@ class CmsLib
$content = $content->saveHTML();
$content = str_replace('dms.php', APP_ROOT . 'cms/dms.php', $content);
return success($content);
return success([
"content" => $content,
"full_count" => $news[0]->full_count ?? 0
]);
}
}
+18 -16
View File
@@ -63,9 +63,10 @@ class News_model extends DB_Model
* 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, $maxalter = 0, $page = 1, $page_size = 10, $all = false, $mischen = true, $filterForDegreePrograms = false, $allowedDegreePrograms = [])
{
$this->addSelect(["*", "COUNT(*) OVER() AS full_count"]);
$this->addOrder('datum', 'DESC');
$studiengang_kz = trim($studiengang_kz);
@@ -99,44 +100,45 @@ class News_model extends DB_Model
$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 . ")";
$allowedSemesterValues = [$semester, 0, null];
$where[] = "(studiengang_kz = " . $this->db->escape($studiengang_kz) . " AND (semester in (" . implode(',', array_map([$this->db, 'escape'], $allowedSemesterValues)) . ") OR semester IS NULL) OR (studiengang_kz = 0 AND semester = " . $this->db->escape($semester) . ")" . $add . ")";
}
$this->addJoin('campus.tbl_contentsprache cs', 'content_id');
$where[] = "cs.sichtbar = " . ($sichtbar ? "true" : "false");
if (is_bool($sichtbar) || $sichtbar === 'true' || $sichtbar === 'false') {
$where[] = "cs.sichtbar = " . ($sichtbar ? "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.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);
$this->db->where($where, NULL, FALSE);
}
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, $maxalter = 0, $page = 1, $page_size = 10, $all = false, $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, $maxalter, $page, $page_size, $all, $mischen, $filterForDegreePrograms, $allowedDegreePrograms);
// getting the number of rows of the query and adding pagination to the query result
$num_rows = $this->getNumRows(true);
$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
$this->prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxalter, $page, $page_size, $all, $mischen);
$this->prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxalter, $page, $page_size, $all, $mischen, $filterForDegreePrograms, $allowedDegreePrograms);
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();
}
}
@@ -885,6 +885,14 @@ class Studiengang_model extends DB_Model
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) {
if(count($benutzerfunktionen) && defined('DOMAIN'))
{
@@ -24,7 +24,8 @@ $includesArray = array(
'public/css/components/FormUnderline.css',
'public/css/components/abgabetool/abgabe.css',
'public/css/Cis4/Cms.css',
'public/css/Cis4/Studium.css'
'public/css/Cis4/Studium.css',
'public/css/components/vue-datepicker.css',
),
'customJSs' => array(
'vendor/npm-asset/primevue/accordion/accordion.min.js',
+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="../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">
<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/components/jqueryui/jquery-ui.min.js"></script>
<script type="text/javascript" src="../include/js/jquery.ui.datepicker.translation.js"></script>
<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/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="../include/js/jquery.ui.datepicker.translation.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 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(){
},
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 win = tinyMCEPopup.getWindowArg("window");
+17 -38
View File
@@ -1,45 +1,24 @@
export default {
content(content_id, version=null, sprache=null, sichtbar=null) {
return this.$fhcApi.get(
"/api/frontend/v1/Cms/content",
{
content_id: content_id,
...(version?{version}:{}),
...(sprache?{sprache}:{}),
...(sichtbar?{sichtbar}:{}),
}
);
},
content(content_id, version = null, sprache = null, sichtbar = null) {
return this.$fhcApi.get('/api/frontend/v1/Cms/content', {
content_id: content_id,
...(version ? { version } : {}),
...(sprache ? { sprache } : {}),
...(sichtbar ? { sichtbar } : {}),
});
},
//api function used for the news View that renders the html
getNews(page = 1, page_size = 10, sprache) {
return this.$fhcApi.get(
"/api/frontend/v1/Cms/getNews",
{
page,
page_size,
sprache,
},
);
return this.$fhcApi.get('/api/frontend/v1/Cms/getNews', {
page,
page_size,
sprache,
});
},
//api function used for the widget component
news(limit) {
return this.$fhcApi.get(
"/api/frontend/v1/Cms/news",
{
limit: limit
}
);
return this.$fhcApi.get('/api/frontend/v1/Cms/news', {
limit: limit,
});
},
getNewsRowCount: function () {
return this.$fhcApi.get(
"/api/frontend/v1/Cms/getNewsRowCount",
{}
);
},
getNewsExtra: function(){
return this.$fhcApi.get(
"/api/frontend/v1/Cms/getStudiengangInfoForNews",
{}
);
}
}
};
+16 -25
View File
@@ -16,7 +16,7 @@
*/
export default {
content(content_id, version=null, sprache=null, sichtbar=null) {
content(content_id, version = null, sprache = null, sichtbar = null) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/content',
@@ -24,12 +24,18 @@ export default {
content_id,
...(version ? { version } : {}),
...(sprache ? { sprache } : {}),
...(sichtbar ? { sichtbar } : {})
}
...(sichtbar ? { sichtbar } : {}),
},
};
},
//api function used for the news View that renders the html
getNews(page = 1, page_size = 10, sprache, maxAge = 60) {
getNews(
page = 1,
page_size = 10,
sprache,
maxAge = 60,
filterPublished = true,
) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/getNews',
@@ -37,32 +43,17 @@ export default {
page,
page_size,
sprache,
maxAlter: maxAge
maxAlter: maxAge,
published: filterPublished,
},
};
},
//api function used for the widget component
news(limit, maxAge = 60) {
newsForWidget(limit, maxAge = 60) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/news',
params: { limit, maxAlter: maxAge }
url: '/api/frontend/v1/Cms/newsForWidget',
params: { limit, maxAlter: maxAge },
};
},
getNewsRowCount(maxAge = 60) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/getNewsRowCount',
params: {
maxAlter: maxAge
}
};
},
getNewsExtra() {
// TODO(chris): seems to be called from nowhere?
return {
method: 'get',
url: '/api/frontend/v1/Cms/getStudiengangInfoForNews'
};
}
};
};
@@ -0,0 +1,65 @@
/**
* 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,
maxAge = 60,
filterPublished = true,
) {
return {
method: 'get',
url: '/api/frontend/v1/NewsAdministrationAPI/getNews',
params: {
page,
page_size,
sprache,
maxAlter: maxAge,
published: filterPublished,
},
};
},
getNewsItem(newsId) {
return {
method: 'get',
url: `/api/frontend/v1/NewsAdministrationAPI/getNewsItem/${newsId}`,
};
},
storeNewsItem(data) {
console.log('Storing news item:', 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 {
getDegreePrograms() {
return {
method: 'get',
url: '/api/frontend/v1/Studgang/getDegreePrograms',
};
},
studiengangInformation() {
return {
method: 'get',
url: '/api/frontend/v1/Studgang/getStudiengangInfo'
url: '/api/frontend/v1/Studgang/getStudiengangInfo',
};
},
getStudiengangByKz(studiengang_kz) {
return {
method: 'get',
url: '/api/frontend/v1/organisation/StudiengangEP/getStudiengangByKz',
params: { studiengang_kz }
params: { studiengang_kz },
};
}
};
},
};
+133 -111
View File
@@ -2,30 +2,35 @@ import FhcDashboard from '../../components/Dashboard/Dashboard.js';
import PluginsPhrasen from '../../plugins/Phrasen.js';
import Theme from '../../plugins/Theme.js';
import contrast from '../../directives/contrast.js';
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js";
import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js";
import MyLvPlan from "../../components/Cis/LvPlan/MyLvPlan.js";
import MylvStudent from "../../components/Cis/Mylv/Student.js";
import Profil from "../../components/Cis/Profil/Profil.js";
import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js";
import CmsNews from "../../components/Cis/Cms/News.js";
import NewsItemForm from "../../components/Cis/Cms/NewsItemForm.js";
import CmsContent from "../../components/Cis/Cms/Content.js";
import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js";
import RoomInformation, {DEFAULT_MODE_RAUMINFO_DESKTOP, DEFAULT_MODE_RAUMINFO_MOBILE} from "../../components/Cis/Mylv/RoomInformation.js";
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js";
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.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 { setScrollbarWidth } from '../../helpers/CssVarCalcHelpers.js';
import LvPlan from '../../components/Cis/LvPlan/Lehrveranstaltung.js';
import MyLvPlan from '../../components/Cis/LvPlan/MyLvPlan.js';
import MylvStudent from '../../components/Cis/Mylv/Student.js';
import Profil from '../../components/Cis/Profil/Profil.js';
import Raumsuche from '../../components/Cis/Raumsuche/Raumsuche.js';
import CmsNews from '../../components/Cis/Cms/News.js';
import NewsAdministration from '../../components/Cis/Cms/NewsAdministration/NewsAdministration.js';
import CmsContent from '../../components/Cis/Cms/Content.js';
import Info from '../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js';
import RoomInformation, {
DEFAULT_MODE_RAUMINFO_DESKTOP,
DEFAULT_MODE_RAUMINFO_MOBILE,
} from '../../components/Cis/Mylv/RoomInformation.js';
import AbgabetoolStudent from '../../components/Cis/Abgabetool/AbgabetoolStudent.js';
import AbgabetoolMitarbeiter from '../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.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 {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 isMobile = window.matchMedia("(max-width: 767px)").matches;
const ciPath =
FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') +
FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const isMobile = window.matchMedia('(max-width: 767px)').matches;
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(`/${ciPath}`),
@@ -34,62 +39,66 @@ const router = VueRouter.createRouter({
path: `/Cis/Studium`,
name: 'Studium',
component: Studium,
props: true
props: true,
},
{
path: `/Cis/Profil/View/:uid`,
name: 'ProfilView',
component: Profil,
props: true
props: true,
},
{
path: `/Cis/Profil`,
name: 'Profil',
component: Profil,
props: true
props: true,
},
{
path: `/Cis/Abgabetool/Student/:student_uid_prop?`,
name: 'AbgabetoolStudent',
component: AbgabetoolStudent,
props: true
props: true,
},
{
path: `/Cis/Abgabetool/Mitarbeiter`,
name: 'AbgabetoolMitarbeiter',
component: AbgabetoolMitarbeiter,
props: true
props: true,
},
{
path: `/Cis/Abgabetool/Assistenz/:stg_kz_prop?`,
name: 'AbgabetoolAssistenz',
component: AbgabetoolAssistenz,
props: true
props: true,
},
{
path: `/Cis/Abgabetool/Deadlines/:person_uid_prop?`,
name: 'DeadlineOverview',
component: DeadlineOverview,
props: true
props: true,
},
{
path: `/Cis/Raumsuche`,
name: 'Raumsuche',
component: Raumsuche,
props: true
props: true,
},
// Redirect old links to new format
{
path: "/CisVue/Cms/getRoomInformation/:ort_kurzbz",
name: "RoomInformationOld",
path: '/CisVue/Cms/getRoomInformation/:ort_kurzbz',
name: 'RoomInformationOld',
component: RoomInformation,
redirect: (to) => {
return { // redirect to longer Rauminfo url and map params
name: "RoomInformation",
params: { // in this case always populate other params since they are not optional
return {
// redirect to longer Rauminfo url and map params
name: 'RoomInformation',
params: {
// in this case always populate other params since they are not optional
ort_kurzbz: to.params.ort_kurzbz,
mode: isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP,
focus_date: new Date().toISOString().split("T")[0]
mode: isMobile
? DEFAULT_MODE_RAUMINFO_MOBILE
: DEFAULT_MODE_RAUMINFO_DESKTOP,
focus_date: new Date().toISOString().split('T')[0],
},
};
},
@@ -98,18 +107,28 @@ const router = VueRouter.createRouter({
path: `/CisVue/Cms/getRoomInformation/:mode/:focus_date/:ort_kurzbz`,
name: 'RoomInformation',
component: RoomInformation,
props: (route) => { // validate and set mode/focus date if for some reason missing
const validModes = ["Month", "Week", "Day"];
props: (route) => {
// validate and set mode/focus date if for some reason missing
const validModes = ['Month', 'Week', 'Day'];
// check mode string
const mode = route.params.mode &&
validModes.includes(route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase())
? route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()
: (isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP);
const mode =
route.params.mode &&
validModes.includes(
route.params.mode.charAt(0).toUpperCase() +
route.params.mode.slice(1).toLowerCase(),
)
? route.params.mode.charAt(0).toUpperCase() +
route.params.mode.slice(1).toLowerCase()
: isMobile
? DEFAULT_MODE_RAUMINFO_MOBILE
: DEFAULT_MODE_RAUMINFO_DESKTOP;
// default to today date if not provided
const d = new Date(route.params.focus_date)
const focus_date = !isNaN(d) ? route.params.focus_date : new Date().toISOString().split("T")[0];
const d = new Date(route.params.focus_date);
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
// existing viewData declaration written from codeigniter 3 into routerview tag
@@ -117,43 +136,44 @@ const router = VueRouter.createRouter({
propsViewData: {
mode,
focus_date,
ort_kurzbz: route.params.ort_kurzbz
}
ort_kurzbz: route.params.ort_kurzbz,
},
};
},
beforeEnter: (to, from, next) => {
// missing mode or focus_date -> set defaults
if (!to.params.mode || !to.params.focus_date) {
next({
name: "RoomInformation",
name: 'RoomInformation',
params: {
mode: to.params.mode || DEFAULT_MODE_RAUMINFO,
focus_date: to.params.focus_date || new Date().toISOString().split("T")[0],
ort_kurzbz: route.params.ort_kurzbz
}
focus_date:
to.params.focus_date || new Date().toISOString().split('T')[0],
ort_kurzbz: route.params.ort_kurzbz,
},
});
} else {
next();
}
}
},
},
{
path: `/CisVue/Cms/Content/:content_id`,
name: 'Content',
component: CmsContent,
props: true
props: true,
},
{
path: `/CisVue/Cms/News`,
name: 'News',
component: CmsNews,
props: true
props: true,
},
{
path: `/CisVue/Cms/News/Create`,
name: 'NewsItemCreate',
component: NewsItemForm,
props: true
path: `/CisVue/Cms/NewsAdministration`,
name: 'NewsAdministration',
component: NewsAdministration,
props: true,
},
{
path: `/Cis/MyLv/:studiensemester?`,
@@ -165,23 +185,24 @@ const router = VueRouter.createRouter({
path: `/Cis/MyLv/Info/:studien_semester/:lehrveranstaltung_id`,
name: 'LvInfo',
component: Info,
props: true
props: true,
},
// Redirect old links to new format
{
// only trigger on first param being numeric to avoid paths like "LvPlan/Month" entering here
path: "/Cis/LvPlan/:lv_id(\\d+)",
name: "LvPlanOld",
path: '/Cis/LvPlan/:lv_id(\\d+)',
name: 'LvPlanOld',
component: LvPlan,
redirect(to) {
const route = Vue.unref(router.currentRoute);
const { mode, focus_date } = route.params; // keep mode and focus_date if available
return { // redirect to longer LvPlan url and map params
name: "LvPlan",
return {
// redirect to longer LvPlan url and map params
name: 'LvPlan',
params: {
mode,
focus_date,
lv_id: to.params.lv_id
lv_id: to.params.lv_id,
},
};
},
@@ -192,9 +213,9 @@ const router = VueRouter.createRouter({
component: LvPlan,
props(route) {
return {
propsViewData: route.params
propsViewData: route.params,
};
}
},
},
{
path: `/Cis/MyLvPlan/:mode?/:focus_date?`,
@@ -202,9 +223,9 @@ const router = VueRouter.createRouter({
component: MyLvPlan,
props(route) {
return {
propsViewData: route.params
propsViewData: route.params,
};
}
},
},
{
path: `/Cis/StgOrgLvPlan/:mode?/:focus_date?/:stgkz?/:sem?/:verband?/:gruppe?`,
@@ -212,48 +233,48 @@ const router = VueRouter.createRouter({
component: StgOrgLvPlan,
props(route) {
return {
propsViewData: route.params
propsViewData: route.params,
};
}
},
},
{
path: `/Cis/OtherLvPlan/:otherUid/:mode?/:focus_date?`,
name: "OtherLvPlan",
component: OtherLvPlan,
props(route) {
return {
propsViewData: route.params
};
}
},
path: `/Cis/OtherLvPlan/:otherUid/:mode?/:focus_date?`,
name: 'OtherLvPlan',
component: OtherLvPlan,
props(route) {
return {
propsViewData: route.params,
};
},
},
{
path: `/Cis4`,
name: 'Cis4',
component: FhcDashboard,
props: {dashboard: 'CIS'},
props: { dashboard: 'CIS' },
},
{
path: `/`,
name: 'FhcDashboard',
component: FhcDashboard,
props: {dashboard: 'CIS'},
props: { dashboard: 'CIS' },
},
{
path: '/:pathMatch(.*)*',
name: 'Fallback',
component: FhcDashboard,
props: {dashboard: 'CIS'},
props: { dashboard: 'CIS' },
redirect: () => {
return {
name: "Cis4",
name: 'Cis4',
params: {
dashboard: 'CIS'
dashboard: 'CIS',
},
};
},
},
]
})
],
});
const app = Vue.createApp({
name: 'CisApp',
@@ -262,48 +283,49 @@ const app = Vue.createApp({
windowWidth: 0,
}),
provide() {
return { // provide injectable & watchable language property
return {
// provide injectable & watchable language property
language: Vue.computed(() => this.$p.user_language),
isMobile: Vue.computed(() => this.isMobile),
}
};
},
computed: {
isMobile: function() {
return (this.windowWidth < 767);
}
isMobile: function () {
return this.windowWidth < 767;
},
},
methods: {
isInternalRoute(href) {
const internalBase = window.location.origin
const internalBase = window.location.origin;
return href.startsWith(internalBase);
},
handleClick(event) {
const target = event.target.closest('a');
if(target?.id == 'skiplink') return
if (target?.id == 'skiplink') return;
if (target && this.isInternalRoute(target.href)) {
const url = new URL(target.href)
const path = url.pathname
const base = this.$router.options.history.base
const route = path.replace(base, '') || '/'
const url = new URL(target.href);
const path = url.pathname;
const base = this.$router.options.history.base;
const route = path.replace(base, '') || '/';
// let click event propagate normally if we dont route internally
const res = this.$router.resolve(route)
if(!res?.matched?.length || res.name === 'Fallback') return
const res = this.$router.resolve(route);
if (!res?.matched?.length || res.name === 'Fallback') return;
event.preventDefault(); // Prevent browser navigation
if(this.isMobile) { // toggle the menu
if (this.isMobile) {
// toggle the menu
const navMain = document.getElementById('nav-main');
// 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();
}
}
}
this.$router.push(route);
}
},
handleWindowResize() {
@@ -315,11 +337,11 @@ const app = Vue.createApp({
},
async mounted() {
document.addEventListener('click', this.handleClick);
window.addEventListener("resize", this.handleWindowResize);
window.addEventListener('resize', this.handleWindowResize);
},
beforeUnmount() {
document.removeEventListener('click', this.handleClick);
window.removeEventListener("resize", this.handleWindowResize);
window.removeEventListener('resize', this.handleWindowResize);
},
});
@@ -334,9 +356,9 @@ app.use(router);
app.use(primevue.config.default, {
zIndex: {
overlay: 9000,
tooltip: 8000
}
})
tooltip: 8000,
},
});
app.directive('tooltip', primevue.tooltip);
app.use(PluginsPhrasen);
app.use(Theme);
+22 -30
View File
@@ -1,28 +1,28 @@
import Pagination from "../../Pagination/Pagination.js";
import StudiengangInformation from "./StudiengangInformation/StudiengangInformation.js";
import BsConfirm from "../../Bootstrap/Confirm.js";
import Pagination from '../../Pagination/Pagination.js';
import StudiengangInformation from './StudiengangInformation/StudiengangInformation.js';
import BsConfirm from '../../Bootstrap/Confirm.js';
import ApiCms from "../../../api/factory/cms.js";
import ApiCms from '../../../api/factory/cms.js';
const MAX_NEW_AGE = 60;
export default {
name: "NewsComponent",
name: 'NewsComponent',
components: {
Pagination,
StudiengangInformation,
},
inject: ["isMobile"],
inject: ['isMobile'],
data() {
return {
content: null,
maxPageCount: 0,
page_size: 10,
page_size: 100,
page: 1,
};
},
watch: {
"$p.user_language.value": function (sprache) {
'$p.user_language.value': function (sprache) {
this.fetchNews();
},
},
@@ -37,9 +37,10 @@ export default {
ApiCms.getNews(this.page, this.page_size, this.sprache, MAX_NEW_AGE),
);
this.content = newsResponse.data;
this.maxPageCount = newsResponse.meta.row_count;
document.querySelectorAll("#cms [data-confirm]").forEach((el) => {
el.addEventListener("click", (evt) => {
document.querySelectorAll('#cms [data-confirm]').forEach((el) => {
el.addEventListener('click', (evt) => {
evt.preventDefault();
BsConfirm.popup(el.dataset.confirm)
.then(() => {
@@ -48,12 +49,12 @@ export default {
// TODO(chris): check for success then show message and/or reload
location = location;
})
.catch((err) => console.error("ERROR:", err));
.catch((err) => console.error('ERROR:', err));
})
.catch(() => {});
});
});
document.querySelectorAll("#cms [data-href]").forEach((el) => {
document.querySelectorAll('#cms [data-href]').forEach((el) => {
el.href = el.dataset.href.replace(
/^ROOT\//,
FHC_JS_DATA_STORAGE_OBJECT.app_root,
@@ -75,36 +76,27 @@ export default {
},
formatExternalHtml() {
document
.querySelectorAll(".news-list-item .card-header")
.querySelectorAll('.news-list-item .card-header')
.forEach((el) => {
el.classList.add("fhc-primary");
el.classList.add('fhc-primary');
});
document.querySelectorAll(".news-list-item .row").forEach((el) => {
el.classList.add("w-100");
el.classList.add("align-items-center");
document.querySelectorAll('.news-list-item .row').forEach((el) => {
el.classList.add('w-100');
el.classList.add('align-items-center');
});
document.querySelectorAll('.news-list-item .row h2').forEach((el) => {
el.classList.add('mb-0');
});
document
.querySelectorAll(".news-list-item .row h2")
.forEach((el) => {
el.classList.add("mb-0");
});
},
afterPageUpdated(event) {
this.page = event.page;
this.page_size = event.rows;
this.$refs.newsPageHeading.scrollIntoView({block: 'end'});
this.$refs.newsPageHeading.scrollIntoView({ block: 'end' });
this.loadNewPageContent(event);
},
},
created() {
this.fetchNews();
this.$api
.call(ApiCms.getNewsRowCount(MAX_NEW_AGE))
.then((res) => res.data)
.then((result) => {
this.maxPageCount = result;
});
},
template: /*html*/ `
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
@@ -0,0 +1,122 @@
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,
},
data() {
return {
isNewsFormShown: false,
isNewsPreviewShown: false,
newsToEdit: null,
newsPreview: null,
};
},
methods: {
async reloadNews() {
await this.$refs.newsList.reload();
},
async handleNewsItemUpdated() {
this.isNewsFormShown = false;
this.isNewsPreviewShown = false;
this.newsToEdit = null;
this.newsPreview = null;
await this.reloadNews();
},
handleNewsItemSaved() {
this.reloadNews();
this.isNewsFormShown = false;
this.isNewsPreviewShown = false;
this.newsToEdit = null;
this.newsPreview = null;
},
showCreateNewsForm() {
this.newsToEdit = null;
this.isNewsPreviewShown = false;
this.isNewsFormShown = true;
},
async editNews(news) {
console.log('Editing news item:', news);
try {
const response = await this.$api.call(
ApiNewsAdministration.getNewsItem(news.newsId),
);
const freshNews = response.data;
if (!freshNews) {
return;
}
this.newsToEdit = freshNews;
this.isNewsPreviewShown = false;
this.isNewsFormShown = true;
} catch (error) {
this.$fhcAlert.handleSystemError(error);
}
},
cancelNewsForm() {
this.isNewsFormShown = false;
this.isNewsPreviewShown = false;
this.newsToEdit = null;
this.newsPreview = null;
},
toggleNewsPreview() {
this.isNewsPreviewShown = !this.isNewsPreviewShown;
},
handlePreviewChange(preview) {
this.newsPreview = preview;
},
},
template: /*html*/ `
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 ref="newsPageHeading" class="fhc-primary-color mb-0">News Administration</h2>
<button
v-if="!isNewsFormShown"
@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="$p.t('news', 'createNews')"
:aria-label="$p.t('news', 'createNews')"
aria-controls="news-item-form"
>
<i class="fa-solid fa-plus" aria-hidden="true"></i>
</button>
</div>
<Transition>
<div v-if="isNewsFormShown" class="row g-4 align-items-start">
<div :class="isNewsPreviewShown ? 'col-12 col-xl-6' : 'col-12'">
<news-item-form
:key="newsToEdit?.newsId || 'new-news-item'"
id="news-item-form"
:news="newsToEdit"
:is-preview-shown="isNewsPreviewShown"
@created="handleNewsItemSaved"
@update="handleNewsItemUpdated"
@cancel="cancelNewsForm"
@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>
</Transition>
<news-list
ref="newsList"
@edit="editNews"
></news-list>
</div>
`,
};
@@ -0,0 +1,45 @@
import BootstrapModal from '../../../Bootstrap/Modal.js';
export default {
name: 'NewsItemAddLanguageModal',
components: {
BootstrapModal,
},
emits: ['add-language', 'hidden'],
props: {
languages: {
type: Array,
default: () => [],
},
},
methods: {
show() {
this.$refs.modal.show();
},
hide() {
this.$refs.modal.hide();
},
},
template: /*html*/ `
<bootstrap-modal
ref="modal"
dialog-class="modal-dialog-centered"
body-class="p-4"
@hidden-bs-modal="$emit('hidden')"
>
<template #title>Add translation language</template>
<div v-if="languages.length" class="d-grid gap-2">
<button
v-for="language in languages"
:key="language.value"
type="button"
class="btn btn-outline-primary text-start"
@click="$emit('add-language', language.value)"
>
{{ language.label }}
</button>
</div>
<p v-else class="mb-0 text-muted">All available languages have been added.</p>
</bootstrap-modal>
`,
};
@@ -0,0 +1,25 @@
export default {
name: 'NewsItemAddLanguageTab',
emits: ['tab-action'],
props: {
config: {
type: Object,
default: () => ({}),
},
},
template: /*html*/ `
<div class="py-4 text-center text-muted">
<button
v-if="config.hasAvailableLanguages"
type="button"
class="btn btn-link text-decoration-none"
aria-label="Add translation language"
@click="$emit('tab-action')"
>
<i class="fa-solid fa-plus-circle fs-4 mb-2" aria-hidden="true"></i>
<span class="d-block">Select a language to add a translation.</span>
</button>
<p v-else class="mb-0">All available languages have been added.</p>
</div>
`,
};
@@ -0,0 +1,242 @@
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
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: 'Blocks', block: 'div' },
{ title: 'Paragraph', block: 'p' },
{ title: 'Heading 1', block: 'h1' },
{ title: 'Heading 2', block: 'h2' },
{ title: 'Heading 3', block: 'h3' },
{ title: 'Heading 4', block: 'h4' },
{ title: 'Heading 5', block: 'h5' },
{ title: 'Heading 6', block: 'h6' },
],
setup: (editor) => {
vm.editor = editor;
editor.on('input change undo redo', () => {
vm.contentData.text = editor.getContent();
});
},
});
},
},
mounted() {
this.initTinyMCE();
},
beforeUnmount() {
this.cleanupDmsFileBrowser(true);
if (this.editor) {
this.editor.destroy();
}
},
template: /*html*/ `
<div :class="{'pb-3': isMobile}" 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="Remove language"
aria-label="Remove language"
@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="$p.t('notiz', 'verfasser')"
name="author"
v-model="contentData.author"
>
</form-input>
<form-input
type="text"
:label="$p.t('global', 'betreff')"
name="title"
v-model="contentData.title"
>
</form-input>
<form-input
type="checkbox"
:label="$p.t('ui', 'publish')"
name="isPublished"
v-model="contentData.isPublished"
>
</form-input>
</div>
<form-input
ref="editor"
:label="$p.t('global','text') + ' *'"
type="textarea"
name="text"
v-model="contentData.text"
rows="5"
cols="75"
>
</form-input>
</div>
`,
};
@@ -0,0 +1,97 @@
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');
},
},
template: /*html*/ `
<bootstrap-modal
ref="modal"
dialog-class="modal-dialog-centered"
body-class="p-4"
@hidden-bs-modal="resetSelection"
>
<template #title>
{{ selectedSourceLanguage ? 'Overwrite translation content?' : 'Copy translation content' }}
</template>
<template v-if="selectedSourceLanguage">
<p class="mb-3">
Copying from <strong>{{ selectedSourceLanguage.label }}</strong> will replace the author, title, and text in <strong>{{ targetLanguageLabel }}</strong>.
</p>
<p class="text-muted small">The existing content in the current language will be lost.</p>
<div class="d-flex justify-content-end gap-2 mt-4">
<button type="button" class="btn btn-outline-secondary" @click="selectedSourceLanguage = null">
Choose another language
</button>
<button type="button" class="btn btn-primary" @click="copyTranslation()">
Overwrite and copy
</button>
</div>
</template>
<template v-else>
<p class="text-muted">Choose the language whose content should be copied into {{ targetLanguageLabel }}.</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">Add another language before copying content.</p>
</template>
</bootstrap-modal>
`,
};
@@ -0,0 +1,636 @@
import CoreForm from '../../../Form/Form.js';
import FormInput from '../../../Form/Input.js';
import FhcTabs from '../../../Tabs.js';
import NewsItemAddLanguageModal from './NewsItemAddLanguageModal.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,
NewsItemAddLanguageModal,
NewsItemCopyTranslationModal,
},
emits: [
'created',
'update',
'cancel',
'translation-copied',
'preview-change',
'toggle-preview',
],
props: {
news: {
type: Object,
default: null,
},
isPreviewShown: Boolean,
},
data() {
return {
degreePrograms: [],
filteredDegreePrograms: [],
isSaving: false,
activeContentFormKey: 'germanContentForm',
contentFormRenderKey: 0,
availableLanguages: [
{ label: 'German', value: 'German' },
{ label: 'English', value: 'English' },
{ label: 'French', value: 'French' },
{ label: 'Spanish', value: 'Spanish' },
],
semesters: [
{
label: 'Alle Semester',
value: null,
},
{
label: '1. Semester',
value: '1',
},
{
label: '2. Semester',
value: '2',
},
{
label: '3. Semester',
value: '3',
},
{
label: '4. Semester',
value: '4',
},
{
label: '5. Semester',
value: '5',
},
{
label: '6. Semester',
value: '6',
},
{
label: '7. Semester',
value: '7',
},
{
label: '8. Semester',
value: '8',
},
],
formData: {
visibleFrom: null,
visibleTo: null,
degreeProgram: null,
semester: null,
translations: [
{
language: 'German',
author: '',
title: '',
text: '',
isPublished: false,
},
],
},
contentFormItems: {
germanContentForm: {
title: 'German Content Form',
component: BASE_COMPONENT_URL + 'NewsItemContentForm.js',
config: {
language: 'German',
type: 'news',
},
key: 'germanContentForm',
},
addLanguage: {
title: '+',
component: BASE_COMPONENT_URL + 'NewsItemAddLanguageTab.js',
config: { hasAvailableLanguages: true },
key: 'addLanguage',
},
},
};
},
watch: {
'$p.user_language.value': function (sprache) {
this.fetchNews();
},
formData: {
deep: true,
immediate: true,
handler() {
this.emitPreviewChange();
},
},
activeContentLanguage() {
this.emitPreviewChange();
},
},
computed: {
sprache: function () {
return this.$p.user_language.value;
},
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: `${language} Content Form`,
component: BASE_COMPONENT_URL + 'NewsItemContentForm.js?124',
config: {
language,
type: 'news',
},
key,
};
},
showLanguageModal() {
this.$refs.addLanguageModal.show();
},
showCopyTranslationModal() {
this.$refs.copyTranslationModal.show();
},
getLanguageLabel(language) {
return (
this.availableLanguages.find(
(availableLanguage) => availableLanguage.value === language,
)?.label ?? 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) {
if (key === 'addLanguage') {
this.showLanguageModal();
return;
}
this.activeContentFormKey = key;
},
handleContentTabAction({ key, payload }) {
if (key === 'addLanguage') {
this.showLanguageModal();
return;
}
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.$refs.addLanguageModal.hide();
this.$nextTick(() => this.$refs.tabs.change(key));
},
restoreActiveContentTab() {
this.$refs.tabs.change(this.activeContentFormKey);
},
fillFormData(news) {
if (!news) {
return;
}
this.formData.visibleFrom =
news.visibleFrom ?? news.dateTime?.slice(0, 10) ?? null;
this.formData.visibleTo = news.visibleTo ?? null;
const degreeProgramShortCode =
news.degreeProgramShortCode ?? news.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 = news.semester ?? null;
const translations = news.translations ?? [
{
language: news.language,
author: news.author,
title: news.title,
text: news.content,
isPublished: news.isPublished,
},
];
translations.forEach((sourceTranslation) => {
const translation = this.formData.translations.find(
(item) => item.language === sourceTranslation.language,
);
if (translation) {
Object.assign(translation, sourceTranslation);
return;
}
this.formData.translations.push({
language: sourceTranslation.language,
author: sourceTranslation.author ?? '',
title: sourceTranslation.title ?? '',
text: sourceTranslation.text ?? '',
isPublished: Boolean(sourceTranslation.isPublished),
});
});
},
filterDegreePrograms(event) {
let defaultItem = {
label: this.$p.t('ui', 'dropdownEmptyOption'),
value: null,
};
const query = event.query.toLowerCase();
if (!query) {
return (this.filteredDegreePrograms = [
defaultItem,
...this.dropdownParsedDegreePrograms,
]);
}
return (this.filteredDegreePrograms = [defaultItem]
.concat(this.dropdownParsedDegreePrograms)
.filter((unit) => {
return unit.label.toLowerCase().includes(query);
}));
},
async storeNewsItem() {
let parsedFormData = JSON.parse(JSON.stringify(this.formData));
parsedFormData.degreeProgramShortCode =
parsedFormData.degreeProgram?.value;
delete parsedFormData.degreeProgram;
let response;
try {
response = await this.$refs.form.call(
ApiNewsAdministration.storeNewsItem(parsedFormData),
);
} catch (error) {
console.error('Error storing news item:', error);
this.$fhcAlert.handleSystemError(error);
return;
}
if (response.meta.status !== 'success') {
this.$fhcAlert.alertError(this.$p.t('ui', 'errorStoringNewsItem'));
return;
}
this.$fhcAlert.alertSuccess(
this.$p.t('ui', 'newsItemStoredSuccessfully'),
);
this.$emit('created');
},
async updateNewsItem() {
if (!this.news || 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.news.newsId,
parsedFormData,
),
);
if (response.meta.status !== 'success') {
this.$fhcAlert.alertError(this.$p.t('ui', 'errorStoringNewsItem'));
return;
}
this.$fhcAlert.alertSuccess(
this.$p.t('ui', 'newsItemStoredSuccessfully'),
);
this.$emit('update');
} catch (error) {
if (!error?.handled) {
this.$fhcAlert.handleSystemError(error);
}
} finally {
this.isSaving = false;
}
},
},
async created() {
this.fillFormData(this.news);
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),
);
this.fillFormData(this.news);
} else {
this.$fhcAlert.alertError(this.$p.t('ui', 'errorFetchingDegreePrograms'));
}
console.log(FHC_JS_DATA_STORAGE_OBJECT);
},
template: /*html*/ `
<section
:class="{'pb-3': isMobile}"
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">News Form</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="isPreviewShown ? 'Hide preview' : 'Show preview'"
:aria-label="isPreviewShown ? 'Hide preview' : 'Show preview'"
: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="$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="$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="copySourceLanguages.length ? 'Copy content from another language' : 'Add another language to copy content'"
aria-haspopup="dialog"
>
<i class="fa-solid fa-copy me-1" aria-hidden="true"></i>
Copy content from another language
</button>
</div>
<news-item-add-language-modal
ref="addLanguageModal"
:languages="languagesToAdd"
@add-language="addLanguage"
@hidden="restoreActiveContentTab"
></news-item-add-language-modal>
<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 class="d-flex justify-content-end gap-2 mt-4 pt-3 border-top">
<button
@click="$emit('cancel')"
type="button"
class="btn btn-secondary"
>
{{$p.t('ui', 'abbrechen')}}
</button>
<button
@click="news ? updateNewsItem() : storeNewsItem()"
type="button"
class="btn btn-primary"
:disabled="isSaving"
>
{{$p.t('global', 'speichern')}}
</button>
</div>
</core-form>
</div>
</section>
`,
};
@@ -0,0 +1,100 @@
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: {
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,
);
},
},
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] ?? '');
},
},
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">News preview</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"
>
{{ 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 || 'Untitled news' }}</h5>
<span v-if="!translation.isPublished" class="badge text-bg-secondary">Not published yet</span>
</div>
<address v-if="translation.author" class="small mb-0 mt-2">{{ translation.author }}</address>
</header>
<div class="card-body">
<div v-if="translation.text" class="card-text" v-html="translation.text"></div>
<p v-else class="text-muted mb-0">Enter text to see the news content preview.</p>
</div>
</article>
<p v-else class="text-muted mb-0">Select a language tab to preview its content.</p>
</div>
</section>
`,
};
@@ -0,0 +1,154 @@
import Pagination from '../../../Pagination/Pagination.js';
import ApiNewsAdministration from '../../../../api/factory/newsAdministration.js';
import NewsListItem from './NewsListItem.js';
const MAX_NEW_AGE = 60;
export default {
name: 'NewsList',
emits: ['edit'],
components: {
NewsListItem,
Pagination,
},
data() {
return {
newsItems: [],
isLoading: false,
loadError: null,
maxPageCount: 0,
page: 1,
pageSize: 10,
};
},
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,
MAX_NEW_AGE,
'all',
),
);
this.newsItems = this.parseNewsHtml(response.data);
this.maxPageCount = response.meta.row_count;
} catch (error) {
this.newsItems = [];
this.loadError = this.$p.t('global', 'fehlerBeimLadenDesDatensatzes');
} 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) {
console.log('Parsing news item:', 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 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,
};
},
afterPageUpdated(event) {
this.page = event.page;
this.pageSize = event.rows;
this.fetchNews();
this.$refs.newsListHeading.scrollIntoView({ block: 'end' });
},
},
created() {
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">
News
</h3>
<hr>
<pagination
v-if="maxPageCount > pageSize"
:page="page"
:page_size="pageSize"
:maxPageCount="maxPageCount"
@pageUpdated="afterPageUpdated"
></pagination>
<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">Loading news</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">
No news available.
</div>
<div v-else>
<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>
</div>
<pagination
v-if="maxPageCount > pageSize"
:page="page"
:page_size="pageSize"
:maxPageCount="maxPageCount"
@pageUpdated="afterPageUpdated"
></pagination>
</section>
`,
};
@@ -0,0 +1,133 @@
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.$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;
}
},
},
template: /*html*/ `
<article
class="card mb-3"
:class="{'border-secondary': !news.isPublished}"
>
<header
class="card-header d-flex justify-content-between align-items-center gap-3"
:class="news.isPublished ? 'fhc-primary' : 'bg-secondary bg-opacity-25 text-secondary'"
>
<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">
Not published yet
</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="News 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="Edit news"
aria-label="Edit news"
@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="Delete news"
aria-label="Delete news"
: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>
`,
};
@@ -1,85 +0,0 @@
import FormInput from "../../Form/Input.js";
export default {
name: "NewsItemContentForm",
components: {
FormInput,
},
props: {
config: {
type: Object,
default: () => ({})
}
},
methods: {
initTinyMCE() {
const vm = this;
tinymce.init({
target: this.$refs.editor.$refs.input, //Important: not selector: to enable multiple import of component
//height: 800,
//plugins: ['lists'],
//toolbar: " blocks | bold italic underline | alignleft aligncenter alignright alignjustify",
toolbar: 'styleselect | bold italic underline | alignleft aligncenter alignright alignjustify',
style_formats: [
{title: 'Blocks', block: 'div'},
{title: 'Paragraph', block: 'p'},
{title: 'Heading 1', block: 'h1'},
{title: 'Heading 2', block: 'h2'},
{title: 'Heading 3', block: 'h3'},
{title: 'Heading 4', block: 'h4'},
{title: 'Heading 5', block: 'h5'},
{title: 'Heading 6', block: 'h6'},
],
autoresize_bottom_margin: 16,
setup: (editor) => {
vm.editor = editor;
editor.on('input', () => {
const newContent = editor.getContent();
vm.notizData.text = newContent;
});
},
});
},
},
mounted() {
this.initTinyMCE();
},
template: /*html*/ `
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
<div class="d-flex flex-column flex-md-row align-items-md-end gap-3">
<form-input
type="text"
:label="$p.t('wawi/nummer')"
name="nummer"
>
</form-input>
<form-input
type="text"
:label="$p.t('wawi/nummer')"
name="nummer"
>
</form-input>
<form-input
type="checkbox"
:label="$p.t('wawi/nummer')"
name="nummer"
>
</form-input>
</div>
<form-input
ref="editor"
:label="$p.t('global','text') + ' *'"
type="textarea"
name="text"
rows="5"
cols="75"
>
>
</form-input>
</div>
`,
};
@@ -1,116 +0,0 @@
import CoreForm from "../../Form/Form.js";
import FormInput from "../../Form/Input.js";
import FhcTabs from "../../Tabs.js";
const BASE_COMPONENT_URL =
"https://c3p0.dev.technikum-wien.at/ma1433/core/FHC-Core/public/js/components/Cis/Cms/";
export default {
name: "NewsItemForm",
components: {
CoreForm,
FormInput,
FhcTabs,
},
data() {
return {
contentFormItems: {
germanContentForm: {
title: "German Content Form",
component: BASE_COMPONENT_URL + "NewsItemContentForm.js?123",
config: {
language: "de",
type: "news",
},
key: "germanContentForm",
},
englishContentForm: {
title: "English Content Form",
component: BASE_COMPONENT_URL + "NewsItemContentForm.js?1233",
config: {
language: "en",
type: "news",
},
key: "englishContentForm",
},
},
};
},
watch: {
"$p.user_language.value": function (sprache) {
this.fetchNews();
},
},
computed: {
sprache: function () {
return this.$p.user_language.value;
},
},
methods: {},
created() {},
template: /*html*/ `
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
<h2 ref="newsPageHeading" class="fhc-primary-color">News Form</h2>
<div>
<core-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="$p.t('ui', 'validityPeriod') + ' ' + $p.t('global', 'bis')"
:teleport="true"
:enable-time-picker="false"
type="datePicker"
name="validityPeriodTo"
format="dd.MM.yyyy"
auto-apply
/>
<form-input
:label="$p.t('ui', 'validityPeriod') + ' ' + $p.t('global', 'bis')"
:teleport="true"
:enable-time-picker="false"
type="datePicker"
name="validityPeriodTo"
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/studiensemester'))"
:suggestions="filteredSemesters"
:optionValue="(option) => option.value"
:optionLabel="(option) => option.label"
@complete="filterSemesters($event)"
type="autocomplete"
name="selectedSemester"
dropdown
forceSelection
>
</form-input>
<form-input
:label="$capitalize($p.t('lehre/studiensemester'))"
:suggestions="filteredSemesters"
:optionValue="(option) => option.value"
:optionLabel="(option) => option.label"
@complete="filterSemesters($event)"
type="autocomplete"
name="selectedSemester"
dropdown
forceSelection
>
</form-input>
</div>
</div>
<fhc-tabs
ref="tabs"
:useprimevue="true"
:config="contentFormItems"
style="flex: 1 1 0%; height: 0%"
class="mt-3"
>
</fhc-tabs>
</core-form>
</div>
</div>
`,
};
-36
View File
@@ -1,36 +0,0 @@
import CoreForm from "../../Form/Form.js";
import FormInput from "../../Form/Input.js";
import Tabs from "../../Tabs.js";
export default {
name: "NewsItemForm",
components: {
CoreForm,
FormInput,
Tabs,
},
data() {
return {
};
},
watch: {
"$p.user_language.value": function (sprache) {
this.fetchNews();
},
},
computed: {
sprache: function () {
return this.$p.user_language.value;
},
},
methods: {
},
created() {
},
template: /*html*/ `
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
test
</div>
`,
};
+123 -104
View File
@@ -7,18 +7,17 @@ const MAX_LOADED_NEWS = 30;
const MAX_NEW_AGE = 60;
export default {
name: "WidgetsNews",
name: 'WidgetsNews',
components: {
BsModal
BsModal,
},
data: () => ({
allNewsList: [],
singleNews: {},
selected: null,
size:0,
size: 0,
}),
props: ['width', 'height']
,
props: ['width', 'height'],
mixins: [AbstractWidget],
computed: {
sizeClass() {
@@ -29,7 +28,6 @@ export default {
// let quantity = this.width;
let quantity = MAX_LOADED_NEWS;
if (this.width === 1) {
quantity = this.height === 1 ? 4 : MAX_LOADED_NEWS;
}
@@ -40,107 +38,129 @@ export default {
},
carouselItems() {
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);
return acc
}, [])
}
return acc;
}, []);
},
},
methods: {
updateNewsContentClasses:function(){
updateNewsContentClasses: function () {
Vue.nextTick(() => {
document.querySelectorAll(".fhc-news-card-item .card-body, .fhc-news-card-item .card, .fhc-news-card-item .card-header").forEach((el) => {
if (!el.classList.contains("border-0")) {
el.classList.add("border-0");
}
});
document.querySelectorAll(".fhc-news-card-item .card-header").forEach((el) => {
if (!el.classList.contains("px-5")) {
el.classList.add("px-5");
}
if (!el.classList.contains("fhc-primary")) {
el.classList.add("fhc-primary");
}
if (!el.classList.contains("position-sticky")) {
el.classList.add("position-sticky");
}
if (!el.classList.contains("top-0")) {
el.classList.add("top-0");
}
});
document.querySelectorAll(".fhc-news-card-item .card-header .row").forEach((el) => {
if (!el.classList.contains("w-100")) {
el.classList.add("w-100");
}
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");
}
});
})
document
.querySelectorAll(
'.fhc-news-card-item .card-body, .fhc-news-card-item .card, .fhc-news-card-item .card-header',
)
.forEach((el) => {
if (!el.classList.contains('border-0')) {
el.classList.add('border-0');
}
});
document
.querySelectorAll('.fhc-news-card-item .card-header')
.forEach((el) => {
if (!el.classList.contains('px-5')) {
el.classList.add('px-5');
}
if (!el.classList.contains('fhc-primary')) {
el.classList.add('fhc-primary');
}
if (!el.classList.contains('position-sticky')) {
el.classList.add('position-sticky');
}
if (!el.classList.contains('top-0')) {
el.classList.add('top-0');
}
});
document
.querySelectorAll('.fhc-news-card-item .card-header .row')
.forEach((el) => {
if (!el.classList.contains('w-100')) {
el.classList.add('w-100');
}
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) {
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) {
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]';
},
setNext(){
const thisIndex = this.allNewsList.findIndex(n=>n.news_id == this.selected.news_id)
const nextIndex = thisIndex == (this.allNewsList.length - 1) ? 0 : thisIndex + 1
setNext() {
const thisIndex = this.allNewsList.findIndex(
(n) => n.news_id == this.selected.news_id,
);
const nextIndex =
thisIndex == this.allNewsList.length - 1 ? 0 : thisIndex + 1;
this.setSelected(this.allNewsList[nextIndex])
this.setSelected(this.allNewsList[nextIndex]);
this.updateNewsContentClasses();
},
setPrev() {
const thisIndex = this.allNewsList.findIndex(n=>n.news_id == this.selected.news_id)
const prevIndex = thisIndex ? thisIndex - 1 : this.allNewsList.length - 1
const thisIndex = this.allNewsList.findIndex(
(n) => n.news_id == this.selected.news_id,
);
const prevIndex = thisIndex ? thisIndex - 1 : this.allNewsList.length - 1;
this.setSelected(this.allNewsList[prevIndex])
this.setSelected(this.allNewsList[prevIndex]);
this.updateNewsContentClasses();
},
getMenuItemClass(news) {
let classString = ''
if(this.selected && this.selected.news_id === news.news_id) {
classString += 'selected'
let classString = '';
if (this.selected && this.selected.news_id === news.news_id) {
classString += 'selected';
}
return classString
return classString;
},
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 oldElementIndex = this.allNewsList.indexOf(this.selected);
//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
if(clickedElementIndex > oldElementIndex) {
if (clickedElementIndex > oldElementIndex) {
clickedElement.classList.add('carousel-item-next');
}else{
} else {
clickedElement.classList.add('carousel-item-prev');
}
// move to clicked element
await Vue.nextTick(() => { this.carouselInstance.to(clickedElementIndex); })
await Vue.nextTick(() => {
this.carouselInstance.to(clickedElementIndex);
});
this.selected = news;
},
contentURI: function (content_id) {
return (
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/CisVue/Cms/content/" +
'/CisVue/Cms/content/' +
content_id
);
},
@@ -148,7 +168,7 @@ export default {
return (
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/CisVue/Cms/news"
'/CisVue/Cms/news'
);
},
setSingleNews(singleNews) {
@@ -156,73 +176,72 @@ export default {
this.$refs.newsModal.show();
},
initCarouselInstance() {
Vue.nextTick(()=> {
if(this.$refs.carousel) { // carousel ref might not exist in every widget width/height
Vue.nextTick(() => {
if (this.$refs.carousel) {
// carousel ref might not exist in every widget width/height
this.carouselInstance = new bootstrap.Carousel(this.$refs.carousel, {
wrap: false, // keep this off even though it actually wraps
interval: false
interval: false,
});
}
})
});
},
initActiveItem() {
Vue.nextTick(()=> {
if (Array.isArray(this.$refs.carouselItems) && this.$refs.carouselItems.length >0) {
this.$refs.carouselItems[0].classList.add("active")
Vue.nextTick(() => {
if (
Array.isArray(this.$refs.carouselItems) &&
this.$refs.carouselItems.length > 0
) {
this.$refs.carouselItems[0].classList.add('active');
}
})
}
});
},
},
watch: {
width(newVal, oldVal) {
if(oldVal == 1 && newVal > 1) { // carousel instance will have been disposed
this.initCarouselInstance()
this.initActiveItem()
if (oldVal == 1 && newVal > 1) {
// carousel instance will have been disposed
this.initCarouselInstance();
this.initActiveItem();
}
}
},
},
created() {
this.$emit("setConfig", false);
this.$emit('setConfig', false);
this.$api
.call(ApiCms.news(MAX_LOADED_NEWS, MAX_NEW_AGE))
.then(res => res.data)
.call(ApiCms.newsForWidget(MAX_LOADED_NEWS, MAX_NEW_AGE))
.then((res) => res.data)
.then((news) => {
this.allNewsList = Array.from(Object.values(news));
this.allNewsList.sort((a, b) => {
return new Date(b.datum) - new Date(a.datum);
});
this.selected = this.allNewsList.length ? this.allNewsList[0] : null
this.initActiveItem()
this.selected = this.allNewsList.length ? this.allNewsList[0] : null;
this.initActiveItem();
this.updateNewsContentClasses();
})
.catch((err) => {
console.error("ERROR: ", err);
console.error('ERROR: ', err);
});
},
mounted() {
if (this.$refs.container) {
new ResizeObserver(entries => {
new ResizeObserver((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
if (w > 600)
this.size = 3;
else if (w > 350)
this.size = 2;
else if (w > 250)
this.size = 1;
else
this.size = 0;
}
if (w > 600) this.size = 3;
else if (w > 350) this.size = 2;
else if (w > 250) this.size = 1;
else this.size = 0;
}
}).observe(this.$refs.container);
}
this.initCarouselInstance()
this.initCarouselInstance();
},
template: /*html*/ `
<div ref="container" class="widgets-news h-100" :class="sizeClass" >
+4 -2
View File
@@ -14,7 +14,8 @@ export default {
emits: [
'update:modelValue',
'change',
'changed'
'changed',
'tab-action'
],
props: {
config: {
@@ -234,6 +235,7 @@ export default {
v-model="value"
:config="tab.config"
@update:suffix="updateSuffix($event)"
@tab-action="$emit('tab-action', { key: tab.key, payload: $event })"
></component>
</keep-alive>
</tabpanel>
@@ -265,6 +267,7 @@ export default {
v-model="value"
:config="currentTab.config"
@update:suffix="updateSuffix($event)"
@tab-action="$emit('tab-action', { key: currentTab.key, payload: $event })"
></component>
</keep-alive>
</div>
@@ -272,4 +275,3 @@ export default {
</template>`
};
+40
View File
@@ -60359,6 +60359,46 @@ 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'
)
)
),
// generic tabulator phrases END
);