From 233e768aad1b322fc2df54d0e8a0d58259f27a2f Mon Sep 17 00:00:00 2001 From: ma0068 Date: Fri, 9 May 2025 09:11:33 +0200 Subject: [PATCH 1/6] copy Function files to component --- .../frontend/v1/funktionen/FunctionsAPI.php | 239 +++++++ .../api/frontend/v1/funktionen/OrgAPI.php | 153 +++++ .../api/frontend/v1/stv/Config.php | 5 + application/views/Studentenverwaltung.php | 2 + public/js/api/factory/funktionen.js | 22 + public/js/api/factory/funktionen/person.js | 68 ++ public/js/components/Funktionen/Funktionen.js | 620 ++++++++++++++++++ public/js/components/Funktionen/Modal.js | 48 ++ .../js/components/Funktionen/ModalDialog.js | 69 ++ public/js/components/Funktionen/OrgChooser.js | 69 ++ public/js/components/Funktionen/Toast.js | 47 ++ .../Studentenverwaltung/Details/Funktionen.js | 28 + system/phrasesupdate.php | 25 +- 13 files changed, 1394 insertions(+), 1 deletion(-) create mode 100644 application/controllers/api/frontend/v1/funktionen/FunctionsAPI.php create mode 100644 application/controllers/api/frontend/v1/funktionen/OrgAPI.php create mode 100644 public/js/api/factory/funktionen.js create mode 100644 public/js/api/factory/funktionen/person.js create mode 100644 public/js/components/Funktionen/Funktionen.js create mode 100644 public/js/components/Funktionen/Modal.js create mode 100644 public/js/components/Funktionen/ModalDialog.js create mode 100644 public/js/components/Funktionen/OrgChooser.js create mode 100644 public/js/components/Funktionen/Toast.js create mode 100644 public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js 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 + ); From d3dc81eae8783e8b6191ee0f76d77978df6cc4c4 Mon Sep 17 00:00:00 2001 From: ma0068 Date: Fri, 9 May 2025 09:51:30 +0200 Subject: [PATCH 2/6] add missing fileendings in imports --- public/js/components/Funktionen/Funktionen.js | 2 +- public/js/components/Funktionen/OrgChooser.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/js/components/Funktionen/Funktionen.js b/public/js/components/Funktionen/Funktionen.js index c54628b24..0f0fd42c5 100644 --- a/public/js/components/Funktionen/Funktionen.js +++ b/public/js/components/Funktionen/Funktionen.js @@ -5,7 +5,7 @@ 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"; +import ApiPerson from "../../../extensions/FHC-Core-Personalverwaltung/js/api/factory/person.js"; export const Funktionen = { name: 'FunctionComponent', diff --git a/public/js/components/Funktionen/OrgChooser.js b/public/js/components/Funktionen/OrgChooser.js index 9c3086e76..64109cc2c 100644 --- a/public/js/components/Funktionen/OrgChooser.js +++ b/public/js/components/Funktionen/OrgChooser.js @@ -1,4 +1,4 @@ -import {CoreRESTClient} from '../../../js/RESTClient'; +import {CoreRESTClient} from '../../../js/RESTClient.js'; export const OrgChooser = { name: 'OrgChooser', From 12415279d1b8b55e071b7e2c98dd49204361118e Mon Sep 17 00:00:00 2001 From: ma0068 Date: Fri, 9 May 2025 14:08:49 +0200 Subject: [PATCH 3/6] hide column and formular field dienstverhaeltnis_unternehmen via prop --- public/js/components/Funktionen/Funktionen.js | 9 +++-- .../Studentenverwaltung/Details/Funktionen.js | 34 +++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/public/js/components/Funktionen/Funktionen.js b/public/js/components/Funktionen/Funktionen.js index 0f0fd42c5..335edd906 100644 --- a/public/js/components/Funktionen/Funktionen.js +++ b/public/js/components/Funktionen/Funktionen.js @@ -23,6 +23,7 @@ export const Funktionen = { personID: { type: Number, required: false }, personUID: { type: String, required: false }, writePermission: { type: Boolean, required: false }, + showDvCompany: { tpye: Boolean, required: false, default: true } }, emits: ['updateHeader'], setup( props, { emit } ) { @@ -197,7 +198,7 @@ export const Funktionen = { 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','dv_unternehmen'), field: "dienstverhaeltnis_unternehmen", formatter: dvFormatter, sorter:"string", headerFilter:"list", headerFilterParams: {valuesLookup:true, autocomplete:true, sort:"asc"}, visible: props.showDvCompany }, { 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 }, @@ -511,8 +512,10 @@ export const Funktionen = {
-
- +
diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js b/public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js index df73b6464..b58c0805c 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js +++ b/public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js @@ -1,28 +1,26 @@ 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: ` -
+ components: { + PersonFunctions, + }, + props: { + modelValue: Object, + }, + template: ` +
- - + + -
` +
`, }; From c26bb98960eb5d6186f8efc2a25144bbce59f175 Mon Sep 17 00:00:00 2001 From: ma0068 Date: Wed, 21 May 2025 15:34:43 +0200 Subject: [PATCH 4/6] new Core Component Function - autocomplete and dropdown field for search oes and search functions with active and non active entries - possibility to dynamic styling of display switch, newButton in Tabulator, different editLabels - possibility to show/hide column company - possibility to show/hide to save function as copy --- .../api/frontend/v1/funktionen/Funktionen.php | 385 ++++++ .../api/frontend/v1/stv/Config.php | 5 + .../Organisationseinheit_model.php | 68 + .../models/ressource/Funktion_model.php | 22 + application/views/Studentenverwaltung.php | 3 +- public/css/components/function.css | 3 + public/js/api/factory/functions.js | 124 ++ public/js/components/Funktionen/Funktionen.js | 1126 ++++++++--------- .../Funktionen/Funktionen_composition_api.js | 622 +++++++++ .../Studentenverwaltung/Details/Funktionen.js | 11 +- .../Details/FunktionenOld.js | 28 + system/phrasesupdate.php | 102 +- 12 files changed, 1891 insertions(+), 608 deletions(-) create mode 100644 application/controllers/api/frontend/v1/funktionen/Funktionen.php create mode 100644 public/css/components/function.css create mode 100644 public/js/api/factory/functions.js create mode 100644 public/js/components/Funktionen/Funktionen_composition_api.js create mode 100644 public/js/components/Stv/Studentenverwaltung/Details/FunktionenOld.js diff --git a/application/controllers/api/frontend/v1/funktionen/Funktionen.php b/application/controllers/api/frontend/v1/funktionen/Funktionen.php new file mode 100644 index 000000000..d6d4a0ae4 --- /dev/null +++ b/application/controllers/api/frontend/v1/funktionen/Funktionen.php @@ -0,0 +1,385 @@ + ['admin:r', 'assistenz:r'], + 'getContractFunctions' => ['admin:r', 'assistenz:r'], + 'getCurrentFunctions' => ['admin:r', 'assistenz:r'], + 'getAllUserFunctions' => ['admin:r', 'assistenz:r'], + 'getOrgHeads' => ['admin:r', 'assistenz:r'], + 'getOrgetsForCompany' => ['admin:r', 'assistenz:r'], + 'loadAllOes' => ['admin:r', 'assistenz:r'], + 'searchOes' => ['admin:r', 'assistenz:r'], + 'searchFunctions' => ['admin:r', 'assistenz:r'], + 'loadFunction' => ['admin:r', 'assistenz:r'], + 'insertFunction' => ['admin:rw', 'assistenz:rw'], + 'updateFunction' => ['admin:rw', 'assistenz:rw'], + 'deleteFunction' => ['admin:rw', 'assistenz:rw'], + ) + ); + + // Load Libraries + $this->load->library('VariableLib', ['uid' => getAuthUID()]); + $this->load->library('form_validation'); + + // Load language phrases + $this->loadPhrases([ + 'ui', + ]); + + // Load models + $this->load->model('extensions/FHC-Core-Personalverwaltung/Api_model', 'ApiModel'); + $this->load->model('ressource/Funktion_model', 'FunktionModel'); + $this->load->model('person/Benutzerfunktion_model', 'BenutzerfunktionModel'); + + $this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); + } + + public function getOrgHeads() + { + $result = $this->OrganisationseinheitModel->getHeads(); + + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + + public function getAllUserFunctions($uid) + { + if(!$uid) + { + $this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=> 'UID']), self::ERROR_TYPE_GENERAL); + } + + $sql = " + SELECT + dv.dienstverhaeltnis_id, + un.bezeichnung || ' (' || TO_CHAR(dv.von, 'DD.MM.YYYY') || CASE WHEN dv.bis IS NOT NULL THEN ' - ' + || TO_CHAR(dv.bis, 'DD.MM.YYYY') ELSE '' END || ')' AS dienstverhaeltnis_unternehmen , + '[' || oet.bezeichnung || '] ' || oe.bezeichnung AS funktion_oebezeichnung, + f.beschreibung AS funktion_beschreibung, + bf.*, + fb.bezeichnung AS fachbereich_bezeichnung, + CASE + WHEN + bf.datum_bis IS NOT NULL AND bf.datum_bis::date < now()::date + THEN + false + ELSE + true + END aktiv + FROM + public.tbl_benutzerfunktion bf + JOIN + public.tbl_organisationseinheit oe ON oe.oe_kurzbz = bf.oe_kurzbz + JOIN + public.tbl_organisationseinheittyp oet ON oe.organisationseinheittyp_kurzbz = oet.organisationseinheittyp_kurzbz + JOIN + public.tbl_funktion f ON f.funktion_kurzbz = bf.funktion_kurzbz + LEFT JOIN + hr.tbl_vertragsbestandteil_funktion vf ON vf.benutzerfunktion_id = bf.benutzerfunktion_id + LEFT JOIN + hr.tbl_vertragsbestandteil v ON vf.vertragsbestandteil_id = v.vertragsbestandteil_id + LEFT JOIN + hr.tbl_dienstverhaeltnis dv ON v.dienstverhaeltnis_id = dv.dienstverhaeltnis_id + LEFT JOIN + public.tbl_organisationseinheit un ON dv.oe_kurzbz = un.oe_kurzbz + LEFT JOIN + public.tbl_fachbereich fb ON fb.fachbereich_kurzbz = bf.fachbereich_kurzbz + WHERE + bf.uid = ? + ORDER BY + f.beschreibung, bf.datum_von ASC"; + + $benutzerfunktionen = $this->BenutzerfunktionModel->execReadOnlyQuery($sql, array($uid)); + $data = $this->getDataOrTerminateWithError($benutzerfunktionen); + + $this->terminateWithSuccess($data); + } + + //TODO(Manu) DELETE + public function DEPR_getCurrentFunctions($uid, $companyOrgetkurzbz) + { + $sql = " + SELECT + bf.benutzerfunktion_id, f.beschreibung || ', ' + || oe.bezeichnung || ' [' || oet.bezeichnung || '], ' + || COALESCE(to_char(bf.datum_von, 'dd.mm.YYYY'), 'n/a') + || ' - ' || COALESCE(to_char(bf.datum_bis, 'dd.mm.YYYY'), 'n/a') + || COALESCE(dvu.attachedtovb, '') AS label + + FROM ( + WITH RECURSIVE oes(oe_kurzbz, oe_parent_kurzbz) as + ( + SELECT oe_kurzbz, oe_parent_kurzbz FROM public.tbl_organisationseinheit + WHERE oe_kurzbz = ? + UNION ALL + SELECT o.oe_kurzbz, o.oe_parent_kurzbz FROM public.tbl_organisationseinheit o, oes + WHERE o.oe_parent_kurzbz=oes.oe_kurzbz + ) + SELECT oe_kurzbz + FROM oes + GROUP BY oe_kurzbz + ) c + JOIN public.tbl_organisationseinheit oe ON oe.oe_kurzbz = c.oe_kurzbz + JOIN public.tbl_organisationseinheittyp oet ON oe.organisationseinheittyp_kurzbz = oet.organisationseinheittyp_kurzbz + JOIN public.tbl_benutzerfunktion bf ON bf.oe_kurzbz = oe.oe_kurzbz + JOIN public.tbl_funktion f ON f.funktion_kurzbz = bf.funktion_kurzbz + LEFT JOIN ( + SELECT + benutzerfunktion_id, ' [DV]' AS attachedtovb + FROM + hr.tbl_vertragsbestandteil_funktion + GROUP BY + benutzerfunktion_id + ) dvu ON dvu.benutzerfunktion_id = bf.benutzerfunktion_id + WHERE bf.uid = ? + ORDER BY f.beschreibung ASC"; + + $benutzerfunktionen = $this->BenutzerfunktionModel->execReadOnlyQuery($sql, array( $companyOrgetkurzbz,$uid)); + $data = $this->getDataOrTerminateWithError($benutzerfunktionen); + + $this->terminateWithSuccess($data); + } + + /* + * 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) + { + $sql = " + SELECT + oe.oe_kurzbz, + '[' || COALESCE(oet.bezeichnung, oet.organisationseinheittyp_kurzbz) || + '] ' || COALESCE(oe.bezeichnung, oe.oe_kurzbz) AS label + FROM ( + WITH RECURSIVE oes(oe_kurzbz, oe_parent_kurzbz) as + ( + SELECT oe_kurzbz, oe_parent_kurzbz FROM public.tbl_organisationseinheit + WHERE oe_kurzbz=? + UNION ALL + SELECT o.oe_kurzbz, o.oe_parent_kurzbz FROM public.tbl_organisationseinheit o, oes + WHERE o.oe_parent_kurzbz=oes.oe_kurzbz + ) + SELECT oe_kurzbz + FROM oes + GROUP BY oe_kurzbz + ) c + JOIN public.tbl_organisationseinheit oe ON oe.oe_kurzbz = c.oe_kurzbz + JOIN public.tbl_organisationseinheittyp oet ON oe.organisationseinheittyp_kurzbz = oet.organisationseinheittyp_kurzbz + ORDER BY oet.bezeichnung ASC, oe.bezeichnung ASC"; + + $childorgets = $this->OrganisationseinheitModel->execReadOnlyQuery($sql, array($companyOrgetkurzbz)); + $data = $this->getDataOrTerminateWithError($childorgets); + + $this->terminateWithSuccess($data); + } + + //TODO(Manu) DELETE + /* + * List of Oes for autocomplete field organisation unit + */ + public function DEPR_loadAllOes($filterStudent=false, $aktiv=true) + { + $sql = " + SELECT + oe_kurzbz, + CONCAT('[', organisationseinheittyp_kurzbz, '] ', bezeichnung) as label + FROM public.tbl_organisationseinheit + WHERE aktiv = ?"; + + if($filterStudent) + $sql .= " AND organisationseinheittyp_kurzbz in ('Studiengang','Lehrgang')"; + + $sql .= " ORDER BY organisationseinheittyp_kurzbz, bezeichnung"; + + $result = $this->OrganisationseinheitModel->execReadOnlyQuery($sql, array($aktiv)); + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + + public function searchOes($companyOrgetKurzbz, $searchString = null) + { + $result = $this->OrganisationseinheitModel->getAutocompleteSuggestionsWithCompany($companyOrgetKurzbz, $searchString); + if (isError($result)) { + $this->terminateWithError($result, self::ERROR_TYPE_GENERAL); + } + $this->terminateWithSuccess($result ?: []); + } + + public function searchFunctions($searchString = null) + { + + $result = $this->FunktionModel->getAutocompleteSuggestions($searchString); + if (isError($result)) { + $this->terminateWithError($result, self::ERROR_TYPE_GENERAL); + } + $this->terminateWithSuccess($result ?: []); + } + + public function loadFunction($benutzerfunktion_id) + { + $this->BenutzerfunktionModel->addSelect("*"); + $result = $this->BenutzerfunktionModel->loadWhere( + array('benutzerfunktion_id' => $benutzerfunktion_id) + ); + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess(current($data)); + } + + public function insertFunction() + { + $this->load->library('form_validation'); + $authUID = getAuthUID(); + + $uid = $this->input->post('uid'); + + if(!$uid) + { + return $this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=> 'UID']), self::ERROR_TYPE_GENERAL); + } + + $formData = $this->input->post('formData'); + + $datum_von = $formData['datum_von'] ?? null; + $datum_bis = $formData['datum_bis'] ?? null; + $formData['oe_kurzbz'] = is_array($formData['oe_kurzbz']) ? $formData['oe_kurzbz']['oe_kurzbz'] : $formData['oe_kurzbz']; + $formData['funktion_kurzbz'] = is_array($formData['funktion_kurzbz']) + ? $formData['funktion_kurzbz']['funktion_kurzbz'] + : $formData['funktion_kurzbz']; + $bezeichnung = $formData['bezeichnung'] ?? null; + $wochenstunden = $formData['wochenstunden'] ?? null; + + $this->form_validation->set_data($formData); + $this->form_validation->set_rules('datum_von', 'VonDatum', 'required|is_valid_date', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => 'VonDatum']), + 'is_valid_date' => $this->p->t('ui', 'error_notValidDate', ['field' => 'VonDatum']) + ]); + $this->form_validation->set_rules('datum_bis', 'BisDatum', 'is_valid_date', [ + 'is_valid_date' => $this->p->t('ui', 'error_notValidDate', ['field' => 'BisDatum']) + ]); + $this->form_validation->set_rules('oe_kurzbz', 'Organisationseinheit', 'required', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => 'Organisationseinheit']) + ]); + $this->form_validation->set_rules('funktion_kurzbz', 'Funktion', 'required', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => 'Funktion']) + ]); + $this->form_validation->set_rules('wochenstunden', 'Wochenstunden', 'numeric', [ + 'numeric' => $this->p->t('ui', 'error_fieldNotNumeric', ['field' => 'Wochenstunden']) + ]); + + if ($this->form_validation->run() == false) + { + $this->terminateWithValidationErrors($this->form_validation->error_array()); + } + + $result = $this->BenutzerfunktionModel->insert([ + 'uid' => $uid, + 'datum_von' => $datum_von, + 'datum_bis' => $datum_bis , + 'oe_kurzbz' => $formData['oe_kurzbz'], + 'funktion_kurzbz' => $formData['funktion_kurzbz'], + 'bezeichnung' => $bezeichnung, + 'wochenstunden' => $wochenstunden, + 'insertamum' => date('c'), + 'insertvon' => $authUID, + ]); + + $data = $this->getDataOrTerminateWithError($result); + $this->terminateWithSuccess($data); + } + + public function updateFunction() + { + $this->load->library('form_validation'); + $authUID = getAuthUID(); + + $uid = $this->input->post('uid'); + + if(!$uid) + { + return $this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=> 'UID']), self::ERROR_TYPE_GENERAL); + } + $benutzerfunktion_id = $this->input->post('benutzerfunktion_id'); + + if(!$benutzerfunktion_id) + { + return $this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=> 'Benutzerfunktion ID']), self::ERROR_TYPE_GENERAL); + } + + $formData = $this->input->post('formData'); + + $datum_von = $formData['datum_von'] ?? null; + $datum_bis = $formData['datum_bis'] ?? null; + $formData['oe_kurzbz'] = is_array($formData['oe_kurzbz']) ? $formData['oe_kurzbz']['oe_kurzbz'] : $formData['oe_kurzbz']; + $formData['funktion_kurzbz'] = is_array($formData['funktion_kurzbz']) + ? $formData['funktion_kurzbz']['funktion_kurzbz'] + : $formData['funktion_kurzbz']; + $bezeichnung = $formData['bezeichnung'] ?? null; + $wochenstunden = $formData['wochenstunden'] ?? null; + + $this->form_validation->set_data($formData); + $this->form_validation->set_rules('datum_von', 'VonDatum', 'required|is_valid_date', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => 'VonDatum']), + 'is_valid_date' => $this->p->t('ui', 'error_notValidDate', ['field' => 'VonDatum']) + ]); + $this->form_validation->set_rules('datum_bis', 'BisDatum', 'is_valid_date', [ + 'is_valid_date' => $this->p->t('ui', 'error_notValidDate', ['field' => 'BisDatum']) + ]); + $this->form_validation->set_rules('oe_kurzbz', 'Organisationseinheit', 'required', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => 'Organisationseinheit']) + ]); + $this->form_validation->set_rules('funktion_kurzbz', 'Funktion', 'required', [ + 'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => 'Funktion']) + ]); + $this->form_validation->set_rules('wochenstunden', 'Wochenstunden', 'numeric', [ + 'numeric' => $this->p->t('ui', 'error_fieldNotNumeric', ['field' => 'Wochenstunden']) + ]); + + if ($this->form_validation->run() == false) + { + $this->terminateWithValidationErrors($this->form_validation->error_array()); + } + + $result = $this->BenutzerfunktionModel->update( + [ + 'benutzerfunktion_id' => $benutzerfunktion_id, + ], + [ + 'uid' => $uid, + 'datum_von' => $datum_von, + 'datum_bis' => $datum_bis , + 'oe_kurzbz' => $formData['oe_kurzbz'], + 'funktion_kurzbz' => $formData['funktion_kurzbz'], + 'bezeichnung' => $bezeichnung, + 'wochenstunden' => $wochenstunden, + 'updateamum' => date('c'), + 'updatevon' => $authUID, + ] + ); + + $data = $this->getDataOrTerminateWithError($result); + $this->terminateWithSuccess($data); + } + + public function deleteFunction($benutzerfunktion_id) + { + $result = $this->BenutzerfunktionModel->delete( + array('benutzerfunktion_id' => $benutzerfunktion_id) + ); + + $data = $this->getDataOrTerminateWithError($result); + $this->terminateWithSuccess($data); + } +} diff --git a/application/controllers/api/frontend/v1/stv/Config.php b/application/controllers/api/frontend/v1/stv/Config.php index d37a6d6ec..f22f888de 100644 --- a/application/controllers/api/frontend/v1/stv/Config.php +++ b/application/controllers/api/frontend/v1/stv/Config.php @@ -132,6 +132,11 @@ class Config extends FHCAPI_Controller 'title' => $this->p->t('stv', 'tab_functions'), 'component' => './Stv/Studentenverwaltung/Details/Funktionen.js' ]; +/* TODO(Manu) REMOVE just for Testing*/ + $result['functions_composition'] = [ + 'title' => 'F_PV21', + 'component' => './Stv/Studentenverwaltung/Details/FunktionenOld.js' + ]; Events::trigger('stv_conf_student', function & () use (&$result) { return $result; diff --git a/application/models/organisation/Organisationseinheit_model.php b/application/models/organisation/Organisationseinheit_model.php index 1b1a826aa..b146ee7e5 100644 --- a/application/models/organisation/Organisationseinheit_model.php +++ b/application/models/organisation/Organisationseinheit_model.php @@ -217,4 +217,72 @@ class Organisationseinheit_model extends DB_Model oe_kurzbz ILIKE '%". $this->escapeLike($eventQuery). "%' "); } + + /** + * Get OEs by eventQuery string and companyOrgetKurzbz + * Use with autocomplete event queries in Function Component + * @param $searchString String + * @param $companyOrgetKurzbz String oe_kurzbz of the company (gst vs gmbh) + * @return array + */ + public function getAutocompleteSuggestionsWithCompany($companyOrgetKurzbz, $searchString) + { + $sql = " + WITH RECURSIVE oes(oe_kurzbz, oe_parent_kurzbz) AS ( + SELECT oe_kurzbz, oe_parent_kurzbz + FROM public.tbl_organisationseinheit + WHERE oe_kurzbz = ? + UNION ALL + SELECT o.oe_kurzbz, o.oe_parent_kurzbz + FROM public.tbl_organisationseinheit o + INNER JOIN oes ON o.oe_parent_kurzbz = oes.oe_kurzbz + ) + SELECT + oe.oe_kurzbz, oe.aktiv, + '[' || COALESCE(oet.bezeichnung, oet.organisationseinheittyp_kurzbz) || + '] ' || COALESCE(oe.bezeichnung, oe.oe_kurzbz) AS label + FROM ( + SELECT oe_kurzbz FROM oes GROUP BY oe_kurzbz + ) c + JOIN public.tbl_organisationseinheit oe ON oe.oe_kurzbz = c.oe_kurzbz + JOIN public.tbl_organisationseinheittyp oet ON oe.organisationseinheittyp_kurzbz = oet.organisationseinheittyp_kurzbz + "; + + $params = [$companyOrgetKurzbz]; + + if (!empty($searchString)) { + $escaped = $this->escapeLike($searchString); + $ilike = '%' . $escaped . '%'; + + $sql .= " + WHERE + oe.oe_kurzbz ILIKE ? OR + oe.bezeichnung ILIKE ? OR + oe.organisationseinheittyp_kurzbz ILIKE ? + "; + + $params[] = $ilike; + $params[] = $ilike; + $params[] = $ilike; + } + + $sql .= " ORDER BY oet.bezeichnung ASC, oe.bezeichnung ASC"; + + $result = $this->execQuery($sql, $params); + + return $result; + } + + + /** + * get highest organisation units + */ + public function getHeads() + { + $this->addSelect('*'); + $this->addSelect('oe_kurzbz as head'); + $result = $this->loadWhere(array('oe_parent_kurzbz' => null, 'aktiv' => true)); + + return $result; + } } diff --git a/application/models/ressource/Funktion_model.php b/application/models/ressource/Funktion_model.php index 43908167c..60965b438 100644 --- a/application/models/ressource/Funktion_model.php +++ b/application/models/ressource/Funktion_model.php @@ -11,4 +11,26 @@ class Funktion_model extends DB_Model $this->dbTable = 'public.tbl_funktion'; $this->pk = 'funktion_kurzbz'; } + + /** + * Get Functions by eventQuery string. Use with autocomplete event queries in Function Component + * @param $eventQuery String + * @return array + */ + public function getAutocompleteSuggestions($eventQuery) + { + $this->addSelect('funktion_kurzbz, beschreibung, aktiv'); + $this->addSelect("beschreibung AS label"); + $this->addOrder('beschreibung', 'ASC'); + + if($eventQuery === null) + { + return $this->load(); + } + + return $this->loadWhere(" + funktion_kurzbz ILIKE '%". $this->escapeLike($eventQuery). "%' + OR beschreibung ILIKE '%". $this->escapeLike($eventQuery). "%' + "); + } } diff --git a/application/views/Studentenverwaltung.php b/application/views/Studentenverwaltung.php index 398b921d2..a1b7fe347 100644 --- a/application/views/Studentenverwaltung.php +++ b/application/views/Studentenverwaltung.php @@ -18,7 +18,8 @@ #datepicker fuer component functions 'public/css/components/vue-datepicker.css', 'public/css/components/primevue.css', - 'public/css/Studentenverwaltung.css' + 'public/css/Studentenverwaltung.css', + 'public/css/components/function.css' ], 'customJSs' => [ 'vendor/vuejs/vuedatepicker_js/vue-datepicker.iife.js' diff --git a/public/css/components/function.css b/public/css/components/function.css new file mode 100644 index 000000000..3e5e4a632 --- /dev/null +++ b/public/css/components/function.css @@ -0,0 +1,3 @@ +.item-inactive { + text-decoration: line-through; +} \ No newline at end of file diff --git a/public/js/api/factory/functions.js b/public/js/api/factory/functions.js new file mode 100644 index 000000000..ac717050a --- /dev/null +++ b/public/js/api/factory/functions.js @@ -0,0 +1,124 @@ +/** + * 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/Funktionen/getContractFunctions'; + if( typeof filter !== 'undefined' && filter !== null ) { + url = url + '/' + filter; + } + return { + method: 'get', + url, + }; + + }, + getOrgHeads() { + var url = 'api/frontend/v1/funktionen/Funktionen/getOrgHeads'; + return { + method: 'get', + url, + }; + }, + getOrgetsForCompany(unternehmen) { + var url = 'api/frontend/v1/funktionen/Funktionen/getOrgetsForCompany' + + '/' + unternehmen; + return { + method: 'get', + url, + }; + }, + loadAllOes(filterStudent) { + var url = 'api/frontend/v1/funktionen/Funktionen/loadAllOes' + + '/' + filterStudent; + return { + method: 'get', + url, + }; + }, + getCompanyByOrget(orget) { + var url = 'api/frontend/v1/funktionen/Funktionen/getCompanyByOrget' + + '/' + orget; + return { + method: 'get', + url, + }; + }, + getCurrentFunctions(mitarbeiter_uid, unternehmen) { + var url = 'api/frontend/v1/funktionen/Funktionen/getCurrentFunctions' + + '/' + mitarbeiter_uid + '/' + unternehmen; + return { + method: 'get', + url, + }; + } , + getAllUserFunctions(mitarbeiter_uid) { + var url = 'api/frontend/v1/funktionen/Funktionen/getAllUserFunctions' + + '/' + mitarbeiter_uid; + return { + method: 'get', + url, + }; + }, + getAllFunctions() { + var url = 'api/frontend/v1/funktionen/Funktionen/getAllFunctions'; + return { + method: 'get', + url, + }; + }, + addFunction(params) { + return { + method: 'post', + url: 'api/frontend/v1/funktionen/Funktionen/insertFunction/', + params + }; + }, + loadFunction(benutzerfunktion_id) { + var url = 'api/frontend/v1/funktionen/Funktionen/loadFunction' + + '/' + benutzerfunktion_id; + return { + method: 'get', + url, + }; + }, + updateFunction(params) { + return { + method: 'post', + url: 'api/frontend/v1/funktionen/Funktionen/updateFunction/', + params + }; + }, + deleteFunction(benutzerfunktion_id) { + return { + method: 'post', + url: 'api/frontend/v1/funktionen/Funktionen/deleteFunction/' + benutzerfunktion_id + }; + }, + getOes(head, searchString) { + return { + method: 'get', + url: 'api/frontend/v1/funktionen/Funktionen/searchOes/' + head + '/' + searchString + }; + }, + getFunctions(searchString) { + return { + method: 'get', + url: 'api/frontend/v1/funktionen/Funktionen/searchFunctions/' + searchString + }; + } + +}; \ No newline at end of file diff --git a/public/js/components/Funktionen/Funktionen.js b/public/js/components/Funktionen/Funktionen.js index 335edd906..9618bdf9b 100644 --- a/public/js/components/Funktionen/Funktionen.js +++ b/public/js/components/Funktionen/Funktionen.js @@ -1,623 +1,547 @@ -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 {CoreFilterCmpt} from "../filter/Filter.js"; +import FormInput from "../Form/Input.js"; +import FormForm from "../Form/Form.js"; +import BsModal from "../Bootstrap/Modal.js"; +import PvAutoComplete from "../../../../index.ci.php/public/js/components/primevue/autocomplete/autocomplete.esm.min.js"; -import ApiFunktion from '../../api/factory/funktionen/person.js'; -import ApiPerson from "../../../extensions/FHC-Core-Personalverwaltung/js/api/factory/person.js"; +import ApiCoreFunktion from '../../api/factory/functions.js'; -export const Funktionen = { +export default { name: 'FunctionComponent', components: { - Modal, - ModalDialog, - Toast, - OrgChooser, - "datepicker": VueDatePicker + CoreFilterCmpt, + FormInput, + FormForm, + BsModal, + PvAutoComplete }, 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 }, - showDvCompany: { tpye: Boolean, required: false, default: true } + modelValue: { + type: Object, + default: () => ({}), + required: false + }, + config: {type: Object, default: () => ({}), required: false}, + readonlyMode: {type: Boolean, required: false, default: false}, + personID: {type: Number, required: true}, + personUID: {type: String, required: true}, + writePermission: {type: Boolean, required: false}, + showDvCompany: {type: Boolean, required: false, default: true}, + saveFunctionAsCopy: {type: Boolean, required: false, default: false}, + stylePv21: {type: Boolean, required: false, default: false}, + companyLinkFormatter: {type: Function || null, default: null} }, - 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"}, visible: props.showDvCompany }, - { 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/`; - + data(){ return { - jobfunctionList, orgUnitList, jobfunctionListArray, - jobfunctionDefList, - currentValue, - readonly, - frmState, - dialogRef, - toastRef, deleteToastRef, - jobFunctionFrm, - modalRef, - fullPath, - route, - aktivChecked, - unternehmen, - tabulator, - table, + tabulatorOptions: { + ajaxURL: 'dummy', + ajaxRequestFunc: () => this.$api.call( + ApiCoreFunktion.getAllUserFunctions(this.personUID) + ), + ajaxResponse: (url, params, response) => response.data, + columns: [ + { + title: "dienstverhaeltnis_unternehmen", + field: "dienstverhaeltnis_unternehmen", + width: 50, + headerFilter: "list", + headerFilterParams: {valuesLookup: true, autocomplete: true, sort: "asc"}, + formatter: this.companyLinkFormatter + }, + { + title: "funktion_beschreibung", field: "funktion_beschreibung", headerFilter: "list", + headerFilterParams: {valuesLookup: true, autocomplete: true, sort: "asc"}, + }, + { + title: "funktion_oebezeichnung", field: "funktion_oebezeichnung", headerFilter: "list", + headerFilterParams: {valuesLookup: true, autocomplete: true, sort: "asc"} + }, + {title: "wochenstunden", field: "wochenstunden", headerFilter: true}, + { + title: "Von", + field: "datum_von", + headerFilter: true, + formatter: function (cell) { + const dateStr = cell.getValue(); + if (!dateStr) return ""; - toggleMode, formatDate, notEmpty, - showToast, showDeletedToast, - showAddModal, hideModal, okHandler, - showDeleteModal, showEditModal, confirmDeleteRef, t, unternehmenSelectedHandler, + const date = new Date(dateStr); + return date.toLocaleString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + }, + }, + { + title: "Bis", + field: "datum_bis", + headerFilter: true, + formatter: function (cell) { + const dateStr = cell.getValue(); + if (!dateStr) return ""; + + const date = new Date(dateStr); + return date.toLocaleString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); + }, + }, + {title: "bezeichnung", field: "bezeichnung", headerFilter: true}, + {title: "aktiv", field: "aktiv", visible: false}, + {title: "benutzerfunktion_id", field: "benutzerfunktion_id", visible: false}, + {title: "uid", field: "uid", visible: false}, + { + //title: 'Aktionen', field: 'actions', + minWidth: 150, // Ensures Action-buttons will be always fully displayed + formatter: (cell, formatterParams, onRendered) => { + let container = document.createElement('div'); + container.className = "d-flex gap-2"; + + + if( cell.getRow().getData().dienstverhaeltnis_unternehmen === null ) { + let button = document.createElement('button'); + button.className = 'btn btn-outline-secondary btn-action'; + if(this.stylePv21) + button.innerHTML = ''; + else + button.innerHTML = ''; + button.title = this.$p.t('ui', 'bearbeiten'); + button.addEventListener('click', (event) => + this.actionEditFunction(cell.getData().benutzerfunktion_id) + ); + if(this.readonlyMode === true) button.disabled = true; + container.append(button); + } + if( cell.getRow().getData().dienstverhaeltnis_unternehmen === null ) { + let button = document.createElement('button'); + button.className = 'btn btn-outline-secondary btn-action'; + button.innerHTML = ''; + button.title = this.$p.t('ui', 'loeschen'); + button.addEventListener('click', () => + this.actionDeleteFunction(cell.getData().benutzerfunktion_id) + ); + if(this.readonlyMode === true) button.disabled = true; + container.append(button); + } + + if (cell.getRow().getData().dienstverhaeltnis_unternehmen === null && this.saveFunctionAsCopy) { + let button = document.createElement('button'); + button.className = 'btn btn-outline-secondary btn-action'; + button.innerHTML = ''; + button.title = this.$p.t('ui', 'saveAsCopy'); + button.addEventListener('click', () => + this.actionCopyFunction(cell.getData().benutzerfunktion_id) + ); + if(this.readonlyMode === true) button.disabled = true; + container.append(button); + } + + return container; + }, + frozen: true + } + ], + layout: 'fitDataFill', + layoutColumnsOnNewData: false, + height: '300', + persistenceID: 'core-functions', + }, + tabulatorEvents: [ + { + event: 'tableBuilt', + handler: async () => { + await this.$p.loadCategory(['global', 'lehre', 'person', 'ui']); + let cm = this.$refs.table.tabulator.columnManager; + + //Field Company: if visible show link to dv + const column = cm.getColumnByField('dienstverhaeltnis_unternehmen'); + const companyDv = { + title: this.$p.t('person', 'dv_unternehmen'), + width: 100, + visible: this.showDvCompany, + }; + column.component.updateDefinition(companyDv); + + cm.getColumnByField('funktion_beschreibung').component.updateDefinition({ + title: this.$p.t('person', 'zuordnung_taetigkeit'), + width: 100 + }); + cm.getColumnByField('funktion_oebezeichnung').component.updateDefinition({ + title: this.$p.t('lehre', 'organisationseinheit'), + width: 100 + }); + cm.getColumnByField('wochenstunden').component.updateDefinition({ + title: this.$p.t('person', 'wochenstunden') + }); + + const columnDatumVon = cm.getColumnByField('datum_von'); + const fieldVonDatum = { + title: this.$p.t('ui', 'from') + }; + + columnDatumVon.component.updateDefinition(fieldVonDatum); + + const columnDatumBis = cm.getColumnByField('datum_bis'); + const fieldBisDatum = { + title: this.$p.t('global', 'bis'), + }; + columnDatumBis.component.updateDefinition(fieldBisDatum); + + cm.getColumnByField('bezeichnung').component.updateDefinition({ + title: this.$p.t('ui', 'bezeichnung'), + width: 100 + }); + + } + } + ], + isFilterSet: false, + listOrgHeads: [], + listOrgUnits: [], + formData: { + head: 'gst', + oe_kurzbz: '' + }, + statusNew: true, + listAllFunctions: [], + abortController: { + oes: null, + functions: null + }, + filteredOes: [], + filteredFunctions: [], + newBtnStyle: '' } }, + computed: { + }, + methods: { + onSwitchChange() { + if (this.isFilterSet) { + this.$refs.table.tabulator.setFilter("aktiv", "=", true); + } + else { + this.$refs.table.tabulator.clearFilter(); + this.isFilterSet = false; + } + }, + actionNewFunction(){ + this.resetModal(); + this.statusNew = true; + this.$refs.functionModal.show(); + }, + actionCopyFunction(benutzerfunktion_id) { + this.statusNew = true; + this.loadFunction(benutzerfunktion_id).then(() => { + this.$refs.functionModal.show(); + }); + }, + actionDeleteFunction(benutzerfunktion_id) { + this.$fhcAlert + .confirmDelete() + .then(result => result + ? benutzerfunktion_id + : Promise.reject({handled: true})) + .then(this.deleteFunction) + .catch(this.$fhcAlert.handleSystemError); + }, + actionEditFunction(benutzerfunktion_id) { + this.resetModal(); + this.statusNew = false; + this.loadFunction(benutzerfunktion_id); + this.$refs.functionModal.show(); + }, + addFunction() { + const dataToSend = { + uid: this.personUID, + formData: this.formData + }; + return this.$refs.functionData + .call(ApiCoreFunktion.addFunction(dataToSend)) + .then(response => { + this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); + this.hideModal('functionModal'); + this.resetModal(); + }).catch(this.$fhcAlert.handleSystemError) + .finally(() => { + this.reload(); + }); + }, + loadFunction(benutzerfunktion_id) { + return this.$api + .call(ApiCoreFunktion.loadFunction(benutzerfunktion_id)) + .then(result => { + this.formData = result.data; + }) + .catch(this.$fhcAlert.handleSystemError); + }, + updateFunction(benutzerfunktion_id){ + const dataToSend = { + uid: this.personUID, + formData: this.formData, + benutzerfunktion_id: benutzerfunktion_id + }; + return this.$refs.functionData + .call(ApiCoreFunktion.updateFunction(dataToSend)) + .then(response => { + this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); + this.hideModal('functionModal'); + this.resetModal(); + }).catch(this.$fhcAlert.handleSystemError) + .finally(() => { + this.reload(); + }); + }, + deleteFunction(benutzerfunktion_id) { + return this.$api + .call(ApiCoreFunktion.deleteFunction(benutzerfunktion_id)) + .then(response => { + this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete')); + }) + .catch(this.$fhcAlert.handleSystemError) + .finally(() => { + this.reload(); + }); + }, + getOrgetsForCompany(){ + return this.$api + .call(ApiCoreFunktion.getOrgetsForCompany(this.formData.head)) + .then(result => { + this.listOrgUnits = result.data; + }) + .catch(this.$fhcAlert.handleSystemError); + }, + hideModal(modalRef) { + this.$refs[modalRef].hide(); + }, + reload() { + this.$refs.table.reloadTable(); + }, + resetModal(){ + this.formData = {}; + this.formData.head = 'gst'; + this.formData.oe_kurzbz = ''; + this.formData.funktion_kurzbz = ''; + }, + searchOe(event) { + if (this.abortController.oes) { + this.abortController.oes.abort(); + } + + this.abortController.oes = new AbortController(); + + return this.$api + .call(ApiCoreFunktion.getOes(this.formData.head, event.query)) + .then(result => { + this.filteredOes = result.data.retval; + }); + }, + searchFunctions(event) { + if (this.abortController.functions) { + this.abortController.functions.abort(); + } + + this.abortController.functions = new AbortController(); + + return this.$api + .call(ApiCoreFunktion.getFunctions(event.query)) + .then(result => { + this.filteredFunctions = result.data.retval; + }); + }, + styleNewButton(){ + if(this.stylePv21) { + this.newBtnStyle = "btn-sm"; + } + }, + //helper function: workaround to trigger validation if input is not a number + normalizeStunden() { + if (this.formData.wochenstunden === null || this.formData.wochenstunden === '') { + this.formData.wochenstunden = 'xxx' + } + } + }, + created() { + this.$api + .call(ApiCoreFunktion.getOrgHeads()) + .then(result => { + this.listOrgHeads = result.data; + }) + .catch(this.$fhcAlert.handleSystemError); + + this.styleNewButton(); + }, template: ` -
+
-
- - - -
+
+ + +
+
+ + +
-
- - - -
-
-
-
-
-
-
{{ t('person','funktionen') }}
-
+ + -
-
- -
- - -
-
+ + + - -
- -
-
-
-
- + + + + - - - - + + + + - + + + + - - - + + + - - - +
+ + + + + + + + +
+ + +
+ + + + +
` } - -export default Funktionen; \ No newline at end of file diff --git a/public/js/components/Funktionen/Funktionen_composition_api.js b/public/js/components/Funktionen/Funktionen_composition_api.js new file mode 100644 index 000000000..d48e77fd3 --- /dev/null +++ b/public/js/components/Funktionen/Funktionen_composition_api.js @@ -0,0 +1,622 @@ +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.js"; + +export const Funktionen = { + name: 'FunctionComponentOld', + 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 }, + showDvCompany: { type: Boolean, required: false, default: true } + }, + 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"}, visible: props.showDvCompany }, + { 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/Stv/Studentenverwaltung/Details/Funktionen.js b/public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js index b58c0805c..3e82cc48b 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js +++ b/public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js @@ -9,16 +9,17 @@ export default { }, template: `
- - +

