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 */`
+
+
+
+
+
{{ message.summary }}
+
+
{{ message.detail }}
+
+
+
+
+
+
+
+
{{slotProps.message.summary}}
+
Sorry! Ein interner technischer Fehler ist aufgetreten.
+
+
+
+ {{slotProps.message.detail}}
+
+
+
+
+
+ `
+ });
+
+ // 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