From 94eb7021bba8ba52bb08b7814c8810d4a62a4770 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Fri, 23 Jan 2026 13:48:04 +0100 Subject: [PATCH] created FhcBase.js plugin which coordinates time orderly setup between Alerts, Phrasen & Api Plugins. These have slightly rewritten Adaption as BaseAlerts.js, BasePhrase.js & BaseApi.js which have internal ready states activated once their required dependencies have been injected. Added await/async to several handlers to guarantee initialized cross dependencies in all plugins; While testing new Plugin Setup found a few legacy Instances of $fhcApi in Cis4 Studium.js that were only still working because plugins/Phrasen is still importing the legacyApi. Fixed that code and the corresponding errors in Studium.php that where now alerted into ui! wow! Also added FhcBase Plugin to all apps inside fhc-core/public/js/apps; --- .../controllers/api/frontend/v1/Studium.php | 36 +- public/js/api/factory/studium.js | 37 ++ public/js/apps/Cis.js | 7 +- public/js/apps/Cis/Documents.js | 3 +- public/js/apps/Cis/ProfilUpdateRequests.js | 5 +- public/js/apps/Dashboard/Admin.js | 4 +- public/js/apps/Dashboard/Fhc.js | 5 +- public/js/apps/DashboardAdmin.js | 4 +- public/js/apps/LVVerwaltung.js | 4 +- public/js/apps/LogsViewer/LogsViewer.js | 4 +- public/js/apps/Messages/NewMessage.js | 4 +- public/js/apps/Nachrichten.js | 5 +- public/js/apps/Studentenverwaltung.js | 7 +- public/js/apps/lehre/Antrag.js | 4 +- public/js/apps/lehre/Antrag/Leitung.js | 4 +- public/js/apps/lehre/Antrag/Lvzuweisung.js | 4 +- public/js/apps/lehre/Antrag/Student.js | 4 +- public/js/apps/lehre/lvplanung/LvTemplates.js | 4 +- public/js/components/Cis/Menu.js | 1 - public/js/components/Cis/Mylv/LvUebersicht.js | 7 +- .../Cis/Renderer/Lehreinheit/calendarEvent.js | 2 +- public/js/components/Cis/Studium/Studium.js | 15 +- public/js/plugins/FhcBase/BaseAlert.js | 359 +++++++++++ public/js/plugins/FhcBase/BaseApi.js | 589 ++++++++++++++++++ public/js/plugins/FhcBase/BasePhrasen.js | 100 +++ public/js/plugins/FhcBase/FhcBase.js | 37 ++ 26 files changed, 1189 insertions(+), 66 deletions(-) create mode 100644 public/js/api/factory/studium.js create mode 100644 public/js/plugins/FhcBase/BaseAlert.js create mode 100644 public/js/plugins/FhcBase/BaseApi.js create mode 100644 public/js/plugins/FhcBase/BasePhrasen.js create mode 100644 public/js/plugins/FhcBase/FhcBase.js diff --git a/application/controllers/api/frontend/v1/Studium.php b/application/controllers/api/frontend/v1/Studium.php index d17f0c1a1..7b2bcdb1d 100644 --- a/application/controllers/api/frontend/v1/Studium.php +++ b/application/controllers/api/frontend/v1/Studium.php @@ -29,7 +29,7 @@ class Studium extends FHCAPI_Controller public function __construct() { parent::__construct([ - 'getStudienAllSemester'=> self::PERM_LOGGED, + 'getAllStudienSemester'=> self::PERM_LOGGED, 'getStudiengaengeForStudienSemester'=> self::PERM_LOGGED, 'getStudienplaeneBySemester'=> self::PERM_LOGGED, 'getLvEvaluierungInfo'=> self::PERM_LOGGED, @@ -51,7 +51,7 @@ class Studium extends FHCAPI_Controller //------------------------------------------------------------------------------------------------------------------ // Public methods - public function getStudienAllSemester(){ + public function getAllStudienSemester(){ $parameter_studiensemester = $this->input->get('studiensemester',true); $parameter_studiengang = $this->input->get('studiengang',true); @@ -59,24 +59,26 @@ class Studium extends FHCAPI_Controller $parameter_studienplan = $this->input->get('studienplan',true); $aktuelles_studiensemester = current($this->getDataOrTerminateWithError($this->StudiensemesterModel->getAktOrNextSemester())); - + if($this->getDataOrTerminateWithError($this->StudentModel->isStudent(getAuthUID()))){ - $studentLehrverband =$this->StudentlehrverbandModel->loadWhere(["student_uid" => getAuthUID(), "studiensemester_kurzbz" => $aktuelles_studiensemester->studiensemester_kurzbz]); - $studentLehrverband = current($this->getDataOrTerminateWithError($studentLehrverband)); + $lv_result =$this->StudentlehrverbandModel->loadWhere([ + "student_uid" => getAuthUID(), + "studiensemester_kurzbz" => $aktuelles_studiensemester->studiensemester_kurzbz + ]); + $lv_data = $this->getDataOrTerminateWithError($lv_result); + + if ($studentLehrverband = current($lv_data)) { + $student_studiensemester = $studentLehrverband->studiensemester_kurzbz; + $student_studiengang = $studentLehrverband->studiengang_kz; + $student_semester = $studentLehrverband->semester; + } - $student_studiensemester = $studentLehrverband->studiensemester_kurzbz; - $student_studiengang = $studentLehrverband->studiengang_kz; - $student_semester = $studentLehrverband->semester; $student_studienplan = $this->getStudienPlanFromPrestudentStatus(getAuthPersonId())->studienplan_id; - - if(!isset($parameter_studiensemester)) - $parameter_studiensemester = $student_studiensemester; - if(!isset($parameter_studiengang)) - $parameter_studiengang = $student_studiengang; - if(!isset($parameter_semester)) - $parameter_semester = $student_semester; - if(!isset($parameter_studienplan)) - $parameter_studienplan = $student_studienplan; + + $parameter_studiensemester = $parameter_studiensemester ?? $student_studiensemester; + $parameter_studiengang = $parameter_studiengang ?? $student_studiengang; + $parameter_semester = $parameter_semester ?? $student_semester; + $parameter_studienplan = $parameter_studienplan ?? $student_studienplan; } if(isset($parameter_studiensemester)){ diff --git a/public/js/api/factory/studium.js b/public/js/api/factory/studium.js new file mode 100644 index 000000000..266008deb --- /dev/null +++ b/public/js/api/factory/studium.js @@ -0,0 +1,37 @@ +export default { + getAllStudienSemester(studiensemester=undefined, studiengang=undefined, semester=undefined, studienplan=undefined) { + return { + method: 'get', + url: '/api/frontend/v1/Studium/getAllStudienSemester', + params: {studiensemester, studiengang, semester, studienplan} + }; + }, + getStudiengaengeForStudienSemester(studiensemester) { + return { + method: 'get', + url: `/api/frontend/v1/Studium/getStudiengaengeForStudienSemester/${studiensemester}`, + }; + }, + getStudienplaeneBySemester(studiengang, studiensemester) { + return { + method: 'get', + url: `/api/frontend/v1/Studium/getStudienplaeneBySemester`, + params: { + studiengang, + studiensemester, + } + }; + }, + getLvPlanForStudiensemester(studiensemester_kurzbz, lvid) { + return { + method: 'get', + url: `/api/frontend/v1/LvPlan/getLvPlanForStudiensemester/${studiensemester}/${lvid}`, + }; + }, + getLvEvaluierungInfo(studiensemester_kurzbz, lvid) { + return { + method: 'get', + url: `/api/frontend/v1/Studium/getLvEvaluierungInfo/${studiensemester_kurzbz}/${lvid}`, + }; + } +}; \ No newline at end of file diff --git a/public/js/apps/Cis.js b/public/js/apps/Cis.js index 5b76c5721..7129c590e 100644 --- a/public/js/apps/Cis.js +++ b/public/js/apps/Cis.js @@ -1,9 +1,9 @@ import FhcSearchbar from "../components/searchbar/searchbar.js"; import CisMenu from "../components/Cis/Menu.js"; -import PluginsPhrasen from '../plugins/Phrasen.js'; import ApiSearchbar from '../api/factory/searchbar.js'; import Theme from "../plugins/Theme.js"; - +import FhcBase from '../plugins/FhcBase/FhcBase.js'; +// import PluginsPhrasen from '../plugins/Phrasen.js'; const app = Vue.createApp({ name: 'CisApp', components: { @@ -148,6 +148,7 @@ app.use(primevue.config.default, { tooltip: 8000 } }) -app.use(PluginsPhrasen); +// app.use(PluginsPhrasen); +app.use(FhcBase); app.use(Theme); app.mount('#cis-header'); diff --git a/public/js/apps/Cis/Documents.js b/public/js/apps/Cis/Documents.js index de03769b4..fde3baafd 100644 --- a/public/js/apps/Cis/Documents.js +++ b/public/js/apps/Cis/Documents.js @@ -1,5 +1,6 @@ import PluginsPhrasen from '../../plugins/Phrasen.js'; import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js"; +import FhcBase from "../../plugins/FhcBase/FhcBase.js"; const app = Vue.createApp({ name: 'DocumentsApp', @@ -89,5 +90,5 @@ const app = Vue.createApp({ setScrollbarWidth(); -app.use(PluginsPhrasen); +app.use(FhcBase); app.mount('#content'); diff --git a/public/js/apps/Cis/ProfilUpdateRequests.js b/public/js/apps/Cis/ProfilUpdateRequests.js index 68465b202..36ff48544 100644 --- a/public/js/apps/Cis/ProfilUpdateRequests.js +++ b/public/js/apps/Cis/ProfilUpdateRequests.js @@ -1,7 +1,6 @@ import ProfilUpdateView from "../../components/Cis/ProfilUpdate/ProfilUpdateView.js"; -import PluginsPhrasen from "../../plugins/Phrasen.js"; - import ApiProfilUpdate from '../../api/factory/profilUpdate.js'; +import FhcBase from "../../plugins/FhcBase/FhcBase.js"; // TODO: sobald in verwendung den vue router pfad zu ProfilUpdateView definieren und diese app in component auslagern const app = Vue.createApp({ @@ -34,4 +33,4 @@ const app = Vue.createApp({ }); }, }); -app.use(PluginsPhrasen).mount("#content"); \ No newline at end of file +app.use(FhcBase).mount("#content"); \ No newline at end of file diff --git a/public/js/apps/Dashboard/Admin.js b/public/js/apps/Dashboard/Admin.js index 4a5fcee2f..c706d89fb 100644 --- a/public/js/apps/Dashboard/Admin.js +++ b/public/js/apps/Dashboard/Admin.js @@ -1,6 +1,6 @@ import {CoreNavigationCmpt} from '../../components/navigation/Navigation.js'; import DashboardAdmin from '../../components/Dashboard/Admin.js'; -import PluginsPhrasen from '../../plugins/Phrasen.js'; +import FhcBase from "../../plugins/FhcBase/FhcBase.js"; const app = Vue.createApp({ name: 'AdminApp', @@ -12,5 +12,5 @@ const app = Vue.createApp({ DashboardAdmin } }); -app.use(PluginsPhrasen); +app.use(FhcBase); app.mount('#main'); \ No newline at end of file diff --git a/public/js/apps/Dashboard/Fhc.js b/public/js/apps/Dashboard/Fhc.js index 190ed1a93..08b626135 100644 --- a/public/js/apps/Dashboard/Fhc.js +++ b/public/js/apps/Dashboard/Fhc.js @@ -1,5 +1,6 @@ import FhcDashboard from '../../components/Dashboard/Dashboard.js'; -import PluginsPhrasen from '../../plugins/Phrasen.js'; +import FhcBase from '../../plugins/FhcBase/FhcBase.js'; + import Theme from '../../plugins/Theme.js'; import contrast from '../../directives/contrast.js'; import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js"; @@ -330,7 +331,7 @@ app.use(primevue.config.default, { } }) app.directive('tooltip', primevue.tooltip); -app.use(PluginsPhrasen); +app.use(FhcBase); app.use(Theme); app.directive('contrast', contrast); app.mount('#fhccontent'); diff --git a/public/js/apps/DashboardAdmin.js b/public/js/apps/DashboardAdmin.js index 05b438798..bf3d8bbd7 100644 --- a/public/js/apps/DashboardAdmin.js +++ b/public/js/apps/DashboardAdmin.js @@ -1,6 +1,6 @@ import {CoreNavigationCmpt} from '../components/navigation/Navigation.js'; import DashboardAdmin from '../components/Dashboard/Admin.js'; -import Phrases from "../plugin/Phrasen.js" +import FhcBase from "../plugins/FhcBase/FhcBase.js"; Vue.createApp({ name: 'DashboardAdminApp', @@ -13,4 +13,4 @@ Vue.createApp({ }, mounted() { } -}).use(Phrases).mount('#main'); \ No newline at end of file +}).use(FhcBase).mount('#main'); \ No newline at end of file diff --git a/public/js/apps/LVVerwaltung.js b/public/js/apps/LVVerwaltung.js index f365b74a8..a3b224655 100644 --- a/public/js/apps/LVVerwaltung.js +++ b/public/js/apps/LVVerwaltung.js @@ -1,5 +1,5 @@ import LVVerwaltung from "../components/LVVerwaltung/LVVerwaltung.js"; -import Phrasen from "../plugins/Phrasen.js"; +import FhcBase from "../plugins/FhcBase/FhcBase.js"; const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router; @@ -98,5 +98,5 @@ app overlay: 1100 } }) - .use(Phrasen) + .use(FhcBase) .mount('#main'); diff --git a/public/js/apps/LogsViewer/LogsViewer.js b/public/js/apps/LogsViewer/LogsViewer.js index eeb4b38a7..702077d96 100644 --- a/public/js/apps/LogsViewer/LogsViewer.js +++ b/public/js/apps/LogsViewer/LogsViewer.js @@ -21,7 +21,7 @@ import {LogsViewerTabulatorEventHandlers} from './TabulatorSetup.js'; import {CoreFilterCmpt} from '../../components/filter/Filter.js'; import {CoreNavigationCmpt} from '../../components/navigation/Navigation.js'; -import PluginsPhrasen from '../../plugins/Phrasen.js'; +import FhcBase from "../../plugins/FhcBase/FhcBase.js"; const logsViewerApp = Vue.createApp({ data: function() { @@ -42,5 +42,5 @@ const logsViewerApp = Vue.createApp({ } }); -logsViewerApp.use(PluginsPhrasen).mount('#main'); +logsViewerApp.use(FhcBase).mount('#main'); diff --git a/public/js/apps/Messages/NewMessage.js b/public/js/apps/Messages/NewMessage.js index 693c8b0e2..100073c22 100644 --- a/public/js/apps/Messages/NewMessage.js +++ b/public/js/apps/Messages/NewMessage.js @@ -1,7 +1,7 @@ //TODO Manu //use this instead of Nachrichten.js import NewMessage from "../../components/Messages/Details/NewMessage/NewDiv.js"; -import Phrasen from "../../plugin/Phrasen.js"; +import FhcBase from "../../plugins/FhcBase/FhcBase.js"; const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router; @@ -23,5 +23,5 @@ app overlay: 1100 } }) - .use(Phrasen) + .use(FhcBase) .mount('#main'); diff --git a/public/js/apps/Nachrichten.js b/public/js/apps/Nachrichten.js index ab9baf084..d9275f1b8 100644 --- a/public/js/apps/Nachrichten.js +++ b/public/js/apps/Nachrichten.js @@ -1,6 +1,5 @@ import NewMessage from "../components/Messages/Details/NewMessage/NewDiv.js"; - -import Phrasen from "../plugins/Phrasen.js"; +import FhcBase from "../plugins/FhcBase/FhcBase.js"; const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router; @@ -23,5 +22,5 @@ app overlay: 1100 } }) - .use(Phrasen) + .use(FhcBase) .mount('#main'); \ No newline at end of file diff --git a/public/js/apps/Studentenverwaltung.js b/public/js/apps/Studentenverwaltung.js index e6f77d5f5..b3344f678 100644 --- a/public/js/apps/Studentenverwaltung.js +++ b/public/js/apps/Studentenverwaltung.js @@ -16,10 +16,7 @@ */ import FhcStudentenverwaltung from "../components/Stv/Studentenverwaltung.js"; -import fhcapifactory from "./api/fhcapifactory.js"; - -import PluginsPhrasen from "../plugins/Phrasen.js"; - +import FhcBase from "../plugins/FhcBase/FhcBase.js"; const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router; @@ -217,5 +214,5 @@ app overlay: 1100 } }) - .use(PluginsPhrasen) + .use(FhcBase) .mount('#main'); diff --git a/public/js/apps/lehre/Antrag.js b/public/js/apps/lehre/Antrag.js index 97b4bda97..660d78134 100644 --- a/public/js/apps/lehre/Antrag.js +++ b/public/js/apps/lehre/Antrag.js @@ -1,7 +1,7 @@ import StudierendenantragAntrag from "../../components/Studierendenantrag/Antrag.js"; import StudierendenantragStatus from "../../components/Studierendenantrag/Status.js"; import StudierendenantragInfoblock from "../../components/Studierendenantrag/Infoblock.js"; -import PluginsPhrasen from '../../plugins/Phrasen.js'; +import FhcBase from "../../plugins/FhcBase/FhcBase.js"; const app = Vue.createApp({ name: 'AntragApp', @@ -21,5 +21,5 @@ const app = Vue.createApp({ } }); app - .use(PluginsPhrasen) + .use(FhcBase) .mount('#wrapper'); \ No newline at end of file diff --git a/public/js/apps/lehre/Antrag/Leitung.js b/public/js/apps/lehre/Antrag/Leitung.js index 8fd8b117e..4c09e6a67 100644 --- a/public/js/apps/lehre/Antrag/Leitung.js +++ b/public/js/apps/lehre/Antrag/Leitung.js @@ -1,5 +1,5 @@ import StudierendenantragLeitung from '../../../components/Studierendenantrag/Leitung.js'; -import PluginsPhrasen from '../../../plugins/Phrasen.js'; +import FhcBase from "../../../plugins/FhcBase/FhcBase.js"; const app = Vue.createApp({ name: 'LeitungApp', @@ -8,6 +8,6 @@ const app = Vue.createApp({ } }); app - .use(PluginsPhrasen) + .use(FhcBase) .use(primevue.config.default,{zIndex: {overlay: 9999}}) .mount('#wrapper'); \ No newline at end of file diff --git a/public/js/apps/lehre/Antrag/Lvzuweisung.js b/public/js/apps/lehre/Antrag/Lvzuweisung.js index 7504e7dcf..3a251d8ac 100644 --- a/public/js/apps/lehre/Antrag/Lvzuweisung.js +++ b/public/js/apps/lehre/Antrag/Lvzuweisung.js @@ -1,5 +1,5 @@ import LvZuweisung from '../../../components/Studierendenantrag/Lvzuweisung.js'; -import PluginsPhrasen from '../../../plugins/Phrasen.js'; +import FhcBase from "../../../plugins/FhcBase/FhcBase.js"; const app = Vue.createApp({ name: 'LvzuweisungApp', @@ -13,5 +13,5 @@ const app = Vue.createApp({ } }); app - .use(PluginsPhrasen) + .use(FhcBase) .mount('#wrapper'); \ No newline at end of file diff --git a/public/js/apps/lehre/Antrag/Student.js b/public/js/apps/lehre/Antrag/Student.js index 46150c541..c8995d938 100644 --- a/public/js/apps/lehre/Antrag/Student.js +++ b/public/js/apps/lehre/Antrag/Student.js @@ -1,5 +1,5 @@ import LvPopup from '../../../components/Studierendenantrag/Leitung/LvPopup.js'; -import PluginsPhrasen from '../../../plugins/Phrasen.js'; +import FhcBase from "../../../plugins/FhcBase/FhcBase.js"; const app = Vue.createApp({ name: 'StudentApp', @@ -8,5 +8,5 @@ const app = Vue.createApp({ } }); app - .use(PluginsPhrasen) + .use(FhcBase) .mount('#wrapper'); \ No newline at end of file diff --git a/public/js/apps/lehre/lvplanung/LvTemplates.js b/public/js/apps/lehre/lvplanung/LvTemplates.js index 655d611ce..e13a7b479 100644 --- a/public/js/apps/lehre/lvplanung/LvTemplates.js +++ b/public/js/apps/lehre/lvplanung/LvTemplates.js @@ -17,7 +17,7 @@ import LvTemplateUebersicht from '../../../lehre/lvplanung/LvTemplateUebersicht.js'; import {CoreNavigationCmpt} from '../../../components/navigation/Navigation.js'; -import PluginsPhrasen from "../../../plugins/Phrasen.js"; +import FhcBase from "../../../plugins/FhcBase/FhcBase.js"; const lvTemplatesApp = Vue.createApp({ @@ -30,5 +30,5 @@ const lvTemplatesApp = Vue.createApp({ lvTemplatesApp .use(primevue.config.default,{zIndex: {overlay: 9999}}) - .use(PluginsPhrasen) + .use(FhcBase) .mount('#main') \ No newline at end of file diff --git a/public/js/components/Cis/Menu.js b/public/js/components/Cis/Menu.js index f073d5a38..ce74bbe1e 100644 --- a/public/js/components/Cis/Menu.js +++ b/public/js/components/Cis/Menu.js @@ -3,7 +3,6 @@ import FhcSearchbar from "../searchbar/searchbar.js"; import CisSprachen from "./Sprachen.js" import ThemeSwitch from "./ThemeSwitch.js"; import ApiCisMenu from '../../api/factory/cis/menu.js'; - export default { components: { CisMenuEntry, diff --git a/public/js/components/Cis/Mylv/LvUebersicht.js b/public/js/components/Cis/Mylv/LvUebersicht.js index 1ec93590f..48afb0c62 100644 --- a/public/js/components/Cis/Mylv/LvUebersicht.js +++ b/public/js/components/Cis/Mylv/LvUebersicht.js @@ -2,9 +2,10 @@ import BsModal from "../../Bootstrap/Modal.js"; import LvMenu from "./LvMenu.js"; import ApiAddons from '../../../api/factory/addons.js'; +import ApiStudium from '../../../api/factory/studium.js'; export default { - + name: "LvUebersicht", props:{ event:{ type:Object, @@ -63,7 +64,7 @@ export default { // check lv evaluierung info if (this.studium_studiensemester) { - this.$fhcApi.factory.studium.getLvEvaluierungInfo(this.studium_studiensemester, this.event.lehreinheit_id ?? this.event.lehrveranstaltung_id) + this.$api.call(ApiStudium.getLvEvaluierungInfo(this.studium_studiensemester, this.event.lehreinheit_id ?? this.event.lehrveranstaltung_id)) .then(data => data.data) .then(res => { this.lvEvaluierungMessage = res.message; @@ -72,7 +73,7 @@ export default { // check if the lv has lvplan entries for this studiensemester if (this.studiensemester && this.event) { - return this.$fhcApi.factory.studium.getLvPlanForStudiensemester(this.studiensemester, this.event.lehreinheit_id ?? this.event.lehrveranstaltung_id) + this.$api.call(ApiStudium.getLvPlanForStudiensemester(this.studiensemester, this.event.lehreinheit_id ?? this.event.lehrveranstaltung_id)) .then(data => data.data) .then(res => { if (Array.isArray(res) && res.length > 0) { diff --git a/public/js/components/Cis/Renderer/Lehreinheit/calendarEvent.js b/public/js/components/Cis/Renderer/Lehreinheit/calendarEvent.js index badbf537f..fa3ea6618 100644 --- a/public/js/components/Cis/Renderer/Lehreinheit/calendarEvent.js +++ b/public/js/components/Cis/Renderer/Lehreinheit/calendarEvent.js @@ -31,7 +31,7 @@ export default { this.event.lektor.slice(0, 3).map(lektor => lektor.kurzbz).join("\n") + "\n" + this.$p.t('lehre/weitereLektoren', [this.event.lektor.length - 3]) ].join(": ")); - } else {console.log(this.event.lektor); + } else { tooltipArray.push([ this.$p.t('lehre/lektor'), this.event.lektor.map(lektor => lektor.kurzbz).join("\n") diff --git a/public/js/components/Cis/Studium/Studium.js b/public/js/components/Cis/Studium/Studium.js index 1b7aff81b..99867e44b 100644 --- a/public/js/components/Cis/Studium/Studium.js +++ b/public/js/components/Cis/Studium/Studium.js @@ -1,7 +1,8 @@ import LvUebersicht from "../Mylv/LvUebersicht.js"; - +import ApiStudium from '../../../api/factory/studium.js'; export default { + name: "Studium", data(){ return { studienSemester :[], @@ -97,28 +98,28 @@ export default { return value; }, changeSelectedStudienSemester(studiensemester_kurzbz) { - this.$fhcApi.factory.studium.getAllStudienSemester(studiensemester_kurzbz, this.selectedStudiengang, this.selectedSemester, this.selectedStudienordnung) + this.$api.call(ApiStudium.getAllStudienSemester(studiensemester_kurzbz, this.selectedStudiengang, this.selectedSemester, this.selectedStudienordnung)) .then(data => data.data) .then(res => { this.extractPropertyValues(res); }) }, changeSelectedStudienGang(studiengang_kz) { - this.$fhcApi.factory.studium.getAllStudienSemester(this.selectedStudiensemester, studiengang_kz, this.selectedSemester, this.selectedStudienordnung) + this.$api.call(ApiStudium.getAllStudienSemester(this.selectedStudiensemester, studiengang_kz, this.selectedSemester, this.selectedStudienordnung)) .then(data => data.data) .then(res => { this.extractPropertyValues(res); }) }, changeSelectedSemester(semester) { - this.$fhcApi.factory.studium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, semester, this.selectedStudienordnung) + this.$api.call(ApiStudium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, semester, this.selectedStudienordnung)) .then(data => data.data) .then(res => { this.extractPropertyValues(res); }) }, changeSelectedStudienPlan(studienplan_id) { - this.$fhcApi.factory.studium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, this.selectedSemester, studienplan_id) + this.$api.call(ApiStudium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, this.selectedSemester, studienplan_id)) .then(data => data.data) .then(res => { this.extractPropertyValues(res); @@ -256,8 +257,8 @@ export default { const studienordnung = JSON.parse(this.getDataFromLocalStorage("studienordnung")) ?? undefined; // only fetch default data if no data is stored in the local storage - - this.$fhcApi.factory.studium.getAllStudienSemester(studiensemester, studiengang, semester, studienordnung) + + this.$api.call(ApiStudium.getAllStudienSemester(studiensemester, studiengang, semester, studienordnung)) .then(data => data.data) .then(res => { this.extractPropertyValues(res); diff --git a/public/js/plugins/FhcBase/BaseAlert.js b/public/js/plugins/FhcBase/BaseAlert.js new file mode 100644 index 000000000..026ff7692 --- /dev/null +++ b/public/js/plugins/FhcBase/BaseAlert.js @@ -0,0 +1,359 @@ +import PvConfig from "../../../../index.ci.php/public/js/components/primevue/config/config.esm.min.js"; +import PvToast from "../../../../index.ci.php/public/js/components/primevue/toast/toast.esm.min.js"; +import PvConfirm from "../../../../index.ci.php/public/js/components/primevue/confirmdialog/confirmdialog.esm.min.js"; +import PvConfirmationService from "../../../../index.ci.php/public/js/components/primevue/confirmationservice/confirmationservice.esm.min.js"; +import {CoreRESTClient} from "../../RESTClient.js"; + +export default { + init(app, $p) { + let resolveReady; + const readyPromise = new Promise(resolve => { resolveReady = resolve; }); + + const helperAppContainer = document.createElement('div'); + document.body.appendChild(helperAppContainer); + + const helperApp = Vue.createApp({ + name: "FhcAlertApp", + components: { PvToast, PvConfirm }, + methods: { + mailToUrl(slotProps) { + let mailTo = FHC_JS_DATA_STORAGE_OBJECT.systemerror_mailto; + let subject = 'Meldung%20Systemfehler'; + let body = ` + Danke, dass Sie uns den Fehler melden. %0D%0A %0D%0A + Bitte beschreiben Sie uns ausführlich das Problem.%0D%0A + Bsp: Ich habe X ausgewählt und Y angelegt. Beim Speichern erhielt ich die Fehlermeldung. [Optional: Ich habe den Browser Z verwendet.]%0D%0A + -----------------------------------------------------------------------------------------------------------------------------------%0D%0A + PROBLEM: ... %0D%0A %0D%0A %0D%0A + + -----------------------------------------------------------------------------------------------------------------------------------%0D%0A + Fehler URL: ` + FHC_JS_DATA_STORAGE_OBJECT.called_path + '/' + FHC_JS_DATA_STORAGE_OBJECT.called_method + `%0D%0A + Fehler Meldung: ` + slotProps.message.detail + `%0D%0A + -----------------------------------------------------------------------------------------------------------------------------------%0D%0A %0D%0A + Wir kümmern uns um eine rasche Behebung des Problems!` + + return "mailto:" + mailTo + "?subject=" + subject + "&body=" + body; + }, + openMessagecard(e) { + bootstrap.Collapse.getOrCreateInstance(e.target.getAttribute('href')).toggle(); + } + }, + computed: { + showmaillink() { return FHC_JS_DATA_STORAGE_OBJECT.systemerror_mailto !== ''; } + }, + template: /* html */` + + + + + + + ` + }); + + // Link the helper app to the main app's phrasen system + helperApp.config.globalProperties.$p = $p; + helperApp.use(PvConfig); + helperApp.use(PvConfirmationService); + + const helperAppInstance = helperApp.mount(helperAppContainer); + + const $fhcAlert = { + // Internal storage for cross-plugin dependencies + deps: { $p }, + ready: readyPromise, + // Method used by FhcBase to inject the API later + setDeps(deps) { + Object.assign(this.deps, deps); + // Alert is ready when both Phrases and API are present + if (this.deps.$p && this.deps.$api) resolveReady(); + }, + + alertSuccess(message) { + if (Array.isArray(message)) + return message.forEach(this.alertSuccess); + helperAppInstance.$refs.toast.add({ severity: 'success', summary: 'Info', detail: message, life: 1000}); + }, + alertInfo(message) { + if (Array.isArray(message)) + return message.forEach(this.alertInfo); + helperAppInstance.$refs.toast.add({ severity: 'info', summary: 'Info', detail: message, life: 3000 }); + }, + alertWarning(message) { + if (Array.isArray(message)) + return message.forEach(this.alertWarning); + helperAppInstance.$refs.toast.add({ severity: 'warn', summary: 'Achtung', detail: message}); + }, + alertError(message) { + if (Array.isArray(message)) + return message.forEach(this.alertError); + helperAppInstance.$refs.toast.add({ severity: 'error', summary: 'Achtung', detail: message }); + }, + async alertSystemError(message) { + //TODO(Manu) for translation of content of template: restructure in data + //and update definitions with translations + + await this.ready; + + if (Array.isArray(message)) + return message.forEach(this.alertSystemError); + helperAppInstance.$refs.alert.add({ + severity: 'error', + summary: Vue.computed(() => this.deps.$p.t('alert/systemerror')), + detail: message}); + }, + async confirmDelete() { + await this.ready; + + return new Promise((resolve, reject) => { + helperAppInstance.$confirm.require({ + group: 'fhcAlertConfirm', + header: Vue.computed(() => this.deps.$p.t('alert/attention')), + message: Vue.computed(() => this.deps.$p.t('alert/confirm_delete')), + acceptLabel: Vue.computed(() => this.deps.$p.t('ui/loeschen')), + acceptClass: 'p-button-danger', + rejectLabel: Vue.computed(() => this.deps.$p.t('ui/abbrechen')), + rejectClass: 'p-button-secondary', + accept() { + resolve(true); + }, + reject() { + resolve(false); + }, + }); + }); + }, + confirm(options) { + return new Promise((resolve, reject) => { + helperAppInstance.$confirm.require({ + group: options?.group ?? 'fhcAlertConfirm', + header: options?.header ?? Vue.computed(() => this.deps.$p.t('alert/attention')), + message: options?.message ?? '', + acceptLabel: options?.acceptLabel ?? 'Ok', + acceptClass: options?.acceptClass ?? 'btn btn-primary', + rejectLabel: options?.rejectLabel ?? Vue.computed(() => this.deps.$p.t('ui/abbrechen')), + rejectClass: options?.rejectClass ?? 'btn btn-outline-secondary', + accept() { + resolve(true); + }, + reject() { + resolve(false); + }, + }); + }); + }, + alertDefault(severity, title, message, sticky = false, html = false) { + let options = { severity: severity, summary: title, detail: message, html }; + + if (!sticky) + options.life = 3000; + + helperAppInstance.$refs.toast.add(options); + }, + alertMultiple(messageArray, severity = 'info', title = 'Info', sticky = false, html = false){ + // Check, if array has only string values + if (messageArray.every(message => typeof message === 'string')) { + messageArray.forEach(message => this.alertDefault(severity, title, message, sticky, html)); + return true; + } + return false; + }, + handleSystemError(error) { + // don't show an error message to the user if the error was an aborted request + if(error.hasOwnProperty('name') && error.name.toLowerCase() === "AbortError".toLowerCase()) + return; + + // Error is string + if (typeof error === 'string') + return this.alertSystemError(error); + + // Error is array of strings + if (Array.isArray(error) && error.every(err => typeof err === 'string')) + return error.every(this.alertSystemError); + + // Error has been handled already + if (error.hasOwnProperty('handled') && error.handled) + return; + + // Error is object + if (typeof error === 'object' && error !== null) { + let errMsg = ''; + + + if (error.hasOwnProperty('response') && error.response?.data?.retval) + errMsg += 'Error Message: ' + (error.response.data.retval.message || error.response.data.retval) + '\r\n'; + else if (error.hasOwnProperty('message')) + errMsg += 'Error Message: ' + error.message.toUpperCase() + '\r\n'; + + if (error.hasOwnProperty('config') && error.config.hasOwnProperty('url')) + errMsg += 'Error ConfigURL: ' + error.config.url + '\r\n'; + + if (error.hasOwnProperty('stack')) + errMsg += 'Error Stack: ' + error.stack + '\r\n'; + + // Fallback object error message + if (errMsg == '') + errMsg = 'Error Message: ' + JSON.stringify(error) + '\r\n'; + + errMsg += 'Error Controller Path: ' + FHC_JS_DATA_STORAGE_OBJECT.called_path + '/' + FHC_JS_DATA_STORAGE_OBJECT.called_method; + + return this.alertSystemError(errMsg); + } + + // Fallback + this.alertSystemError('alertSystemError throws Generic Error\r\nError Controller Path: ' + FHC_JS_DATA_STORAGE_OBJECT.called_path + '/' + FHC_JS_DATA_STORAGE_OBJECT.called_method); + }, + handleSystemMessage(message) { + // Message is string + if (typeof message === 'string') + return this.alertWarning(message); + + // Message is array of strings + if (Array.isArray(message)) { + // If Array has only Strings + if (message.every(msg => typeof msg === 'string')) + return message.every(this.alertWarning); + + // If Array has only Objects + if (message.every(msg => typeof msg === 'object') && msg !== null) { + return message.every(msg => { + if (msg.hasOwnProperty('data') && msg.data.hasOwnProperty('retval')) { + this.alertWarning(JSON.stringify(msg.data.retval)); + } else { + this.alertSystemError(JSON.stringify(msg)); + } + }); + } + } + + // Message is Object with data property + if (typeof message === 'object' && message !== null){ + if (message.hasOwnProperty('data') && message.data.hasOwnProperty('retval')) { + // NOTE(chris): changed: alertSystemError => alertWarning + this.alertWarning(JSON.stringify(message.data.retval)); + } else { + this.alertSystemError(JSON.stringify(message)); + } + return; + } + + // Fallback + this.alertSystemError('alertSystemError throws Generic Error\r\nError Controller Path: ' + FHC_JS_DATA_STORAGE_OBJECT.called_path + '/' + FHC_JS_DATA_STORAGE_OBJECT.called_method); + }, + resetFormValidation(form) { + const event = new Event('fhc-form-reset'); + form.querySelectorAll(['[data-fhc-form-validate],[data-fhc-form-error]']).forEach(el => el.dispatchEvent(event)); + /*const alert = form.querySelector('div.alert.alert-danger[role="alert"]'); + if (alert) { + alert.innerHTML = ''; + alert.classList.add('d-none'); + } + form.querySelectorAll('.invalid-feedback').forEach(n => n.remove()); + form.querySelectorAll('.is-invalid').forEach(n => n.classList.remove('is-invalid')); + form.querySelectorAll('.is-valid').forEach(n => n.classList.remove('is-valid'));*/ + }, + handleFormValidation(error, form) { + if (form === undefined) { + if (error && error.nodeType === Node.ELEMENT_NODE) + return err => this.handleFormValidation(err, error); + } else { + if (error?.response?.status == 400) { + let errors = CoreRESTClient.getError(error.response.data); + if (typeof errors !== "object") + errors = error.response.data; + + // NOTE(chris): reset form validation + this.resetFormValidation(form); + + // NOTE(chris): set form input validation + const notFound = Object.entries(errors).filter(([key, detail]) => { + const input = form.querySelector('[data-fhc-form-validate="' + key + '"]'); + if (!input) + return true; + + input.dispatchEvent(new CustomEvent('fhc-form-invalidate', {detail})); + + /*const input = form.querySelector('[name="' + key + '"]'); + if (!input) + return true; + input.classList.add('is-invalid'); + const feedback = document.createElement('div'); + feedback.classList.add('invalid-feedback'); + feedback.innerHTML = detail; + input.after(feedback);*/ + return false; + }).map(arr => arr[1]); + + + //const alert = form.querySelector('div.alert.alert-danger[role="alert"]'); + const alert = form.querySelector('[data-fhc-form-error]'); + if (alert && notFound.length) { + alert.dispatchEvent(new CustomEvent('fhc-form-error', {detail: notFound})); + /*notFound.forEach(txt => { + const p = document.createElement('p'); + p.innerHTML = txt; + alert.append(p); + }); + + if (notFound.length) { + alert.lastChild.classList.add('mb-0'); + alert.classList.remove('d-none'); + }*/ + } else { + notFound.forEach(this.alertError); + } + return; + } + } + + if (error?.response?.status == 400) { + let errors = CoreRESTClient.getError(error.response.data); + this.alertError((typeof errors === 'object') ? Object.values(errors) : errors); + } else { + this.handleSystemError(error); + } + } + }; + + return $fhcAlert; + } +}; \ No newline at end of file diff --git a/public/js/plugins/FhcBase/BaseApi.js b/public/js/plugins/FhcBase/BaseApi.js new file mode 100644 index 000000000..d15103753 --- /dev/null +++ b/public/js/plugins/FhcBase/BaseApi.js @@ -0,0 +1,589 @@ +/** + * BaseApi.js - Contains the original api plugin methods call, get, post, etc. + * Dependencies like phrasen($p) or alert($fhcAlert) are optional and can be injected at initialization + * or at a later stage. + */ +export class BaseApi { + constructor(deps = {}) { + // optional dependencies like { $fhcAlert: ..., $p: ... } + this.deps = deps; + + this.resolveReady = null; + this.ready = new Promise(resolve => { this.resolveReady = resolve; }); + + // If deps were passed in constructor, resolve immediately + if (this.deps.$fhcAlert && this.deps.$p) this.resolveReady(); + + this.setupDefaultConfig() + + this.axiosInstance = axios.create({ + timeout: 500000, + baseURL: FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/" + }); + + this.setupInterceptors(); + } + + // === public functions START === + + // in case of api instantiation before vue app mount and later usage via fhcBase plugin + setDependencies(deps) { + Object.assign(this.deps, deps); + if (this.deps.$fhcAlert && this.deps.$p) this.resolveReady(); + } + + getUri(url) { + return this.axiosInstance.getUri({ url }); + } + + get(form, uri, params, config) { + [uri, params, config] = this.get_config(form, uri, params, config); + if (params) { + config = config ? { ...config, params } : { params }; + } + return this.axiosInstance.get(uri, config); + } + + post(form, uri, data, config) { + [uri, data, config] = this.get_config(form, uri, data, config); + return this.axiosInstance.post(uri, data, config); + } + + getErrorHandler(config) { + return this.get_error_handler(config); + } + + async call(factory, configoverwrite, form) { + if (Array.isArray(factory)) { + return Promise.allSettled(factory.map((config, index) => { + if (!Array.isArray(config)) config = ['#' + index, config]; + return this.call(config[1], { errorHeader: config[0], errorHandling: false }); + })).then(result => { + const [ , , config ] = this.get_config(form, undefined, undefined, configoverwrite || {}); + const errorConfig = this.get_error_handler(config); + + if (!errorConfig.success && !errorConfig.fail) { + return result; + } + + const typedErrors = {}; + for (let res of result) { + const [ allowed, item ] = res.status === 'fulfilled' + ? [ errorConfig.success, res.value ] + : [ errorConfig.fail, res.reason ]; + if (!allowed) + return; + + const errors = this.popHandleableErrors(errorConfig, this.get_error_list(item)); + + for (let type in errors) { + if (!typedErrors[type]) + typedErrors[type] = { + [item.config.errorHeader]: errors[type] + }; + else + typedErrors[type][item.config.errorHeader] = errors[type]; + } + }; + + for (let errType in typedErrors) { + errorConfig.handler[errType](typedErrors[errType]); + } + + return result; + }); + } + + let { method = 'get', url, params, config } = factory; + if (configoverwrite !== undefined) config = configoverwrite; + method = method.toLowerCase(); + + return method === 'post' ? this.post(form, url, params, config) : this.get(form, url, params, config); + } + + // === public functions END === + + // === private functions START === + + setupInterceptors() { + + this.axiosInstance.interceptors.request.use(config => { + if (config.method != 'post' || !config.data) + return config; + + if (config.data instanceof FormData) + return config; + + if (!Object.values(config.data).every(item => { + if (item instanceof FileList) + return false; + if (Array.isArray(item)) + return item.every(i => !(i instanceof File)); + return true; + })) { + const newData = Object.entries(config.data).reduce((nd, [key, item]) => { + if (item instanceof FileList) { + for (const file of item) + nd.FormData.append(key + (item.length > 1 ? '[]' : ''), file); + } else if (Array.isArray(item)) { + if (item.every(i => !(i instanceof File))) { + nd.jsondata[key] = item; + } else { + item.forEach(file => nd.FormData.append(key + (item.length > 1 ? '[]' : ''), file)); + } + } else { + nd.jsondata[key] = item; + } + return nd; + }, { + FormData: new FormData(), + jsondata: {} + }); + newData.FormData.append('_jsondata', JSON.stringify(newData.jsondata)); + config.data = newData.FormData; + } + + return config; + }); + + this.axiosInstance.interceptors.response.use( + response => { + if (response.config?.errorHandling == 'off' + || response.config?.errorHandling === false + || response.config?.errorHandling == 'fail') + return this.clean_return_value(response); + + // NOTE(chris): loop through errors + if (response.data.errors) + response.data.errors = response.data.errors.filter( + err => (response.config[err.type + 'ErrorHandler'] || this.DEFAULT_ERROR_CONFIG.handler[err.type])(err, response.config) + ); + + return this.clean_return_value(response); + }, + error => { + if (error.code == 'ERR_CANCELED') + return Promise.reject({ handled: true, ...error }); + + const errorConfig = this.get_error_handler(error.config); + + if (!errorConfig.fail) + return Promise.reject(error); + + const remaining = this.get_error_list(error); + + const errors = this.popHandleableErrors(errorConfig, remaining); + + for (let type in errors) { + errorConfig.handler[type](errors[type]); + } + + if (remaining.length) + return Promise.reject(error); + + return Promise.reject({ handled: true, ...error }); + } + ); + } + + setupDefaultConfig() { + + this.DEFAULT_ERROR_CONFIG = { + success: true, + fail: true, + combine: { + form: ['validation', 'general'], + toast: ['validation', 'general', 'not_found', 'site_failed'] + }, + handler: { + form: (form, errors) => { + form.clearValidation(); + errors.forEach(err => form.setFeedback( + false, + err.messages || err.message + )); + }, + toast: async (errors) => { + await this.ready; + + async function _format_toast(errors) { + errors = errors.reduce((result, err) => { + switch (err.type) { + case 'not_found': + case 'site_failed': + if (err.message) + result[err.message] = [err.url]; + else + result._default = [err.url]; + break; + case 'general': + if (!result._default) + result._default = []; + result._default.push(err.message); + break; + case 'validation': + Object.entries(err.messages) + .forEach(([field, msg]) => { + if (!result[field]) + result[field] = []; + if (Array.isArray(msg)) + result[field].push(...msg); + else + result[field].push(msg); + }); + break; + } + return result; + }, {}); + let counter = 0; + const msgs = await Promise.all(Object.entries(errors) + .sort((a, b) => ['_default'].indexOf(b[0]) - ['_default'].indexOf(a[0])) // sort _default first + .map(async ([field, msgs]) => { + if (field == '_default') { + await this.deps.$p.loadCategory('dashboard'); + const general = this.deps.$p.t('dashboard/general'); + field = '
' + general + '
'; + } else { + field = '
' + field + '
'; + } + counter += msgs.length; + return field + + '
' + + msgs.join('
') + + '
'; + })); + return { + counter, + msgs + } + } + + let counter, msgs; + if (Array.isArray(errors)) { + ({ counter, msgs } = await _format_toast(errors)); + } else { + ({ counter, msgs } = await Object.entries(errors) + .reduce(async (res, [title, errs]) => { + const result = await res; + const { counter, msgs } = await _format_toast(errs); + result.counter += counter; + result.msgs.push('
' + + title + + '
' + + msgs.join('') + + '
'); + return result; + }, Promise.resolve({ counter: 0, msgs: []}))); + } + + await this.deps.$p.loadCategory('ui'); + const n_errors = this.deps.$p.t('ui/n_errors', { n: counter }); + + this.deps.$fhcAlert.alertDefault( + 'error', + n_errors, + '
' + msgs.join('') + '
', + true, + true + ); + }, + php: async (errors) => { + await this.ready; + + this._send_array_or_object(errors, (error, title) => { + var message = ''; + message += 'Message: ' + error.message + '\n\n'; + message += 'Filename: ' + error.filename + '\n'; + message += 'Line Number: ' + error.line + '\n'; + if (error.backtrace && error.backtrace.length) { + message += '\nBacktrace: '; + error.backtrace.forEach(err => { + message += '\n\tFile: ' + err.file + '\n'; + message += '\tLine: ' + err.line + '\n'; + message += '\tFunction: ' + err.function + '\n'; + }); + } + switch (error.severity) { + case 'Warning': + case 'Core Warning': + case 'Compile Warning': + case 'User Warning': + if (title) + title += ': PHP ' + error.severity; + else + title = 'PHP ' + error.severity; + this.deps.$fhcAlert.alertDefault('warn', title, message, true); + break; + case 'Notice': + case 'User Notice': + case 'Runtime Notice': + if (title) + title += ': PHP ' + error.severity; + else + title = 'PHP ' + error.severity; + this.deps.$fhcAlert.alertDefault('info', title, message, true); + break; + default: + message = 'Type: PHP ' + error.severity + '\n\n' + message; + if (title) + message = title + '\n\n' + message; + this.deps.$fhcAlert.alertSystemError(message); + break; + } + }); + }, + exception: async (errors) => { + await this.ready; + + this._send_array_or_object(errors, (error, title) => { + var message = ''; + if (title) + message += title + '\n\n'; + message += 'Type: ' + error.class + '\n\n'; + message += 'Message: ' + error.message + '\n\n'; + message += 'Filename: ' + error.filename + '\n'; + message += 'Line Number: ' + error.line + '\n'; + if (error.backtrace && error.backtrace.length) { + message += '\nBacktrace: '; + error.backtrace.forEach(err => { + message += '\n\tFile: ' + err.file + '\n'; + message += '\tLine: ' + err.line + '\n'; + message += '\tFunction: ' + err.function + '\n'; + }); + } + this.deps.$fhcAlert.alertSystemError(message); + }); + }, + db: async (errors) => { + await this.ready; + + this._send_array_or_object(errors, (error, title) => { + var message = ''; + if (title) + message += title + '\n\n'; + if (error.heading !== undefined) + message += error.heading + '\n\n'; + if (error.code !== undefined) + message += 'Code: ' + error.code + '\n\n'; + if (error.sql !== undefined) + message += 'SQL: ' + error.sql + '\n\n'; + if (error.message !== undefined) + message += 'Message: ' + error.message + '\n\n'; + else if (error.messages !== undefined) + message += 'Messages: ' + error.messages.join('\n\t') + '\n\n'; + if (error.filename !== undefined) + message += 'Filename: ' + error.filename + '\n'; + if (error.line !== undefined) + message += 'Line Number: ' + error.line + '\n'; + + this.deps.$fhcAlert.alertSystemError(message); + }); + }, + auth: async (errors) => { + await this.ready; + + this._send_array_or_object(errors, (error, title) => { + if (title) + title += ': ' + error.message; + else + title = error.message; + + var message = ''; + message += 'Controller name: ' + error.controller + '\n'; + message += 'Method name: ' + error.method + '\n'; + message += 'Required permissions: ' + error.required_permissions; + + this.deps.$fhcAlert.alertDefault( + 'error', + title, + message, + true + ); + }); + } + } + }; + } + + + _send_array_or_object(errors, func) { + if (!errors) return; + + if (Array.isArray(errors)) { + errors.forEach(error => func(error)); + return; + } + + // Handle Single Error Object + if (errors.type) { + func(errors); + return; + } + + // Handle Category Container + Object.entries(errors).forEach(([title, value]) => { + const errorList = Array.isArray(value) ? value : [value]; + + errorList.forEach(error => { + if (error && typeof error === 'object') { + func(error, title); + } + }); + }); + } + + get_config(form, uri, data, config) { + if (typeof form == 'string' && config === undefined) { + [uri, data, config] = [form, uri, data]; + form = undefined; + } else if (form) { + if (typeof form != 'object') + throw new TypeError('Parameter 1 of _get_config must be an object or a string'); + if (uri === undefined && data === undefined && config === undefined) { + config = form; + form = undefined; + } + } + if (form) { + // NOTE(chris): check if form is fhc-form + if (!form.clearValidation || !form.setFeedback) + throw new TypeError("'form' is not a Form Component"); + + form = { + clearValidation: form.clearValidation, + setFeedback: form.setFeedback + }; + + if (config) + config.form = form; + else + config = {form}; + } + + return [uri, data, config]; + } + + clean_return_value(response) { + if (typeof response.data === 'string' || response.data instanceof String) + return this.clean_return_value({ data: response }); + + const result = response.data; + delete response.data; + if (!result) + return {meta: {response}, data: null}; + if (!result.meta) + result.meta = { response }; + else + result.meta.response = response; + return result; + } + + merge_error_config(config) { + if (config === false || config === 'off') + return { ...this.DEFAULT_ERROR_CONFIG, success: false, fail: false }; + + if (!config || config === true) + return { ...this.DEFAULT_ERROR_CONFIG }; + + if (config === 'success') + return { ...this.DEFAULT_ERROR_CONFIG, fail: false }; + + if (config === 'fail') + return { ...this.DEFAULT_ERROR_CONFIG, success: false }; + + const { success, fail, handler, combine } = config; + + config = { ...this.DEFAULT_ERROR_CONFIG }; + + Object.entries({ fail, success }).forEach(([key, value]) => { + if (value !== undefined) + config[key] = value; + }); + Object.entries({ handler, combine }).forEach(([key, value]) => { + if (value !== undefined) + config[key] = { ...config[key], ...value }; + }); + + return config; + } + + get_error_handler(config) { + const result = this.merge_error_config(config?.errorHandling); + + if (!config?.form) { + result.combine = { ...result.combine, form: [] }; + } else { + const formHandler = result.handler.form; + result.handler = { ...result.handler, form: errors => formHandler(config.form, errors) }; + } + + return result; + } + get_error_list(error) { + if (error.response) { + if (error.response.status == 404) { + return [{ + type: 'not_found', + message: error.message, + url: error.request.responseURL + }]; + } else { + if (error.response.data.errors == undefined) return []; + return error.response.data.errors; + } + } else if (error.request) { + return [{ + type: 'site_failed', + message: error.message, + url: error.request.responseURL + }]; + } else { + return [{ + type: 'script', + message: error.message + }]; + } + } + + popHandleableErrors(errorHandling, errors) { + const result = {}; + const copy = []; + + if (errors == undefined) return {}; + + while (errors.length) + copy.push(errors.pop()); + for (let error of copy) { + let type = error.type; + let newType = null; + for (let t in errorHandling.combine) { + let newTypeCombinesType = errorHandling + .combine[t] + .includes(type); + let newTypeHasHandler = errorHandling.handler[t]; + if (newTypeCombinesType && newTypeHasHandler) { + newType = t; + if (newType == 'form') + break; + } + } + if (newType) + type = newType; + const handler = errorHandling.handler[type]; + if (handler) { + if (!result[type]) + result[type] = []; + if (Array.isArray(error)) + result[type].push(...error); + else + result[type].push(error); + continue; + } + errors.push(error); + } + return result; + } + + // === private functions END === + +} \ No newline at end of file diff --git a/public/js/plugins/FhcBase/BasePhrasen.js b/public/js/plugins/FhcBase/BasePhrasen.js new file mode 100644 index 000000000..3cf8247de --- /dev/null +++ b/public/js/plugins/FhcBase/BasePhrasen.js @@ -0,0 +1,100 @@ +import ApiPhrasen from '../../api/factory/phrasen.js'; + +const categories = Vue.reactive({}); +const loadingModules = {}; +let user_language = Vue.ref(FHC_JS_DATA_STORAGE_OBJECT.user_language); + +export const user_locale = Vue.computed(() => { + if (!user_language.value) return null; + return FHC_JS_DATA_STORAGE_OBJECT.server_languages.find(l => l.sprache == user_language.value).LC_Time; +}); + +function extractCategory(obj, category) { + return obj.filter(e => e.category == category).reduce((res, elem) => { + if (!res[elem.phrase]) res[elem.phrase] = elem.text; + return res; + }, {}); +} + +function getValueForLoadedPhrase(category, phrase, params) { + let result = categories[category][phrase]; + if (!result) + return '<< PHRASE ' + phrase + '>>'; + if (params) + result = result.replace(/\{([^}]*)\}/g, (match, p1) => params[p1] === undefined ? match : params[p1]); + return result; +} + +export default { + init(app) { + // Create a controller to resolve the promise later + let resolveReady; + const readyPromise = new Promise(resolve => { resolveReady = resolve; }); + + const $p = { + deps: {}, + ready: readyPromise, + setDeps(deps) { + Object.assign(this.deps, deps); + // Once we have the API, we are ready to load data + if (this.deps.$api) resolveReady(); + }, + user_language, + user_locale, + + async loadCategory(category) { + if (Array.isArray(category)) + return Promise.all(category.map(cat => this.loadCategory(cat))); + + // 2. SAFETY: Check if API is available via deps + await this.ready; + + if (!loadingModules[category]) + loadingModules[category] = this.deps.$api + .call(ApiPhrasen.loadCategory(category)) + .then(res => res?.data ? extractCategory(res.data, category) : {}) + .then(res => { + categories[category] = res; + }); + return loadingModules[category]; + }, + t_ref(category, phrase, params) { + console.warn('deprecated'); + return Vue.computed(() => this.t(category, phrase, params)); + }, + + t(category, phrase, params) { + if (params === undefined && ( + (Array.isArray(category) && category.length == 2) || + (category.split && category.split('/').length == 2)) + ) { + params = phrase; + [category, phrase] = category.split ? category.split('/') : category; + } + if (phrase === undefined) { + console.error('invalid input', category, phrase, params); + return ''; + } + let val = Vue.computed(() => { + if (!categories[category]) + return ''; + return getValueForLoadedPhrase(category, phrase, params); + }); + if (!categories[category]) + this.loadCategory(category); + return val.value; + }, + + async setLanguage(language) { + await this.ready; + const catArray = Object.keys(categories); + return this.deps.$api.call(ApiPhrasen.setLanguage(catArray, language)).then(res => { + res.data.forEach(row => { categories[row.category][row.phrase] = row.text; }); + user_language.value = language; + return res; + }); + } + }; + return $p; + } +}; \ No newline at end of file diff --git a/public/js/plugins/FhcBase/FhcBase.js b/public/js/plugins/FhcBase/FhcBase.js new file mode 100644 index 000000000..fa5b10e3f --- /dev/null +++ b/public/js/plugins/FhcBase/FhcBase.js @@ -0,0 +1,37 @@ +import Phrasen from './BasePhrasen.js'; +import Alert from './BaseAlert.js'; +import { BaseApi } from './BaseApi.js'; + +export default { + + install(app, options = {}) { + // init in order + const $p = Phrasen.init(app); + const $fhcAlert = Alert.init(app, $p); + + // try to reuse existing CoreRESTClient api instance if one has been active since before + // fhcBase Plugin install + let $api = app.config.globalProperties.$api; + if (!($api instanceof BaseApi)) { + $api = new BaseApi({ $fhcAlert, $p }, options); + } else { + // If api existed pre-app install + $api.setDependencies({ $fhcAlert, $p }); + } + + // set ready promise for awaiting async functions + $p.setDeps({ $api, $fhcAlert }); + $fhcAlert.setDeps({ $api }); + + // globalProperties Binding & provide + app.config.globalProperties.$p = $p; + app.config.globalProperties.$fhcAlert = $fhcAlert; + app.config.globalProperties.$api = $api; + app.config.globalProperties.$fhcApi = $api; + app.provide('$api', $api); + app.provide('$fhcApi', $api); + app.provide('$p', $p); + app.provide('$fhcAlert', $fhcAlert); + } + +}; \ No newline at end of file