{{ $p.t('person', 'funktionen') }}

diff --git a/public/js/components/Stv/Studentenverwaltung/Details/FunktionenOld.js b/public/js/components/Stv/Studentenverwaltung/Details/FunktionenOld.js new file mode 100644 index 000000000..5188c8d1b --- /dev/null +++ b/public/js/components/Stv/Studentenverwaltung/Details/FunktionenOld.js @@ -0,0 +1,28 @@ +import PersonFunctions from "../../../Funktionen/Funktionen_composition_api"; + +//import PersonFunctions from "../../../Funktionen/Funktionen.js"; + +export default { + components: { + PersonFunctions, + }, + props: { + modelValue: Object, + }, + template: ` +
+ + + + + + +
`, +}; diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 6c52517b6..ed6f71a7f 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -25079,7 +25079,7 @@ array( 'phrases' => array( array( 'sprache' => 'German', - 'text' => 'Das Eingabefeld {field} darf nur Zahlen enthalten.', + 'text' => 'Das Eingabefeld {field} darf nur Ziffern enthalten.', 'description' => '', 'insertvon' => 'system' ), @@ -41476,6 +41476,106 @@ and represent the current state of research on the topic. The prescribed citatio ) ) ), + array( + 'app' => 'core', + 'category' => 'funktion', + 'phrase' => 'filter_active', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Nur aktive anzeigen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Display only active', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'funktion', + 'phrase' => 'addFunktion', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Funktion hinzufügen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Add function', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'funktion', + 'phrase' => 'editFunktion', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Funktion bearbeiten', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Edit function', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'saveAsCopy', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Als Kopie speichern', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Save as copy', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'error_fieldMustBePositiveInteger', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Das Eingabefeld {field} darf nur positive Ganzzahlen enthalten.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'The input field {field} may only contain positive integers', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), // FHC-4 Studierendenverwaltung FUNCTIONS END ); From 66e5384de6ed1aeb44f9bedd831eabc19b150f8f Mon Sep 17 00:00:00 2001 From: ma0068 Date: Wed, 21 May 2025 16:01:37 +0200 Subject: [PATCH 5/6] cleanup: remove not needed code and files --- .../frontend/v1/funktionen/FunctionsAPI.php | 239 ------- .../api/frontend/v1/funktionen/Funktionen.php | 69 -- .../api/frontend/v1/funktionen/OrgAPI.php | 153 ----- .../api/frontend/v1/stv/Config.php | 5 - public/js/api/factory/funktionen.js | 22 - public/js/api/factory/funktionen/person.js | 68 -- public/js/components/Funktionen/Funktionen.js | 4 +- .../Funktionen/Funktionen_composition_api.js | 622 ------------------ public/js/components/Funktionen/Modal.js | 48 -- .../js/components/Funktionen/ModalDialog.js | 69 -- public/js/components/Funktionen/OrgChooser.js | 69 -- public/js/components/Funktionen/Toast.js | 47 -- .../Studentenverwaltung/Details/Funktionen.js | 5 +- 13 files changed, 2 insertions(+), 1418 deletions(-) delete mode 100644 application/controllers/api/frontend/v1/funktionen/FunctionsAPI.php delete mode 100644 application/controllers/api/frontend/v1/funktionen/OrgAPI.php delete mode 100644 public/js/api/factory/funktionen.js delete mode 100644 public/js/api/factory/funktionen/person.js delete mode 100644 public/js/components/Funktionen/Funktionen_composition_api.js delete mode 100644 public/js/components/Funktionen/Modal.js delete mode 100644 public/js/components/Funktionen/ModalDialog.js delete mode 100644 public/js/components/Funktionen/OrgChooser.js delete mode 100644 public/js/components/Funktionen/Toast.js diff --git a/application/controllers/api/frontend/v1/funktionen/FunctionsAPI.php b/application/controllers/api/frontend/v1/funktionen/FunctionsAPI.php deleted file mode 100644 index 023100a6e..000000000 --- a/application/controllers/api/frontend/v1/funktionen/FunctionsAPI.php +++ /dev/null @@ -1,239 +0,0 @@ - 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/Funktionen.php b/application/controllers/api/frontend/v1/funktionen/Funktionen.php index d6d4a0ae4..030994736 100644 --- a/application/controllers/api/frontend/v1/funktionen/Funktionen.php +++ b/application/controllers/api/frontend/v1/funktionen/Funktionen.php @@ -105,51 +105,6 @@ class Funktionen extends FHCAPI_Controller $this->terminateWithSuccess($data); } - //TODO(Manu) DELETE - public function DEPR_getCurrentFunctions($uid, $companyOrgetkurzbz) - { - $sql = " - SELECT - bf.benutzerfunktion_id, f.beschreibung || ', ' - || oe.bezeichnung || ' [' || oet.bezeichnung || '], ' - || COALESCE(to_char(bf.datum_von, 'dd.mm.YYYY'), 'n/a') - || ' - ' || COALESCE(to_char(bf.datum_bis, 'dd.mm.YYYY'), 'n/a') - || COALESCE(dvu.attachedtovb, '') AS label - - FROM ( - WITH RECURSIVE oes(oe_kurzbz, oe_parent_kurzbz) as - ( - SELECT oe_kurzbz, oe_parent_kurzbz FROM public.tbl_organisationseinheit - WHERE oe_kurzbz = ? - UNION ALL - SELECT o.oe_kurzbz, o.oe_parent_kurzbz FROM public.tbl_organisationseinheit o, oes - WHERE o.oe_parent_kurzbz=oes.oe_kurzbz - ) - SELECT oe_kurzbz - FROM oes - GROUP BY oe_kurzbz - ) c - JOIN public.tbl_organisationseinheit oe ON oe.oe_kurzbz = c.oe_kurzbz - JOIN public.tbl_organisationseinheittyp oet ON oe.organisationseinheittyp_kurzbz = oet.organisationseinheittyp_kurzbz - JOIN public.tbl_benutzerfunktion bf ON bf.oe_kurzbz = oe.oe_kurzbz - JOIN public.tbl_funktion f ON f.funktion_kurzbz = bf.funktion_kurzbz - LEFT JOIN ( - SELECT - benutzerfunktion_id, ' [DV]' AS attachedtovb - FROM - hr.tbl_vertragsbestandteil_funktion - GROUP BY - benutzerfunktion_id - ) dvu ON dvu.benutzerfunktion_id = bf.benutzerfunktion_id - WHERE bf.uid = ? - ORDER BY f.beschreibung ASC"; - - $benutzerfunktionen = $this->BenutzerfunktionModel->execReadOnlyQuery($sql, array( $companyOrgetkurzbz,$uid)); - $data = $this->getDataOrTerminateWithError($benutzerfunktionen); - - $this->terminateWithSuccess($data); - } - /* * return list of child orgets for a given company orget_kurzbz * as key value list to be used in select or autocomplete @@ -184,30 +139,6 @@ class Funktionen extends FHCAPI_Controller $this->terminateWithSuccess($data); } - //TODO(Manu) DELETE - /* - * List of Oes for autocomplete field organisation unit - */ - public function DEPR_loadAllOes($filterStudent=false, $aktiv=true) - { - $sql = " - SELECT - oe_kurzbz, - CONCAT('[', organisationseinheittyp_kurzbz, '] ', bezeichnung) as label - FROM public.tbl_organisationseinheit - WHERE aktiv = ?"; - - if($filterStudent) - $sql .= " AND organisationseinheittyp_kurzbz in ('Studiengang','Lehrgang')"; - - $sql .= " ORDER BY organisationseinheittyp_kurzbz, bezeichnung"; - - $result = $this->OrganisationseinheitModel->execReadOnlyQuery($sql, array($aktiv)); - $data = $this->getDataOrTerminateWithError($result); - - $this->terminateWithSuccess($data); - } - public function searchOes($companyOrgetKurzbz, $searchString = null) { $result = $this->OrganisationseinheitModel->getAutocompleteSuggestionsWithCompany($companyOrgetKurzbz, $searchString); diff --git a/application/controllers/api/frontend/v1/funktionen/OrgAPI.php b/application/controllers/api/frontend/v1/funktionen/OrgAPI.php deleted file mode 100644 index f6cf5bdb8..000000000 --- a/application/controllers/api/frontend/v1/funktionen/OrgAPI.php +++ /dev/null @@ -1,153 +0,0 @@ - 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 f22f888de..d37a6d6ec 100644 --- a/application/controllers/api/frontend/v1/stv/Config.php +++ b/application/controllers/api/frontend/v1/stv/Config.php @@ -132,11 +132,6 @@ class Config extends FHCAPI_Controller 'title' => $this->p->t('stv', 'tab_functions'), 'component' => './Stv/Studentenverwaltung/Details/Funktionen.js' ]; -/* TODO(Manu) REMOVE just for Testing*/ - $result['functions_composition'] = [ - 'title' => 'F_PV21', - 'component' => './Stv/Studentenverwaltung/Details/FunktionenOld.js' - ]; Events::trigger('stv_conf_student', function & () use (&$result) { return $result; diff --git a/public/js/api/factory/funktionen.js b/public/js/api/factory/funktionen.js deleted file mode 100644 index c1f4efa29..000000000 --- a/public/js/api/factory/funktionen.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * 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 deleted file mode 100644 index 305d0bbeb..000000000 --- a/public/js/api/factory/funktionen/person.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * 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 index 9618bdf9b..7bbd26702 100644 --- a/public/js/components/Funktionen/Funktionen.js +++ b/public/js/components/Funktionen/Funktionen.js @@ -216,8 +216,6 @@ export default { newBtnStyle: '' } }, - computed: { - }, methods: { onSwitchChange() { if (this.isFilterSet) { @@ -387,7 +385,7 @@ export 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 }, - showDvCompany: { type: Boolean, required: false, default: true } - }, - 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"}, visible: props.showDvCompany }, - { 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 deleted file mode 100644 index 716d9ac2a..000000000 --- a/public/js/components/Funktionen/Modal.js +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index c46540cae..000000000 --- a/public/js/components/Funktionen/ModalDialog.js +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index 64109cc2c..000000000 --- a/public/js/components/Funktionen/OrgChooser.js +++ /dev/null @@ -1,69 +0,0 @@ -import {CoreRESTClient} from '../../../js/RESTClient.js'; - -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 deleted file mode 100644 index 6b3faba82..000000000 --- a/public/js/components/Funktionen/Toast.js +++ /dev/null @@ -1,47 +0,0 @@ -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 index 3e82cc48b..f968ebecc 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js +++ b/public/js/components/Stv/Studentenverwaltung/Details/Funktionen.js @@ -9,16 +9,13 @@ export default { }, template: `
-

