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