diff --git a/application/controllers/api/frontend/v1/funktionen/FunctionsAPI.php b/application/controllers/api/frontend/v1/funktionen/FunctionsAPI.php new file mode 100644 index 000000000..023100a6e --- /dev/null +++ b/application/controllers/api/frontend/v1/funktionen/FunctionsAPI.php @@ -0,0 +1,239 @@ + FunctionsAPI::DEFAULT_PERMISSION, + 'getContractFunctions' => FunctionsAPI::DEFAULT_PERMISSION, + 'getCurrentFunctions' => FunctionsAPI::DEFAULT_PERMISSION, + 'getAllUserFunctions' => [FunctionsAPI::DEFAULT_PERMISSION, self::HANDYVERWALTUNG_PERMISSION], + ) + ); + $this->load->library('AuthLib'); + $this->load->model('extensions/FHC-Core-Personalverwaltung/Api_model','ApiModel'); + $this->load->model('ressource/Funktion_model', 'FunktionModel'); + $this->load->model('person/Benutzerfunktion_model', 'BenutzerfunktionModel'); + } + + + + /* + * return list of all functions + * as key value list to be used in select or autocomplete + */ + public function getAllFunctions() + { + $sql = <<FunktionModel->execReadOnlyQuery($sql); + if( hasData($fkts) ) + { + $this->outputJson($fkts); + return; + } + else + { + $this->outputJsonError('no contract relevant funktionen found'); + return; + } + } + + /* + * return list of contract relevant functions + * as key value list to be used in select or autocomplete + */ + public function getContractFunctions($mode='all') + { + $addwhere = ''; + switch ($mode) + { + case 'zuordnung': + $addwhere = ' AND funktion_kurzbz LIKE \'%zuordnung%\''; + break; + case 'funktion': + $addwhere = ' AND funktion_kurzbz NOT LIKE \'%zuordnung%\''; + break; + case 'all': + default: + $addwhere = ''; + break; + } + + $sql = <<FunktionModel->execReadOnlyQuery($sql); + if( hasData($fkts) ) + { + $this->outputJson($fkts); + return; + } + else + { + $this->outputJsonError('no contract relevant funktionen found'); + return; + } + } + + /* + * return list of child orgets for a given company orget_kurzbz + * as key value list to be used in select or autocomplete + */ + public function getCurrentFunctions($uid, $companyOrgetkurzbz) + { + if( empty($uid) ) + { + $this->outputJsonError('Missing Parameter '); + } + + if( empty($companyOrgetkurzbz) ) + { + $this->outputJsonError('Missing Parameter '); + } + + $sql = <<BenutzerfunktionModel->execReadOnlyQuery($sql, array($companyOrgetkurzbz, $uid)); + if( hasData($benutzerfunktionen) ) + { + $this->outputJson($benutzerfunktionen); + return; + } + else + { + $this->outputJsonError('no benutzerfunktionen found for uid ' . $uid . ' and oe_kurzbz ' . $companyOrgetkurzbz ); + return; + } + } + + /* + * return list of functions for a uid + * as objects to be used in as datasource + */ + public function getAllUserFunctions($uid) + { + if( empty($uid) ) + { + $this->outputJsonError('Missing Parameter '); + } + + $sql = <<BenutzerfunktionModel->execReadOnlyQuery($sql, array($uid)); + if( hasData($benutzerfunktionen) ) + { + $this->outputJson($benutzerfunktionen); + return; + } + else + { + $this->outputJsonError('no benutzerfunktionen found for uid ' . $uid); + return; + } + } + + + + + + +} \ No newline at end of file diff --git a/application/controllers/api/frontend/v1/funktionen/OrgAPI.php b/application/controllers/api/frontend/v1/funktionen/OrgAPI.php new file mode 100644 index 000000000..f6cf5bdb8 --- /dev/null +++ b/application/controllers/api/frontend/v1/funktionen/OrgAPI.php @@ -0,0 +1,153 @@ + OrgAPI::DEFAULT_PERMISSION, + 'getOrgStructure' => OrgAPI::DEFAULT_PERMISSION, + 'getOrgPersonen' => OrgAPI::DEFAULT_PERMISSION, + 'getCompanyByOrget' => [OrgAPI::DEFAULT_PERMISSION, self::HANDYVERWALTUNG_PERMISSION], + 'getOrgetsForCompany' => OrgAPI::DEFAULT_PERMISSION, + 'getUnternehmen' => [OrgAPI::DEFAULT_PERMISSION, self::HANDYVERWALTUNG_PERMISSION], + ) + ); + $this->load->library('AuthLib'); + $this->load->model('extensions/FHC-Core-Personalverwaltung/Organisationseinheit_model', 'OrganisationseinheitModel'); + $this->load->model('extensions/FHC-Core-Personalverwaltung/Api_model','ApiModel'); + } + + // ----------------------------- + // Organisation + // ----------------------------- + + function getOrgHeads() + { + $data = $this->OrganisationseinheitModel->getHeads(); + return $this->outputJson($data); + } + + function getOrgStructure() + { + $oe = $this->input->get('oe', TRUE); + + $data = $this->OrganisationseinheitModel->getOrgStructure($oe); + return $this->outputJson($data); + } + + function getOrgPersonen() + { + $oe = $this->input->get('oe', TRUE); + + $data = $this->OrganisationseinheitModel->getPersonen($oe); + return $this->outputJson($data); + } + + + + + public function getCompanyByOrget($oe_kurzbz) + { + + $sql = <<OrganisationseinheitModel->execReadOnlyQuery($sql, array($oe_kurzbz)); + if( hasData($childorgets) ) + { + $this->outputJson($childorgets); + return; + } + else + { + $this->outputJsonError('no orgets found for parent oe_kurzbz ' . $oe_kurzbz ); + return; + } + } + + /* + * return list of child orgets for a given company orget_kurzbz + * as key value list to be used in select or autocomplete + */ + public function getOrgetsForCompany($companyOrgetkurzbz=null) + { + if( empty($companyOrgetkurzbz) ) + { + $this->outputJsonError('Missing Parameter '); + return; + } + + $sql = <<OrganisationseinheitModel->execReadOnlyQuery($sql, array($companyOrgetkurzbz)); + if( hasData($childorgets) ) + { + $this->outputJson($childorgets); + return; + } + else + { + $this->outputJsonError('no orgets found for parent oe_kurzbz ' . $companyOrgetkurzbz ); + return; + } + } + + + public function getUnternehmen() + { + $this->OrganisationseinheitModel->resetQuery(); + $this->OrganisationseinheitModel->addSelect('oe_kurzbz AS value, bezeichnung AS label, \'false\'::boolean AS disabled'); + $this->OrganisationseinheitModel->addOrder('bezeichnung', 'ASC'); + $unternehmen = $this->OrganisationseinheitModel->loadWhere('oe_parent_kurzbz IS NULL'); + if( hasData($unternehmen) ) + { + $this->outputJson($unternehmen); + return; + } + else + { + $this->outputJsonError('no companies (orgets with parent NULL) found'); + return; + } + } + +} \ No newline at end of file diff --git a/application/controllers/api/frontend/v1/stv/Config.php b/application/controllers/api/frontend/v1/stv/Config.php index 42de1b02f..d37a6d6ec 100644 --- a/application/controllers/api/frontend/v1/stv/Config.php +++ b/application/controllers/api/frontend/v1/stv/Config.php @@ -128,6 +128,11 @@ class Config extends FHCAPI_Controller 'component' => './Stv/Studentenverwaltung/Details/Mobility.js' ]; + $result['functions'] = [ + 'title' => $this->p->t('stv', 'tab_functions'), + 'component' => './Stv/Studentenverwaltung/Details/Funktionen.js' + ]; + Events::trigger('stv_conf_student', function & () use (&$result) { return $result; }); diff --git a/application/views/Studentenverwaltung.php b/application/views/Studentenverwaltung.php index c10dc475a..398b921d2 100644 --- a/application/views/Studentenverwaltung.php +++ b/application/views/Studentenverwaltung.php @@ -15,11 +15,13 @@ 'notiz', ), 'customCSSs' => [ + #datepicker fuer component functions 'public/css/components/vue-datepicker.css', 'public/css/components/primevue.css', 'public/css/Studentenverwaltung.css' ], 'customJSs' => [ + 'vendor/vuejs/vuedatepicker_js/vue-datepicker.iife.js' #'vendor/npm-asset/primevue/tree/tree.min.js', #'vendor/npm-asset/primevue/toast/toast.min.js' ], diff --git a/public/js/api/factory/funktionen.js b/public/js/api/factory/funktionen.js new file mode 100644 index 000000000..c1f4efa29 --- /dev/null +++ b/public/js/api/factory/funktionen.js @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2025 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import person from "./funktionen/person.js"; + +export default { + person +}; \ No newline at end of file diff --git a/public/js/api/factory/funktionen/person.js b/public/js/api/factory/funktionen/person.js new file mode 100644 index 000000000..305d0bbeb --- /dev/null +++ b/public/js/api/factory/funktionen/person.js @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2025 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +export default { + getContractFunctions(filter) { + var url = 'api/frontend/v1/funktionen/FunctionsAPI/getContractFunctions'; + if( typeof filter !== 'undefined' && filter !== null ) { + url = url + '/' + filter; + } + return { + method: 'get', + url, + }; + + }, + getOrgetsForCompany(unternehmen) { + var url = 'api/frontend/v1/funktionen/OrgAPI/getOrgetsForCompany' + + '/' + unternehmen; + return { + method: 'get', + url, + }; + }, + getCompanyByOrget(orget) { + var url = 'api/frontend/v1/funktionen/OrgAPI/getCompanyByOrget' + + '/' + orget; + return { + method: 'get', + url, + }; + }, + getCurrentFunctions(mitarbeiter_uid, unternehmen) { + var url = 'api/frontend/v1/funktionen/FunctionsAPI/getCurrentFunctions' + + '/' + mitarbeiter_uid + '/' + unternehmen; + return { + method: 'get', + url, + }; + } , + getAllUserFunctions(mitarbeiter_uid) { + var url = 'api/frontend/v1/funktionen/FunctionsAPI/getAllUserFunctions' + + '/' + mitarbeiter_uid; + return { + method: 'get', + url, + }; + }, + getAllFunctions() { + var url = 'api/frontend/v1/funktionen/FunctionsAPI/getAllFunctions'; + return { + method: 'get', + url, + }; + } +}; \ No newline at end of file diff --git a/public/js/components/Funktionen/Funktionen.js b/public/js/components/Funktionen/Funktionen.js new file mode 100644 index 000000000..c54628b24 --- /dev/null +++ b/public/js/components/Funktionen/Funktionen.js @@ -0,0 +1,620 @@ +import { Modal } from './Modal.js'; +import { ModalDialog } from './ModalDialog.js'; +import { Toast } from './Toast.js'; +import {OrgChooser} from "./OrgChooser.js"; +import { usePhrasen } from '../../mixins/Phrasen.js'; + +import ApiFunktion from '../../api/factory/funktionen/person.js'; +import ApiPerson from "../../../extensions/FHC-Core-Personalverwaltung/js/api/factory/person"; + +export const Funktionen = { + name: 'FunctionComponent', + components: { + Modal, + ModalDialog, + Toast, + OrgChooser, + "datepicker": VueDatePicker + }, + props: { + modelValue: { type: Object, default: () => ({}), required: false}, + config: { type: Object, default: () => ({}), required: false}, + readonlyMode: { type: Boolean, required: false, default: false }, + personID: { type: Number, required: false }, + personUID: { type: String, required: false }, + writePermission: { type: Boolean, required: false }, + }, + emits: ['updateHeader'], + setup( props, { emit } ) { + + const $api = Vue.inject('$api'); + //const fhcAlert = Vue.inject('$fhcAlert'); + + + const readonly = Vue.ref(false); + + const { t } = usePhrasen(); + + //const { personID: currentPersonID , personUID: currentPersonUID } = Vue.toRefs(props); + const currentPersonID = Vue.computed(() => { return props.personID }); + const currentPersonUID = Vue.computed(() => { return props.personUID }); + + const dialogRef = Vue.ref(); + + const isFetching = Vue.ref(false); + + const theModel = Vue.computed({ + get: () => props.modelValue, + set: (value) => emit('update:modelValue', value), + }); + + const unternehmen = Vue.ref(); + const jobfunctionList = Vue.ref([]); + const jobfunctionDefList = Vue.ref([]); + const orgUnitList = Vue.ref([{value: '', label: '-'}]); + + const aktivChecked = Vue.ref(true); + + const table = Vue.ref(null); // reference to your table element + const tabulator = Vue.ref(null); // variable to hold your table + const tableData = Vue.reactive([]); // data for table to display + + + const full = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router; + const route = ( props.readonlyMode !== true ) ? VueRouter.useRoute() : null; + + const convertArrayToObject = (array, key) => { + const initialValue = {}; + return array.reduce((obj, item) => { + return { + ...obj, + [item[key]]: item, + }; + }, initialValue); + }; + + const fetchData = async () => { + if (currentPersonID.value==null && theModel.value.personID==null) { + jobfunctionList.value = []; + return; + } + isFetching.value = true + + // fetch data and map them for easier access + try { + const response = await $api.call( + ApiFunktion.getAllUserFunctions(theModel.value.personUID || currentPersonUID.value)); + if(response.error === 1) { + let rawList = []; + tableData.value = []; + jobfunctionList.value = convertArrayToObject(rawList, 'benutzerfunktion_id'); + } else { + let rawList = response.retval; + tableData.value = response.retval; + jobfunctionList.value = convertArrayToObject(rawList, 'benutzerfunktion_id'); + } + } catch (error) { + console.log(error) + } finally { + isFetching.value = false + } + + } + + const createShape = () => { + return { + benutzerfunktion_id: 0, + uid: theModel.value.personUID || currentPersonUID.value, + oe_kurzbz: "", + funktion_kurzbz: "", + datum_von: "", + datum_bis: "", + bezeichnung: "", + wochenstunden: 0, + } + } + + const currentValue = Vue.ref(createShape()); + const preservedValue = Vue.ref(createShape()); + + Vue.watch([currentPersonID, currentPersonUID], ([id,uid]) => { + fetchData(); + }); + + Vue.watch(unternehmen, (unternehmen_kurzbz) => { + fetchOrgUnits(unternehmen_kurzbz); + fetchFunctions(); + }); + + const toggleMode = async () => { + if (!readonly.value) { + // cancel changes? + if (hasChanged.value) { + const ok = await dialogRef.value.show(); + if (ok) { + console.log("ok=", ok); + currentValue.value = preservedValue.value; + } else { + return + } + } + } else { + // switch to edit mode and preserve data + preservedValue.value = {...currentValue.value}; + } + readonly.value = !readonly.value; + } + + Vue.onMounted(async () => { + currentValue.value = createShape(); + await fetchData(); + + const dateFormatter = (cell) => { + return cell.getValue()?.replace(/(.*)-(.*)-(.*)/, '$3.$2.$1'); + } + + const dvFormatter = (cell) => { + if( props.readonlyMode === true ) { + return (cell.getValue() != null) ? cell.getValue() : ''; + } + const url = fullPath + route.params.id + '/' + route.params.uid + '/contract/' + cell.getRow().getData().dienstverhaeltnis_id; + return cell.getValue() != null ? `` + cell.getValue() + '' : ''; + } + + // helper + const createDomButton = (classValue, clickHandler) => { + const nodeBtn = document.createElement("button"); + const classAttrBtn = document.createAttribute("class"); + classAttrBtn.value = "btn btn-outline-secondary btn-sm"; + nodeBtn.setAttributeNode(classAttrBtn); + nodeBtn.addEventListener("click", clickHandler); + const nodeI = document.createElement("i"); + const classAttrI = document.createAttribute("class"); + classAttrI.value = classValue; + nodeI.setAttributeNode(classAttrI); + nodeBtn.appendChild(nodeI); + return nodeBtn; + } + + const btnFormatter = (cell) => { + + const nodeDiv = document.createElement("div"); + const classAttrDiv = document.createAttribute("class"); + classAttrDiv.value = "d-grid gap-2 d-md-flex justify-content-end align-middle"; + nodeDiv.setAttributeNode(classAttrDiv); + // delete button + const nodeBtnDel = createDomButton("fa fa-xmark",() => { showDeleteModal(cell.getValue()) }) + // edit button + const nodeBtnEdit = createDomButton("fa fa-pen",() => { showEditModal(cell.getValue()) }) + + if( cell.getRow().getData().dienstverhaeltnis_unternehmen === null ) { + nodeDiv.appendChild(nodeBtnEdit); + nodeDiv.appendChild(nodeBtnDel); + } + + return nodeDiv; + } + + + const columnsDef = [ + { title: t('person','dv_unternehmen'), field: "dienstverhaeltnis_unternehmen", formatter: dvFormatter, sorter:"string", headerFilter:"list", headerFilterParams: {valuesLookup:true, autocomplete:true, sort:"asc"} }, + { title: t('person','zuordnung_taetigkeit'), field: "funktion_beschreibung", hozAlign: "left", headerFilter:"list", headerFilterParams: {valuesLookup:true, autocomplete:true, sort:"asc"} }, + { title: t('lehre','organisationseinheit'), field: "funktion_oebezeichnung", headerFilter:"list", headerFilterParams: {valuesLookup:true, autocomplete:true, sort:"asc"} }, + { title: t('person','wochenstunden'), field: "wochenstunden", hozAlign: "right", width: 140, headerFilter:true }, + { title: t('ui','from'), field: "datum_von", hozAlign: "center", formatter: dateFormatter, width: 140, sorter:"string", headerFilter:true, headerFilterFunc:customHeaderFilter }, + { title: t('global','bis'), field: "datum_bis", hozAlign: "center", formatter: dateFormatter, width: 140, sorter:"string", headerFilter:true, headerFilterFunc:customHeaderFilter }, + { title: t('ui','bezeichnung'), field: "bezeichnung", hozAlign: "left", headerFilter:"list", headerFilterParams: {valuesLookup:true, autocomplete:true, sort:"asc"} } + ]; + + if( props.readonlyMode === false) { + columnsDef.push({ title: "", field: "benutzerfunktion_id", formatter: btnFormatter, hozAlign: "right", width: 100, headerSort: false, frozen: true }); + } + + let tabulatorOptions = { + height: "100%", + layout: "fitColumns", + movableColumns: true, + reactiveData: true, + columns: columnsDef, + //data: tableData.value, + data: jobfunctionListArray.value, + }; + + tabulator.value = new Tabulator( + table.value, + tabulatorOptions + ); + + function customHeaderFilter(headerValue, rowValue, rowData, filterParams){ + //headerValue - the value of the header filter element + //rowValue - the value of the column in this row + //rowData - the data for the row being filtered + //filterParams - params object passed to the headerFilterFuncParams property + + const validDate = function(d){ + return d instanceof Date && isFinite(d); + } + + const date1 = new Date(rowValue); + date1.setHours(0,0,0,0); + let [day, month, year] = headerValue.split('.') + if (year < 1000) return true; // prevents dates like 17.5.2 + const date2 = new Date(+year, +month - 1, +day); + + return !(validDate(date2)) || ((date2 - date1) == 0); //must return a boolean, true if it passes the filter. + } + + tabulator.value.on('tableBuilt', () => { + //tabulator.value.setData(tableData.value); + + }) + + }) + + const jobfunctionListArray = Vue.computed(() => { + let temp = (jobfunctionList.value ? Object.values(jobfunctionList.value) : []); + let filtered = temp.filter((e) => ( !aktivChecked.value || (aktivChecked.value && e.aktiv) )); + return filtered; + }); + + // Workaround to update tabulator + Vue.watch(jobfunctionListArray, (newVal, oldVal) => { + console.log('jobfunctionList changed'); + tabulator.value?.setData(jobfunctionListArray.value); + }, {deep: true}) + + // Modal + const modalRef = Vue.ref(); + const confirmDeleteRef = Vue.ref(); + + const showAddModal = () => { + currentValue.value = createShape(); + // reset form state + frmState.orgetBlurred=false; + frmState.funktionBlurred=false; + frmState.beginnBlurred=false; + // call bootstrap show function + modalRef.value.show(); + } + + const hideModal = () => { + modalRef.value.hide(); + } + + const showEditModal = async (id) => { + currentValue.value = { ...jobfunctionList.value[id] }; + // fetch company + isFetching.value = true; + try { + const res = await $api.call(ApiFunktion.getCompanyByOrget(currentValue.value.oe_kurzbz)); + if (res.error == 0) { + unternehmen.value = res.retval[0].oe_kurzbz; + } else { + console.log('company not found for orget!'); + } + + } catch (error) { + console.log(error) + } finally { + isFetching.value = false + } + //delete currentValue.value.bezeichnung; + modalRef.value.show(); + } + + const showDeleteModal = async (id) => { + currentValue.value = { ...jobfunctionList.value[id] }; + const ok = await confirmDeleteRef.value.show(); + + if (ok) { + + try { + const res = await $api.call(ApiPerson.deletePersonJobFunction(id)); + if (res.error == 0) { + delete jobfunctionList.value[id]; + showDeletedToast(); + theModel.value.updateHeader(); + } + } catch (error) { + console.log(error) + } finally { + isFetching.value = false + } + + } + } + + + const okHandler = async () => { + if (!validate()) { + + console.log("form invalid"); + + } else { + + // submit + try { + let payload = {...currentValue.value}; + delete payload.dienstverhaeltnis_id; + delete payload.dienstverhaeltnis_unternehmen; + delete payload.fachbereich_bezeichnung; + delete payload.funktion_oebezeichnung; + delete payload.aktiv; + delete payload.funktion_beschreibung; + const r = await $api.call(ApiPerson.upsertPersonJobFunction(payload)); + if (r.error == 0) { + // fetch all data because of all the references in the changed record + await fetchData(); + console.log('job function successfully saved'); + theModel.value.updateHeader(); + showToast(); + } + } catch (error) { + console.log(error) + } finally { + isFetching.value = false + } + + hideModal(); + } + } + + // ------------- + // form handling + // ------------- + + const jobFunctionFrm = Vue.ref(); + + const frmState = Vue.reactive({ beginnBlurred: false, orgetBlurred: false, funktionBlurred: false, wasValidated: false }); + + const notEmpty = (n) => { + return !!n && n.trim() != ""; + } + + const validate = () => { + frmState.orgetBlurred = true; + frmState.funktionBlurred = true; + frmState.beginnBlurred = true; + return notEmpty(currentValue.value.datum_von) && + notEmpty(currentValue.value.oe_kurzbz) && + notEmpty(currentValue.value.funktion_kurzbz); + } + + + const hasChanged = Vue.computed(() => { + return Object.keys(currentValue.value).some(field => currentValue.value[field] !== preservedValue.value[field]) + }); + + const formatDate = (d) => { + if (d != null && d != '') { + return d.substring(8, 10) + "." + d.substring(5, 7) + "." + d.substring(0, 4); + } else { + return '' + } + } + + // Toast + const toastRef = Vue.ref(); + const deleteToastRef = Vue.ref(); + + const showToast = () => { + toastRef.value.show(); + } + + const showDeletedToast = () => { + deleteToastRef.value.show(); + } + + const fetchOrgUnits = async (unternehmen_kurzbz) => { + if( unternehmen_kurzbz === '' ) { + return; + } + const response = await $api.call( + ApiFunktion.getOrgetsForCompany(unternehmen_kurzbz)); + const orgets = response.retval; + orgets.unshift({ + value: '', + label: t('ui','bitteWaehlen'), + }); + orgUnitList.value = await orgets; + } + + const fetchFunctions = async (uid, unternehmen_kurzbz) => { + if(unternehmen_kurzbz === '' || uid === '' ) { + return; + } + const response = await $api.call(ApiFunktion.getAllFunctions()); + const benutzerfunktionen = response.retval; + benutzerfunktionen.unshift({ + value: '', + label: t('ui','bitteWaehlen'), + }); + jobfunctionDefList.value = benutzerfunktionen; + } + + const unternehmenSelectedHandler = (e) => { + console.log('unternehmen selected: ',e); + unternehmen.value = e; + } + + const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router; + const fullPath = `/${ciPath}/extensions/FHC-Core-Personalverwaltung/Employees/`; + + return { + jobfunctionList, orgUnitList, jobfunctionListArray, + jobfunctionDefList, + currentValue, + readonly, + frmState, + dialogRef, + toastRef, deleteToastRef, + jobFunctionFrm, + modalRef, + fullPath, + route, + aktivChecked, + unternehmen, + tabulator, + table, + + toggleMode, formatDate, notEmpty, + showToast, showDeletedToast, + showAddModal, hideModal, okHandler, + showDeleteModal, showEditModal, confirmDeleteRef, t, unternehmenSelectedHandler, + } + }, + template: ` +
+ +
+ + + +
+ +
+ + + +
+
+
+
+
+
+
{{ t('person','funktionen') }}
+
+ +
+
+ +
+ + +
+
+ + +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + ` +} + +export default Funktionen; \ No newline at end of file diff --git a/public/js/components/Funktionen/Modal.js b/public/js/components/Funktionen/Modal.js new file mode 100644 index 000000000..716d9ac2a --- /dev/null +++ b/public/js/components/Funktionen/Modal.js @@ -0,0 +1,48 @@ +export const Modal = { + name: 'Modal', + props: { + type: String, + title: String, + noscroll: Boolean + }, + expose: ['show', 'hide'], + setup(props, { emit }) { + + let modalEle = Vue.ref(null); + let thisModalObj; + + Vue.onMounted(() => { + thisModalObj = new bootstrap.Modal(modalEle.value); + }); + const show = () => { + thisModalObj.show(); + } + function hide() { + thisModalObj.hide(); + } + + return { modalEle, show, hide }; + }, + + template:` + ` +}; diff --git a/public/js/components/Funktionen/ModalDialog.js b/public/js/components/Funktionen/ModalDialog.js new file mode 100644 index 000000000..c46540cae --- /dev/null +++ b/public/js/components/Funktionen/ModalDialog.js @@ -0,0 +1,69 @@ +import { usePhrasen } from '../../mixins/Phrasen.js'; + +export const ModalDialog = { + name: 'ModalDialog', + props: { + type: String, + title: String, + }, + expose: ['show', 'hide'], + setup(props, { emit }) { + + let modalConfirmEle = Vue.ref(null); + let thisModalObj; + let _resolve; + let _reject; + const { t } = usePhrasen(); + + Vue.onMounted(() => { + thisModalObj = new bootstrap.Modal(modalConfirmEle.value); + }); + + const show = async () => { + thisModalObj.show(); + return new Promise(function (resolve, reject) { + _resolve = resolve; + _reject = reject; + }); + } + + function hide() { + thisModalObj.hide(); + } + + const ok = () => { + _resolve(true); + } + + const cancel = () => { + _resolve(false); + } + + return { modalConfirmEle, show, hide, ok, cancel, t }; + }, + + template:` + ` +}; diff --git a/public/js/components/Funktionen/OrgChooser.js b/public/js/components/Funktionen/OrgChooser.js new file mode 100644 index 000000000..9c3086e76 --- /dev/null +++ b/public/js/components/Funktionen/OrgChooser.js @@ -0,0 +1,69 @@ +import {CoreRESTClient} from '../../../js/RESTClient'; + +export const OrgChooser = { + name: 'OrgChooser', + props: { + placeholder: String, + customClass: String, + oe: String, + }, + emits: ["orgSelected"], + setup(props, { emit }) { + + const orgList = Vue.ref([]); + const isFetching = Vue.ref(false); + const oeRef = Vue.toRefs(props).oe + const selected = Vue.ref(); + + const fetchHead = async () => { + isFetching.value = true + try { + const res = await CoreRESTClient.get( + 'extensions/FHC-Core-Personalverwaltung/api/frontend/v1/OrgAPI/getOrgHeads'); + orgList.value = CoreRESTClient.getData(res.data); + if (orgList.value.length > 0) { + //orgList.value.reverse(); + if (props.oe == undefined || (props.oe != null && props.oe == '')) { + selected.value = orgList.value[0].oe_kurzbz; + } + emit("orgSelected", selected.value); + } + isFetching.value = false + } catch (error) { + console.log(error) + isFetching.value = false + } + } + + Vue.onMounted(() => { + fetchHead(); + }) + + const orgSelected = (e) => { + emit("orgSelected", e.target.value); + } + + Vue.watch( + oeRef, + (val, old) => { + console.log('prop value changed', val); + selected.value = val; + } + ) + + return { + orgList, selected, + + orgSelected + } + + + }, + template: ` + + ` +} diff --git a/public/js/components/Funktionen/Toast.js b/public/js/components/Funktionen/Toast.js new file mode 100644 index 000000000..6b3faba82 --- /dev/null +++ b/public/js/components/Funktionen/Toast.js @@ -0,0 +1,47 @@ +export const Toast = { + name: 'Toast', + props: { + title: { + text: String, + default: "<>", + }, + type: { + text: String, + default: "success", + } + }, + expose: ['show', 'hide'], + setup(props) { + + let toastEle = Vue.ref(null); + let thisToastObj; + + Vue.onMounted(() => { + thisToastObj = new bootstrap.Toast(toastEle.value); + }); + + const show = () => { + thisToastObj.show(); + } + + const hide = () => { + thisToastObj.hide(); + } + + const backgroundColor = Vue.computed(() => { + return props.type == "success" ? "bg-primary" : "bg-danger" + }) + + return { show, hide, toastEle, backgroundColor }; + + }, + template: ` + ` +} diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js b/public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js new file mode 100644 index 000000000..df73b6464 --- /dev/null +++ b/public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js @@ -0,0 +1,28 @@ +import PersonFunctions from "../../../Funktionen/Funktionen.js"; +/*import fhcapifactory from "../../../../apps/api/fhcapifactory.js"; +import pv21apifactory from "../../../../../extensions/FHC-Core-Personalverwaltung/js/api/api.js"; +Vue.$fhcapi = {...fhcapifactory, ...pv21apifactory};*/ + +export default { + components: { + PersonFunctions + }, + props: { + modelValue: Object, + }, + template: ` +
+ + + + + + +
` +}; diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 1afe4a196..6c52517b6 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -41453,8 +41453,31 @@ and represent the current state of research on the topic. The prescribed citatio 'insertvon' => 'system' ) ) - ) + ), // PROJEKTARBEITSBEURTEILUNG SS2025 ENDE --------------------------------------------------------------------------- + // FHC-4 Studierendenverwaltung FUNCTIONS START + array( + 'app' => 'core', + 'category' => 'stv', + 'phrase' => 'tab_functions', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Funktionen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Functions', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + // FHC-4 Studierendenverwaltung FUNCTIONS END + );