{{ $p.t('person', 'funktionen') }}

From fa0b534471185b9708de0bce7db1cad256c34a67 Mon Sep 17 00:00:00 2001 From: ma0068 Date: Fri, 23 May 2025 13:27:18 +0200 Subject: [PATCH 6/6] Refactoring PrimeVue Autocomplete Dropdown Functions Refactoring PrimeVue Autocomplete Dropdown OrganisationUnits Change Search from Backend to Frontend Filtering minor changes Table Functions --- .../api/frontend/v1/funktionen/Funktionen.php | 68 ++++---- .../Organisationseinheit_model.php | 74 +-------- .../models/ressource/Funktion_model.php | 22 --- public/js/api/factory/functions.js | 39 +---- public/js/components/Funktionen/Funktionen.js | 155 +++++++++++------- system/phrasesupdate.php | 20 +++ 6 files changed, 162 insertions(+), 216 deletions(-) diff --git a/application/controllers/api/frontend/v1/funktionen/Funktionen.php b/application/controllers/api/frontend/v1/funktionen/Funktionen.php index 030994736..b2787072b 100644 --- a/application/controllers/api/frontend/v1/funktionen/Funktionen.php +++ b/application/controllers/api/frontend/v1/funktionen/Funktionen.php @@ -11,14 +11,10 @@ class Funktionen extends FHCAPI_Controller //TODO(Manu) check permissions parent::__construct(array( 'getAllFunctions' => ['admin:r', 'assistenz:r'], - 'getContractFunctions' => ['admin:r', 'assistenz:r'], - 'getCurrentFunctions' => ['admin:r', 'assistenz:r'], 'getAllUserFunctions' => ['admin:r', 'assistenz:r'], 'getOrgHeads' => ['admin:r', 'assistenz:r'], 'getOrgetsForCompany' => ['admin:r', 'assistenz:r'], - 'loadAllOes' => ['admin:r', 'assistenz:r'], - 'searchOes' => ['admin:r', 'assistenz:r'], - 'searchFunctions' => ['admin:r', 'assistenz:r'], + 'getAllOrgUnits' => ['admin:r', 'assistenz:r'], 'loadFunction' => ['admin:r', 'assistenz:r'], 'insertFunction' => ['admin:rw', 'assistenz:rw'], 'updateFunction' => ['admin:rw', 'assistenz:rw'], @@ -43,6 +39,20 @@ class Funktionen extends FHCAPI_Controller $this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); } + public function getAllFunctions() + { + $this->FunktionModel->addSelect("funktion_kurzbz"); + $this->FunktionModel->addSelect("beschreibung"); + $this->FunktionModel->addSelect("aktiv"); + $this->FunktionModel->addSelect("beschreibung AS label"); + $this->FunktionModel->addOrder("beschreibung"); + $result = $this->FunktionModel->load(); + + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + public function getOrgHeads() { $result = $this->OrganisationseinheitModel->getHeads(); @@ -97,7 +107,7 @@ class Funktionen extends FHCAPI_Controller WHERE bf.uid = ? ORDER BY - f.beschreibung, bf.datum_von ASC"; + bf.datum_von, bf.datum_von ASC"; $benutzerfunktionen = $this->BenutzerfunktionModel->execReadOnlyQuery($sql, array($uid)); $data = $this->getDataOrTerminateWithError($benutzerfunktionen); @@ -106,14 +116,35 @@ class Funktionen extends FHCAPI_Controller } /* - * return list of child orgets for a given company orget_kurzbz + * returns list of all organisation units * as key value list to be used in select or autocomplete */ - public function getOrgetsForCompany($companyOrgetkurzbz=null) + public function getAllOrgUnits() { $sql = " SELECT - oe.oe_kurzbz, + oe.oe_kurzbz, oe.aktiv, + '[' || COALESCE(oet.bezeichnung, oet.organisationseinheittyp_kurzbz) || + '] ' || COALESCE(oe.bezeichnung, oe.oe_kurzbz) AS label + FROM public.tbl_organisationseinheit oe + JOIN public.tbl_organisationseinheittyp oet ON oe.organisationseinheittyp_kurzbz = oet.organisationseinheittyp_kurzbz + ORDER BY oet.bezeichnung ASC, oe.bezeichnung ASC"; + + $result = $this->OrganisationseinheitModel->execReadOnlyQuery($sql); + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + + /* + * 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) + { + $sql = " + SELECT + oe.oe_kurzbz, oe.aktiv, '[' || COALESCE(oet.bezeichnung, oet.organisationseinheittyp_kurzbz) || '] ' || COALESCE(oe.bezeichnung, oe.oe_kurzbz) AS label FROM ( @@ -139,25 +170,6 @@ class Funktionen extends FHCAPI_Controller $this->terminateWithSuccess($data); } - public function searchOes($companyOrgetKurzbz, $searchString = null) - { - $result = $this->OrganisationseinheitModel->getAutocompleteSuggestionsWithCompany($companyOrgetKurzbz, $searchString); - if (isError($result)) { - $this->terminateWithError($result, self::ERROR_TYPE_GENERAL); - } - $this->terminateWithSuccess($result ?: []); - } - - public function searchFunctions($searchString = null) - { - - $result = $this->FunktionModel->getAutocompleteSuggestions($searchString); - if (isError($result)) { - $this->terminateWithError($result, self::ERROR_TYPE_GENERAL); - } - $this->terminateWithSuccess($result ?: []); - } - public function loadFunction($benutzerfunktion_id) { $this->BenutzerfunktionModel->addSelect("*"); diff --git a/application/models/organisation/Organisationseinheit_model.php b/application/models/organisation/Organisationseinheit_model.php index b146ee7e5..ba91964f6 100644 --- a/application/models/organisation/Organisationseinheit_model.php +++ b/application/models/organisation/Organisationseinheit_model.php @@ -191,7 +191,7 @@ class Organisationseinheit_model extends DB_Model /** * @param string $oe_kurzbz - * + * * @return stdClass */ public function getWithType($oe_kurzbz) @@ -202,78 +202,6 @@ class Organisationseinheit_model extends DB_Model return $this->load($oe_kurzbz); } - /** - * Get OEs by eventQuery string. Use with autocomplete event queries. - * @param $eventQuery String - * @return array - */ - public function getAutocompleteSuggestions($eventQuery) - { - $this->addSelect('oe_kurzbz'); - $this->addSelect('organisationseinheittyp_kurzbz, oe_kurzbz, bezeichnung, aktiv, lehre'); - $this->addOrder('organisationseinheittyp_kurzbz, bezeichnung'); - - return $this->loadWhere(" - oe_kurzbz ILIKE '%". $this->escapeLike($eventQuery). "%' - "); - } - - /** - * Get OEs by eventQuery string and companyOrgetKurzbz - * Use with autocomplete event queries in Function Component - * @param $searchString String - * @param $companyOrgetKurzbz String oe_kurzbz of the company (gst vs gmbh) - * @return array - */ - public function getAutocompleteSuggestionsWithCompany($companyOrgetKurzbz, $searchString) - { - $sql = " - WITH RECURSIVE oes(oe_kurzbz, oe_parent_kurzbz) AS ( - SELECT oe_kurzbz, oe_parent_kurzbz - FROM public.tbl_organisationseinheit - WHERE oe_kurzbz = ? - UNION ALL - SELECT o.oe_kurzbz, o.oe_parent_kurzbz - FROM public.tbl_organisationseinheit o - INNER JOIN oes ON o.oe_parent_kurzbz = oes.oe_kurzbz - ) - SELECT - oe.oe_kurzbz, oe.aktiv, - '[' || COALESCE(oet.bezeichnung, oet.organisationseinheittyp_kurzbz) || - '] ' || COALESCE(oe.bezeichnung, oe.oe_kurzbz) AS label - FROM ( - SELECT oe_kurzbz FROM oes GROUP BY oe_kurzbz - ) c - JOIN public.tbl_organisationseinheit oe ON oe.oe_kurzbz = c.oe_kurzbz - JOIN public.tbl_organisationseinheittyp oet ON oe.organisationseinheittyp_kurzbz = oet.organisationseinheittyp_kurzbz - "; - - $params = [$companyOrgetKurzbz]; - - if (!empty($searchString)) { - $escaped = $this->escapeLike($searchString); - $ilike = '%' . $escaped . '%'; - - $sql .= " - WHERE - oe.oe_kurzbz ILIKE ? OR - oe.bezeichnung ILIKE ? OR - oe.organisationseinheittyp_kurzbz ILIKE ? - "; - - $params[] = $ilike; - $params[] = $ilike; - $params[] = $ilike; - } - - $sql .= " ORDER BY oet.bezeichnung ASC, oe.bezeichnung ASC"; - - $result = $this->execQuery($sql, $params); - - return $result; - } - - /** * get highest organisation units */ diff --git a/application/models/ressource/Funktion_model.php b/application/models/ressource/Funktion_model.php index 60965b438..43908167c 100644 --- a/application/models/ressource/Funktion_model.php +++ b/application/models/ressource/Funktion_model.php @@ -11,26 +11,4 @@ class Funktion_model extends DB_Model $this->dbTable = 'public.tbl_funktion'; $this->pk = 'funktion_kurzbz'; } - - /** - * Get Functions by eventQuery string. Use with autocomplete event queries in Function Component - * @param $eventQuery String - * @return array - */ - public function getAutocompleteSuggestions($eventQuery) - { - $this->addSelect('funktion_kurzbz, beschreibung, aktiv'); - $this->addSelect("beschreibung AS label"); - $this->addOrder('beschreibung', 'ASC'); - - if($eventQuery === null) - { - return $this->load(); - } - - return $this->loadWhere(" - funktion_kurzbz ILIKE '%". $this->escapeLike($eventQuery). "%' - OR beschreibung ILIKE '%". $this->escapeLike($eventQuery). "%' - "); - } } diff --git a/public/js/api/factory/functions.js b/public/js/api/factory/functions.js index ac717050a..ef0be43cb 100644 --- a/public/js/api/factory/functions.js +++ b/public/js/api/factory/functions.js @@ -15,17 +15,6 @@ * along with this program. If not, see . */ export default { - getContractFunctions(filter) { - var url = 'api/frontend/v1/funktionen/Funktionen/getContractFunctions'; - if( typeof filter !== 'undefined' && filter !== null ) { - url = url + '/' + filter; - } - return { - method: 'get', - url, - }; - - }, getOrgHeads() { var url = 'api/frontend/v1/funktionen/Funktionen/getOrgHeads'; return { @@ -41,30 +30,13 @@ export default { url, }; }, - loadAllOes(filterStudent) { - var url = 'api/frontend/v1/funktionen/Funktionen/loadAllOes' - + '/' + filterStudent; + getAllOrgUnits(filterStudent) { + var url = 'api/frontend/v1/funktionen/Funktionen/getAllOrgUnits'; return { method: 'get', url, }; }, - getCompanyByOrget(orget) { - var url = 'api/frontend/v1/funktionen/Funktionen/getCompanyByOrget' - + '/' + orget; - return { - method: 'get', - url, - }; - }, - getCurrentFunctions(mitarbeiter_uid, unternehmen) { - var url = 'api/frontend/v1/funktionen/Funktionen/getCurrentFunctions' - + '/' + mitarbeiter_uid + '/' + unternehmen; - return { - method: 'get', - url, - }; - } , getAllUserFunctions(mitarbeiter_uid) { var url = 'api/frontend/v1/funktionen/Funktionen/getAllUserFunctions' + '/' + mitarbeiter_uid; @@ -114,11 +86,4 @@ export default { url: 'api/frontend/v1/funktionen/Funktionen/searchOes/' + head + '/' + searchString }; }, - getFunctions(searchString) { - return { - method: 'get', - url: 'api/frontend/v1/funktionen/Funktionen/searchFunctions/' + searchString - }; - } - }; \ No newline at end of file diff --git a/public/js/components/Funktionen/Funktionen.js b/public/js/components/Funktionen/Funktionen.js index 7bbd26702..4fdf5b6e4 100644 --- a/public/js/components/Funktionen/Funktionen.js +++ b/public/js/components/Funktionen/Funktionen.js @@ -43,10 +43,8 @@ export default { { title: "dienstverhaeltnis_unternehmen", field: "dienstverhaeltnis_unternehmen", - width: 50, headerFilter: "list", headerFilterParams: {valuesLookup: true, autocomplete: true, sort: "asc"}, - formatter: this.companyLinkFormatter }, { title: "funktion_beschreibung", field: "funktion_beschreibung", headerFilter: "list", @@ -100,7 +98,6 @@ export default { let container = document.createElement('div'); container.className = "d-flex gap-2"; - if( cell.getRow().getData().dienstverhaeltnis_unternehmen === null ) { let button = document.createElement('button'); button.className = 'btn btn-outline-secondary btn-action'; @@ -160,18 +157,19 @@ export default { const column = cm.getColumnByField('dienstverhaeltnis_unternehmen'); const companyDv = { title: this.$p.t('person', 'dv_unternehmen'), - width: 100, + width: 140, visible: this.showDvCompany, + formatter: this.companyLinkFormatter }; column.component.updateDefinition(companyDv); cm.getColumnByField('funktion_beschreibung').component.updateDefinition({ title: this.$p.t('person', 'zuordnung_taetigkeit'), - width: 100 + width: 140 }); cm.getColumnByField('funktion_oebezeichnung').component.updateDefinition({ title: this.$p.t('lehre', 'organisationseinheit'), - width: 100 + width: 140 }); cm.getColumnByField('wochenstunden').component.updateDefinition({ title: this.$p.t('person', 'wochenstunden') @@ -192,18 +190,25 @@ export default { cm.getColumnByField('bezeichnung').component.updateDefinition({ title: this.$p.t('ui', 'bezeichnung'), - width: 100 + width: 140 }); } } ], - isFilterSet: false, + isFilterSet: true, listOrgHeads: [], - listOrgUnits: [], + listOrgUnits: [], //Old + listAllOrgUnits: [], + listOrgUnits_GST: [], + listOrgUnits_GMBH: [], formData: { head: 'gst', - oe_kurzbz: '' + oe_kurzbz: '', + funktion_kurzbz: null, + label:'', + //funktion_label: '', + funktion: null, }, statusNew: true, listAllFunctions: [], @@ -213,7 +218,17 @@ export default { }, filteredOes: [], filteredFunctions: [], - newBtnStyle: '' + newBtnStyle: '', + selectedFunction: null, + selectedOe: null + } + }, + watch: { + selectedFunction(newVal) { + this.formData.funktion_kurzbz = newVal?.funktion_kurzbz || ''; + }, + selectedOe(newVal) { + this.formData.oe_kurzbz = newVal?.oe_kurzbz || ''; } }, methods: { @@ -229,6 +244,7 @@ export default { actionNewFunction(){ this.resetModal(); this.statusNew = true; + this.formData.datum_von = new Date(); this.$refs.functionModal.show(); }, actionCopyFunction(benutzerfunktion_id) { @@ -249,7 +265,15 @@ export default { actionEditFunction(benutzerfunktion_id) { this.resetModal(); this.statusNew = false; - this.loadFunction(benutzerfunktion_id); + this.loadFunction(benutzerfunktion_id).then(() => { + //set selectedFunction and selectedOd to enable viewing label in primevue autocomplete fields + this.selectedFunction = this.listAllFunctions.find( + item => item.funktion_kurzbz === this.formData.funktion_kurzbz + ); + this.selectedOe = this.listAllOrgUnits.find( + item => item.oe_kurzbz === this.formData.oe_kurzbz + ); + }); this.$refs.functionModal.show(); }, addFunction() { @@ -304,14 +328,6 @@ export default { this.reload(); }); }, - getOrgetsForCompany(){ - return this.$api - .call(ApiCoreFunktion.getOrgetsForCompany(this.formData.head)) - .then(result => { - this.listOrgUnits = result.data; - }) - .catch(this.$fhcAlert.handleSystemError); - }, hideModal(modalRef) { this.$refs[modalRef].hide(); }, @@ -324,32 +340,30 @@ export default { this.formData.oe_kurzbz = ''; this.formData.funktion_kurzbz = ''; }, - searchOe(event) { - if (this.abortController.oes) { - this.abortController.oes.abort(); - } - - this.abortController.oes = new AbortController(); - - return this.$api - .call(ApiCoreFunktion.getOes(this.formData.head, event.query)) - .then(result => { - this.filteredOes = result.data.retval; - }); + filterFunctions(event) { + const query = event.query.toLowerCase(); + this.filteredFunctions = this.listAllFunctions.filter(item => + item.label.toLowerCase().includes(query) + ) }, - searchFunctions(event) { - if (this.abortController.functions) { - this.abortController.functions.abort(); + filterOes(event) { + const query = event.query.toLowerCase(); + + if(!this.formData.head) + this.$fhcAlert.alertError(this.$p.t('ui', 'bitteUnternehmenWaehlen')); + + if(this.formData.head == 'gst') { + this.filteredOes = this.listOrgUnits_GST.filter(item => + item.label.toLowerCase().includes(query) + ); + } + if(this.formData.head == 'gmbh') { + this.filteredOes = this.listOrgUnits_GMBH.filter(item => + item.label.toLowerCase().includes(query) + ); } - - this.abortController.functions = new AbortController(); - - return this.$api - .call(ApiCoreFunktion.getFunctions(event.query)) - .then(result => { - this.filteredFunctions = result.data.retval; - }); }, + styleNewButton(){ if(this.stylePv21) { this.newBtnStyle = "btn-sm"; @@ -370,12 +384,38 @@ export default { }) .catch(this.$fhcAlert.handleSystemError); + this.$api + .call(ApiCoreFunktion.getAllFunctions()) + .then(result => { + this.listAllFunctions = result.data; + }) + .catch(this.$fhcAlert.handleSystemError); + this.$api + .call(ApiCoreFunktion.getAllOrgUnits()) + .then(result => { + this.listAllOrgUnits = result.data; + }) + .catch(this.$fhcAlert.handleSystemError); + this.$api + .call(ApiCoreFunktion.getOrgetsForCompany('gst')) + .then(result => { + this.listOrgUnits_GST = result.data; + }) + .catch(this.$fhcAlert.handleSystemError); + this.$api + .call(ApiCoreFunktion.getOrgetsForCompany('gmbh')) + .then(result => { + this.listOrgUnits_GMBH = result.data; + }) + .catch(this.$fhcAlert.handleSystemError); + this.styleNewButton(); + }, template: `
- -
+ +
+ - + - + @@ -465,11 +507,13 @@ export default { type="autocomplete" :label="$p.t('lehre/organisationseinheit') + ' *'" name="oe_kurzbz" - v-model="formData.oe_kurzbz" + v-model="selectedOe" + forceSelection optionLabel="label" + optionValue="oe_kurzbz" :suggestions="filteredOes" dropdown - @complete="searchOe" + @complete="filterOes" >