diff --git a/application/config/routes.php b/application/config/routes.php index 8024f449a..11e9e3280 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -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'; diff --git a/application/controllers/CisVue/Cms.php b/application/controllers/CisVue/Cms.php index aa07b919f..0b6b0c3d2 100644 --- a/application/controllers/CisVue/Cms.php +++ b/application/controllers/CisVue/Cms.php @@ -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 diff --git a/application/controllers/api/frontend/v1/Cms.php b/application/controllers/api/frontend/v1/Cms.php index 4b5ca5334..8c2819eec 100644 --- a/application/controllers/api/frontend/v1/Cms.php +++ b/application/controllers/api/frontend/v1/Cms.php @@ -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"]); } } - diff --git a/application/controllers/api/frontend/v1/NewsAdministrationAPI.php b/application/controllers/api/frontend/v1/NewsAdministrationAPI.php new file mode 100644 index 000000000..163fe4ff8 --- /dev/null +++ b/application/controllers/api/frontend/v1/NewsAdministrationAPI.php @@ -0,0 +1,815 @@ +. + */ + +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); + } + + + +} diff --git a/application/controllers/api/frontend/v1/Studgang.php b/application/controllers/api/frontend/v1/Studgang.php index 82aebb666..9d824967b 100644 --- a/application/controllers/api/frontend/v1/Studgang.php +++ b/application/controllers/api/frontend/v1/Studgang.php @@ -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); diff --git a/application/libraries/CmsLib.php b/application/libraries/CmsLib.php index fda246195..8d4833a12 100644 --- a/application/libraries/CmsLib.php +++ b/application/libraries/CmsLib.php @@ -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 = ''; $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 = 'format('d.m.Y') . ']]>'; $datum .= 'format('Y-m-d H:i') . ']]>'; $id = $edit ? 'news_id . ']]>' : ''; - $xml .= "" . $newsobj->content . $datum . $id . ""; + $isPublished = $edit ? 'sichtbar ? 'true' : 'false') . ']]>' : ''; + $xml .= "" . $newsobj->content . $datum . $id . $isPublished . ""; } - /* 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 .= 'bezeichnung . ']]>'; - } - } */ - if ($titel != '') { $xml .= '' . $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 + ]); } } diff --git a/application/models/content/News_model.php b/application/models/content/News_model.php index 1c2b40c0c..2ec84e6aa 100644 --- a/application/models/content/News_model.php +++ b/application/models/content/News_model.php @@ -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(); - } - - } diff --git a/application/models/organisation/Studiengang_model.php b/application/models/organisation/Studiengang_model.php index 1db658596..a4d72991c 100644 --- a/application/models/organisation/Studiengang_model.php +++ b/application/models/organisation/Studiengang_model.php @@ -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')) { diff --git a/application/views/CisRouterView/CisRouterView.php b/application/views/CisRouterView/CisRouterView.php index fdf31ed31..67a3603ac 100644 --- a/application/views/CisRouterView/CisRouterView.php +++ b/application/views/CisRouterView/CisRouterView.php @@ -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', diff --git a/cms/newsverwaltung.php b/cms/newsverwaltung.php index 8d21d9f14..4497b2e00 100644 --- a/cms/newsverwaltung.php +++ b/cms/newsverwaltung.php @@ -86,11 +86,11 @@ echo ' - - - - - + + + + + diff --git a/cms/tinymce_dms.php b/cms/tinymce_dms.php index 9971eb808..019950601 100644 --- a/cms/tinymce_dms.php +++ b/cms/tinymce_dms.php @@ -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"); diff --git a/public/js/api/cms.js b/public/js/api/cms.js index 10e722b1a..e09b33808 100644 --- a/public/js/api/cms.js +++ b/public/js/api/cms.js @@ -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", - {} - ); - } - } \ No newline at end of file +}; diff --git a/public/js/api/factory/cms.js b/public/js/api/factory/cms.js index 7ce81a8bd..a47e68de4 100644 --- a/public/js/api/factory/cms.js +++ b/public/js/api/factory/cms.js @@ -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' - }; - } -}; \ No newline at end of file +}; diff --git a/public/js/api/factory/newsAdministration.js b/public/js/api/factory/newsAdministration.js new file mode 100644 index 000000000..ae4adaa6a --- /dev/null +++ b/public/js/api/factory/newsAdministration.js @@ -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 . + */ + +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}`, + }; + }, +}; diff --git a/public/js/api/factory/studiengang.js b/public/js/api/factory/studiengang.js index 6d5ae15aa..47e715287 100644 --- a/public/js/api/factory/studiengang.js +++ b/public/js/api/factory/studiengang.js @@ -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 }, }; - } -}; \ No newline at end of file + }, +}; diff --git a/public/js/apps/Cis/Cis.js b/public/js/apps/Cis/Cis.js index 43be92e9a..83218ea0d 100644 --- a/public/js/apps/Cis/Cis.js +++ b/public/js/apps/Cis/Cis.js @@ -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); diff --git a/public/js/components/Cis/Cms/News.js b/public/js/components/Cis/Cms/News.js index a2941b49e..c7824e73e 100644 --- a/public/js/components/Cis/Cms/News.js +++ b/public/js/components/Cis/Cms/News.js @@ -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*/ `
diff --git a/public/js/components/Cis/Cms/NewsAdministration/NewsAdministration.js b/public/js/components/Cis/Cms/NewsAdministration/NewsAdministration.js new file mode 100644 index 000000000..9108770cf --- /dev/null +++ b/public/js/components/Cis/Cms/NewsAdministration/NewsAdministration.js @@ -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*/ ` +
+
+

News Administration

+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ `, +}; diff --git a/public/js/components/Cis/Cms/NewsAdministration/NewsItemAddLanguageModal.js b/public/js/components/Cis/Cms/NewsAdministration/NewsItemAddLanguageModal.js new file mode 100644 index 000000000..5a72df8f8 --- /dev/null +++ b/public/js/components/Cis/Cms/NewsAdministration/NewsItemAddLanguageModal.js @@ -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*/ ` + + +
+ +
+

All available languages have been added.

+
+ `, +}; diff --git a/public/js/components/Cis/Cms/NewsAdministration/NewsItemAddLanguageTab.js b/public/js/components/Cis/Cms/NewsAdministration/NewsItemAddLanguageTab.js new file mode 100644 index 000000000..1f2a74f6e --- /dev/null +++ b/public/js/components/Cis/Cms/NewsAdministration/NewsItemAddLanguageTab.js @@ -0,0 +1,25 @@ +export default { + name: 'NewsItemAddLanguageTab', + emits: ['tab-action'], + props: { + config: { + type: Object, + default: () => ({}), + }, + }, + template: /*html*/ ` +
+ +

All available languages have been added.

+
+ `, +}; diff --git a/public/js/components/Cis/Cms/NewsAdministration/NewsItemContentForm.js b/public/js/components/Cis/Cms/NewsAdministration/NewsItemContentForm.js new file mode 100644 index 000000000..f7d7c81d6 --- /dev/null +++ b/public/js/components/Cis/Cms/NewsAdministration/NewsItemContentForm.js @@ -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*/ ` +
+ +
+ + + + + + +
+ + + +
+ `, +}; diff --git a/public/js/components/Cis/Cms/NewsAdministration/NewsItemCopyTranslationModal.js b/public/js/components/Cis/Cms/NewsAdministration/NewsItemCopyTranslationModal.js new file mode 100644 index 000000000..ede2cb765 --- /dev/null +++ b/public/js/components/Cis/Cms/NewsAdministration/NewsItemCopyTranslationModal.js @@ -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*/ ` + + + + + + `, +}; diff --git a/public/js/components/Cis/Cms/NewsAdministration/NewsItemForm.js b/public/js/components/Cis/Cms/NewsAdministration/NewsItemForm.js new file mode 100644 index 000000000..bc6cfeb70 --- /dev/null +++ b/public/js/components/Cis/Cms/NewsAdministration/NewsItemForm.js @@ -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*/ ` +
+
+
+

News Form

+ +
+ +
+
+ + +
+
+ + + + + +
+
+ + +
+ +
+ + +
+ + +
+
+
+
+ `, +}; diff --git a/public/js/components/Cis/Cms/NewsAdministration/NewsItemPreview.js b/public/js/components/Cis/Cms/NewsAdministration/NewsItemPreview.js new file mode 100644 index 000000000..e22113905 --- /dev/null +++ b/public/js/components/Cis/Cms/NewsAdministration/NewsItemPreview.js @@ -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*/ ` +
+
+
+

News preview

+ + + +
+
+
+
+
{{ translation.title || 'Untitled news' }}
+ Not published yet +
+
{{ translation.author }}
+
+
+
+

Enter text to see the news content preview.

+
+
+

Select a language tab to preview its content.

+
+
+ `, +}; diff --git a/public/js/components/Cis/Cms/NewsAdministration/NewsList.js b/public/js/components/Cis/Cms/NewsAdministration/NewsList.js new file mode 100644 index 000000000..f445b9a53 --- /dev/null +++ b/public/js/components/Cis/Cms/NewsAdministration/NewsList.js @@ -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*/ ` +
+

+ News +

+
+ +
+ + Loading news +
+ +
+ No news available. +
+
+ +
+ +
+ `, +}; diff --git a/public/js/components/Cis/Cms/NewsAdministration/NewsListItem.js b/public/js/components/Cis/Cms/NewsAdministration/NewsListItem.js new file mode 100644 index 000000000..8b393e167 --- /dev/null +++ b/public/js/components/Cis/Cms/NewsAdministration/NewsListItem.js @@ -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*/ ` +
+
+ +
+
+
+ {{ news.author }} +
+ +
+
+ + +
+
+
+
+
+
+
+ `, +}; diff --git a/public/js/components/Cis/Cms/NewsItemContentForm.js b/public/js/components/Cis/Cms/NewsItemContentForm.js deleted file mode 100644 index 7f9eb9d83..000000000 --- a/public/js/components/Cis/Cms/NewsItemContentForm.js +++ /dev/null @@ -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*/ ` -
-
- - - - - - -
- - > - - -
- `, -}; diff --git a/public/js/components/Cis/Cms/NewsItemForm.js b/public/js/components/Cis/Cms/NewsItemForm.js deleted file mode 100644 index 3ee1cd4de..000000000 --- a/public/js/components/Cis/Cms/NewsItemForm.js +++ /dev/null @@ -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*/ ` -
-

News Form

-
- -
-
- - -
-
- - - - -
-
- - -
-
-
- `, -}; diff --git a/public/js/components/Cis/Cms/test.js b/public/js/components/Cis/Cms/test.js deleted file mode 100644 index c72ca8138..000000000 --- a/public/js/components/Cis/Cms/test.js +++ /dev/null @@ -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*/ ` -
- test -
- `, -}; diff --git a/public/js/components/DashboardWidget/News.js b/public/js/components/DashboardWidget/News.js index df8ca7dc9..60016487a 100644 --- a/public/js/components/DashboardWidget/News.js +++ b/public/js/components/DashboardWidget/News.js @@ -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*/ `
diff --git a/public/js/components/Tabs.js b/public/js/components/Tabs.js index 50a8cf0e7..e7e6eb566 100644 --- a/public/js/components/Tabs.js +++ b/public/js/components/Tabs.js @@ -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 })" > @@ -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 })" >
@@ -272,4 +275,3 @@ export default { ` }; - diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 14be42163..ad679a774 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -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 );