diff --git a/application/config/Events.php b/application/config/Events.php new file mode 100644 index 000000000..191a1eb98 --- /dev/null +++ b/application/config/Events.php @@ -0,0 +1,14 @@ + 'TEST', + 'component' => './Stv/Studentenverwaltung/Details/Notizen.js' + ]; + }); + */ diff --git a/application/controllers/public/js/Components.php b/application/controllers/public/js/Components.php new file mode 100644 index 000000000..830e7ea65 --- /dev/null +++ b/application/controllers/public/js/Components.php @@ -0,0 +1,43 @@ +output->set_content_type('text/javascript'); + $this->output->set_output($contents); + } +} diff --git a/application/core/CI3_Events.php b/application/core/CI3_Events.php new file mode 100644 index 000000000..33a96b89e --- /dev/null +++ b/application/core/CI3_Events.php @@ -0,0 +1,51 @@ + CoreRESTClient.getData(result.data)) + .then(result => { + const tabs = {}; + // TODO(chris): check if result is array + Object.entries(result).forEach(([key, config]) => { + if (!config.component) + return console.error('Component missing for ' + key); + + tabs[key] = { + component: Vue.markRaw(Vue.defineAsyncComponent(() => import(config.component))), + title: config.title || key, + config: config.config, + key + } + }); + if (tabs[this.default]) + this.current = this.default; + else + this.current = Object.keys(tabs)[0]; + this.tabs = tabs; + }) + .catch(this.$fhcAlert.handleSystemError); + }, + template: ` +
+ +
+ + + +
+
` +}; \ No newline at end of file diff --git a/public/js/directives/accessibility.js b/public/js/directives/accessibility.js new file mode 100644 index 000000000..77ca8f428 --- /dev/null +++ b/public/js/directives/accessibility.js @@ -0,0 +1,82 @@ +export default { + created(el, binding) { + switch (binding.arg) { + case 'tab': + el.style.cursor = 'pointer'; + const [prev, next] = binding.modifiers.vertical ? ['ArrowUp', 'ArrowDown'] : ['ArrowLeft', 'ArrowRight']; + el.addEventListener('click', () => { + let act = el.parentNode.querySelector('[tabindex="0"]'); + if (act) + act.tabIndex = -1; + el.tabIndex = 0; + }); + el.addEventListener('focus', () => { + el.ariaSelected = "true"; + }); + el.addEventListener('blur', () => { + el.ariaSelected = "false"; + }); + el.addEventListener('keydown', e => { + switch (e.code) { + case prev: + if (el.previousSibling?.tabIndex !== undefined) { + el.previousSibling.tabIndex = 0; + el.tabIndex = -1; + el.previousSibling.focus(); + } + break; + case next: + if (el.nextSibling?.tabIndex !== undefined) { + el.nextSibling.tabIndex = 0; + el.tabIndex = -1; + el.nextSibling.focus(); + } + break; + case 'Enter': + el.click(); + break; + } + }); + break; + } + }, + mounted(el, binding) { + switch (binding.arg) { + case 'tab': + let activetab = -1; + Array.from(el.parentNode.children).forEach((node, index) => { + node.ariaSetSize = el.parentNode.children.length; + node.ariaPosInSet = index; + if (node.tabIndex === 0) + activetab = index; + }); + if (activetab == -1) { + el.tabIndex = 0; + } else if (el.classList.contains('active')) { + el.parentNode.children[activetab].tabIndex = -1; + el.tabIndex = 0; + } else { + el.tabIndex = -1; + } + break; + } + }, + beforeUnmount(el, binding) { + switch (binding.arg) { + case 'tab': + if (el.tabIndex === 0) { + if (el.previousSibling?.tabIndex !== undefined) + el.previousSibling.tabIndex = 0; + else if (el.nextSibling?.tabIndex !== undefined) + el.nextSibling.tabIndex = 0; + } + const pos = parseInt(el.ariaPosInSet); + Array.from(el.parentNode.children).forEach((node, index) => { + node.ariaSetSize = el.parentNode.children.length; + if (index > pos) + node.ariaPosInSet = index-1; + }); + break; + } + } +} \ No newline at end of file diff --git a/public/js/plugin/FhcAlert.js b/public/js/plugin/FhcAlert.js new file mode 100644 index 000000000..079039aed --- /dev/null +++ b/public/js/plugin/FhcAlert.js @@ -0,0 +1,394 @@ +/** + * Copyright (C) 2022 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * @usage: + * Preperations: + * Be sure to have PrimeVue loaded with the toast and confirmdialog + * components as primevue variable + * Install: + * Import this Plugin and install it with the app.use() function. + * Use: + * In your component you can call now the global property $fhcAlert + * which has the following functions: + * + * alertSuccess + * ------------ + * Displays a success message + * @param string message + * @return void + * + * alertInfo + * --------- + * Displays an info message + * @param string message + * @return void + * + * alertWarning + * ------------ + * Displays a warning + * @param string message + * @return void + * + * alertError + * ---------- + * Displays an error + * @param string message + * @return void + * + * alertSystemError + * ---------------- + * Displays an alert with the error details and a button to mail + * the error to the Support Team + * @param string message + * @return void + * + * confirmDelete + * ------------- + * Displays a confirmation dialog and returns a Promise which resolves + * with true or false depending und the pressed button. + * @return Promise + * + * alertDefault + * ------------ + * Displays an alert + * @param string severity can be 'success', 'info', 'warning', 'error' + * @param string title + * @param string message + * @param boolean sticky (optional) defaults to false + * @return void + * + * alertMultiple + * ------------- + * Displays multiple alerts + * @param array messageArray + * @param string severity (optional) defaults to 'info' + * @param string title (optional) defaults to 'Info' + * @param boolean sticky (optional) defaults to false + * @return void + * + * handleSystemError + * ----------------- + * Automatiticly determine how to display an system error and display it. + * This would be used in a catch block of an ajax call. + * @param mixed error + * @return void + * + * handleSystemMessage + * ------------------- + * Automatiticly determine how to display a message and display it. + * @param mixed message + * @return void + */ +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'; + +const helperAppContainer = document.createElement('div'); + +const helperApp = Vue.createApp({ + components: { + PvToast, + PvConfirm + }, + methods: { + mailToUrl(slotProps) { + let mailTo = 'noreply@technikum-wien.at'; // TODO domain anpassen + 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(); + } + }, + unmounted() { + helperAppContainer.parentElement.removeChild(helperAppContainer); + }, + template: ` + + + + + ` +}); + +helperApp.use(PvConfig); +helperApp.use(PvConfirmationService); + +const helperAppInstance = helperApp.mount(helperAppContainer); + +document.body.appendChild(helperAppContainer); + + +export default { + install: (app, options) => { + const $fhcAlert = { + 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 }); + }, + alertSystemError(message) { + if (Array.isArray(message)) + return message.forEach(this.alertSystemError); + helperAppInstance.$refs.alert.add({ severity: 'error', summary: 'Systemfehler', detail: message}); + }, + confirmDelete() { + return new Promise((resolve, reject) => { + helperAppInstance.$confirm.require({ + group: 'fhcAlertConfirm', + header: 'Achtung', + message: 'Möchten Sie sicher löschen?', + acceptLabel: 'Löschen', + acceptClass: 'btn btn-danger', + rejectLabel: 'Abbrechen', + rejectClass: 'btn btn-outline-secondary', + accept() { + resolve(true); + }, + reject() { + resolve(false); + }, + }); + }); + }, + alertDefault(severity, title, message, sticky = false) { + let options = { severity: severity, summary: title, detail: message}; + + if (!sticky) + options.life = 3000; + + helperAppInstance.$refs.toast.add(options); + }, + alertMultiple(messageArray, severity = 'info', title = 'Info', sticky = false){ + // Check, if array has only string values + if (messageArray.every(message => typeof message === 'string')) { + messageArray.every(message => this.alertDefault(severity, title, message, sticky)); + return true; + } + return false; + }, + handleSystemError(error) { + // Error is string + if (typeof error === 'string') + return $fhcAlert.alertSystemError(error); + + // Error is array of strings + if (Array.isArray(error) && error.every(err => typeof err === 'string')) + return error.every($fhcAlert.alertSystemError); + + // 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 $fhcAlert.alertSystemError(errMsg); + } + + // Fallback + $fhcAlert.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 fhcerror.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(fhcerror.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')) { + fhcerror.alertWarning(JSON.stringify(msg.data.retval)); + } else { + fhcerror.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 + fhcerror.alertWarning(JSON.stringify(message.data.retval)); + } else { + fhcerror.alertSystemError(JSON.stringify(message)); + } + return; + } + + // Fallback + fhcerror.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 => $fhcAlert.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 + $fhcAlert.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($fhcAlert.alertError); + } + return; + } + } + + if (error?.response?.status == 400) { + let errors = CoreRESTClient.getError(error.response.data); + $fhcAlert.alertError((typeof errors === 'object') ? Object.values(errors) : errors); + } else { + $fhcAlert.handleSystemError(error); + } + } + }; + app.config.globalProperties.$fhcAlert = $fhcAlert; + } +} \ No newline at end of file diff --git a/system/dbupdate_3.4/28260_vertraege.php b/system/dbupdate_3.4/28260_vertraege.php index 81c96c6b8..78790ec91 100644 --- a/system/dbupdate_3.4/28260_vertraege.php +++ b/system/dbupdate_3.4/28260_vertraege.php @@ -374,6 +374,7 @@ if ($result = $db->db_query("SELECT * FROM information_schema.tables WHERE table INSERT INTO hr.tbl_teilzeittyp(teilzeittyp_kurzbz, bezeichnung) VALUES('altersteilzeit','Altersteilzeit'); INSERT INTO hr.tbl_teilzeittyp(teilzeittyp_kurzbz, bezeichnung) VALUES('elternteilzeit','Elternteilzeit'); + INSERT INTO hr.tbl_teilzeittyp(teilzeittyp_kurzbz, bezeichnung) VALUES('wiedereingliederungteilzeit','Wiedereingliederungsteilzeit'); INSERT INTO hr.tbl_vertragsart(vertragsart_kurzbz, bezeichnung, anmerkung, dienstverhaeltnis, vertragsart_kurzbz_parent, aktiv, sort) VALUES('echterdv','Echter DV','Echter Dienstvertrag', true, null, true, 100); INSERT INTO hr.tbl_vertragsart(vertragsart_kurzbz, bezeichnung, anmerkung, dienstverhaeltnis, vertragsart_kurzbz_parent, aktiv, sort) VALUES('studentischehilfskr','Studentische Hilfskraft','Studentische Hilfskraft', true, 'echterdv', true, 101); @@ -426,6 +427,7 @@ if ($result = $db->db_query("SELECT * FROM information_schema.tables WHERE table GRANT USAGE ON hr.tbl_dienstverhaeltnis_dienstverhaeltnis_id_seq TO vilesci; GRANT USAGE ON hr.tbl_vertragsbestandteil_vertragsbestandteil_id_seq TO vilesci; GRANT USAGE ON hr.tbl_gehaltshistorie_gehaltshistorie_id_seq TO vilesci; + GRANT USAGE ON hr.tbl_sachaufwand_sachaufwand_id_seq TO vilesci; GRANT USAGE ON hr.tbl_gehaltsbestandteil_gehaltsbestandteil_id_seq TO vilesci; "; diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 26a253d9b..8eb10cf0e 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -23150,6 +23150,106 @@ array( ) ) ), + array( + 'app' => 'personalverwaltung', + 'category' => 'person', + 'phrase' => 'abwesenheiten', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Abwesenheiten', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'off time', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'personalverwaltung', + 'category' => 'person', + 'phrase' => 'zeiterfassung', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Zeiterfassung', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'time recording', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'personalverwaltung', + 'category' => 'person', + 'phrase' => 'vertretung', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Vertretung', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'stand-in', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'personalverwaltung', + 'category' => 'person', + 'phrase' => 'erreichbarkeit', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Erreichbarkeit', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'off time', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'personalverwaltung', + 'category' => 'person', + 'phrase' => 'grund', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Grund', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'reason', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), // Personalverwaltung end );