copy Function files to component

This commit is contained in:
ma0068
2025-05-09 09:11:33 +02:00
parent 7030d5b822
commit 233e768aad
13 changed files with 1394 additions and 1 deletions
@@ -0,0 +1,239 @@
<?php
defined('BASEPATH') || exit('No direct script access allowed');
class FunctionsAPI extends Auth_Controller
{
const DEFAULT_PERMISSION = 'basis/mitarbeiter:rw';
const HANDYVERWALTUNG_PERMISSION = 'extension/pv21_handyverwaltung:rw';
public function __construct() {
//TODO(Manu) check permissions
parent::__construct(array(
'getAllFunctions' => 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 = <<<EOSQL
SELECT
funktion_kurzbz AS value, beschreibung AS label
FROM
public.tbl_funktion
WHERE
aktiv = true
ORDER BY beschreibung ASC
EOSQL;
$fkts = $this->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 = <<<EOSQL
SELECT
funktion_kurzbz AS value, beschreibung AS label
FROM
public.tbl_funktion
WHERE
aktiv = true AND vertragsrelevant = true
{$addwhere}
ORDER BY beschreibung ASC
EOSQL;
$fkts = $this->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 <uid>');
}
if( empty($companyOrgetkurzbz) )
{
$this->outputJsonError('Missing Parameter <companyOrgetkurzbz>');
}
$sql = <<<EOSQL
SELECT
bf.benutzerfunktion_id AS value, 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
EOSQL;
$benutzerfunktionen = $this->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 <uid>');
}
$sql = <<<EOSQL
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
EOSQL;
$benutzerfunktionen = $this->BenutzerfunktionModel->execReadOnlyQuery($sql, array($uid));
if( hasData($benutzerfunktionen) )
{
$this->outputJson($benutzerfunktionen);
return;
}
else
{
$this->outputJsonError('no benutzerfunktionen found for uid ' . $uid);
return;
}
}
}
@@ -0,0 +1,153 @@
<?php
defined('BASEPATH') || exit('No direct script access allowed');
class OrgAPI extends Auth_Controller
{
const DEFAULT_PERMISSION = 'basis/mitarbeiter:rw';
const HANDYVERWALTUNG_PERMISSION = 'extension/pv21_handyverwaltung:rw';
public function __construct() {
parent::__construct(array(
'getOrgHeads' => 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 = <<<EOSQL
WITH RECURSIVE unternehmen 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 AS o
INNER JOIN unternehmen u ON u.oe_parent_kurzbz=o.oe_kurzbz
)
SELECT *
FROM unternehmen
WHERE oe_parent_kurzbz is null;
EOSQL;
$childorgets = $this->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 <companyOrgetkurzbz>');
return;
}
$sql = <<<EOSQL
SELECT
oe.oe_kurzbz AS value,
'[' || 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
EOSQL;
$childorgets = $this->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;
}
}
}
@@ -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;
});
@@ -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'
],
+22
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
import person from "./funktionen/person.js";
export default {
person
};
@@ -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 <https://www.gnu.org/licenses/>.
*/
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,
};
}
};
@@ -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 ? `<a href="${url}">` + cell.getValue() + '</a>' : '';
}
// 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: `
<div class="row" v-if="readonlyMode === false">
<div class="toast-container position-absolute top-0 end-0 pt-4 pe-2">
<Toast ref="toastRef">
<template #body><h4>{{ t('person','funktionGespeichert') }}</h4></template>
</Toast>
</div>
<div class="toast-container position-absolute top-0 end-0 pt-4 pe-2">
<Toast ref="deleteToastRef">
<template #body><h4>{{ t('person', 'funktionGeloescht') }}</h4></template>
</Toast>
</div>
</div>
<div class="row pt-md-4">
<div class="col">
<div class="card">
<div class="card-header">
<div class="h5"><h5>{{ t('person','funktionen') }}</h5></div>
</div>
<div class="card-body">
<div class="d-grid d-md-flex justify-content-between pt-2 pb-3" v-if="readonlyMode === false">
<button type="button" class="btn btn-sm btn-primary me-3" @click="showAddModal()">
<i class="fa fa-plus"></i> {{ t('person','funktion') }}
</button>
<div class="form-check">
<input class="form-check-input" type="checkbox" role="switch" id="aktivChecked" v-model="aktivChecked">
<label class="form-check-label" for="aktivChecked">Nur aktive anzeigen</label>
</div>
</div>
<!-- TABULATOR -->
<div ref="table" class="fhc-tabulator"></div>
</div>
</div>
</div>
</div>
<!-- detail modal -->
<Modal :title="t('person','funktion')" ref="modalRef" v-if="readonlyMode === false">
<template #body>
<form class="row g-3" ref="jobFunctionFrm">
<div class="col-md-8">
<label class="form-label">{{ t('core','unternehmen') }}</label><br>
<org-chooser @org-selected="unternehmenSelectedHandler" :oe="unternehmen" class="form-select form-select-sm"></org-chooser>
</div>
<div class="col-md-4">
</div>
<!-- -->
<div class="col-md-8">
<label class="required form-label">{{ t('lehre','organisationseinheit') }}</label><br>
<select id="oe_kurzbz" v-model="currentValue.oe_kurzbz"
@blur="frmState.orgetBlurred = true"
class="form-select form-select-sm" aria-label=".form-select-sm "
:class="{ 'form-control-plaintext': readonly, 'form-control': !readonly, 'is-invalid': !notEmpty(currentValue.oe_kurzbz) && frmState.orgetBlurred}"
>
<option v-for="(item, index) in orgUnitList" :value="item.value">
{{ item.label }}
</option>
</select>
</div>
<div class="col-md-4">
</div>
<!-- -->
<div class="col-md-8">
<label for="funktion_kurzbz" class="required form-label">{{ t('person','funktion') }}</label><br>
<select id="_kurzbz" v-model="currentValue.funktion_kurzbz"
@blur="frmState.funktionBlurred = true"
class="form-select form-select-sm" aria-label=".form-select-sm "
:class="{ 'form-control-plaintext': readonly, 'form-control': !readonly, 'is-invalid': !notEmpty(currentValue.funktion_kurzbz) && frmState.funktionBlurred}"
>
<option v-for="(item, index) in jobfunctionDefList" :value="item.value">
{{ item.label }}
</option>
</select>
</div>
<div class="col-md-4"></div>
<!-- -->
<div class="col-md-2">
<label for="uid" class="form-label">{{ t('person','wochenstunden') }}</label>
<input type="number" :readonly="readonly" class="form-control-sm" :class="{ 'form-control-plaintext': readonly, 'form-control': !readonly}" id="bank" v-model="currentValue.wochenstunden">
</div>
<!-- -->
<div class="col-md-3">
<label for="beginn" class="required form-label">{{ t('ui','from') }}</label>
<datepicker id="beginn"
:teleport="true"
@blur="frmState.beginnBlurred = true"
:input-class-name="(!notEmpty(currentValue.datum_von) && frmState.beginnBlurred) ? 'dp-invalid-input' : ''"
v-model="currentValue.datum_von"
v-bind:enable-time-picker="false"
text-input
locale="de"
format="dd.MM.yyyy"
auto-apply
model-type="yyyy-MM-dd"></datepicker>
</div>
<div class="col-md-3">
<label for="ende" class="form-label">{{ t('global','bis') }}</label>
<datepicker id="ende"
:teleport="true"
v-model="currentValue.datum_bis"
v-bind:enable-time-picker="false"
text-input
locale="de"
format="dd.MM.yyyy"
auto-apply
model-type="yyyy-MM-dd"></datepicker>
</div>
<div class="col-md-3">
</div>
<!-- -->
<div class="col-md-8">
<label for="bezeichnung" class="form-label">{{ t('ui','bezeichnung') }}</label>
<input type="text" :readonly="readonly" class="form-control-sm" :class="{ 'form-control-plaintext': readonly, 'form-control': !readonly}" id="bezeichnung" v-model="currentValue.bezeichnung">
</div>
<div class="col-md-4">
</div>
<!-- changes -->
<div class="col-8">
<div class="modificationdate">{{ currentValue.insertamum }}/{{ currentValue.insertvon }}, {{ currentValue.updateamum }}/{{ currentValue.updatevon }} [id={{ currentValue.benutzerfunktion_id }}]</div>
</div>
</form>
</template>
<template #footer>
<button type="button" class="btn btn-primary" @click="okHandler()" >
{{ t('ui','speichern') }}
</button>
</template>
</Modal>
<ModalDialog :title="t('global','warnung')" ref="dialogRef" v-if="readonlyMode === false">
<template #body>
{{ t('person','funktionNochNichtGespeichert') }}
</template>
</ModalDialog>
<ModalDialog :title="t('global','warnung')" ref="confirmDeleteRef" v-if="readonlyMode === false">
<template #body>
{{ t('person','funktion') }} '{{ currentValue?.funktion_kurzbz }} ({{ currentValue?.datum_von }}-{{ currentValue?.datum_bis }})' {{ t('person', 'wirklichLoeschen') }}?
</template>
</ModalDialog>
`
}
export default Funktionen;
+48
View File
@@ -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:`
<div class="modal fade " id="customModal" tabindex="-1" aria-labelledby=""
aria-hidden="true" ref="modalEle">
<div class="modal-dialog modal-lg" :class="(!noscroll) ? 'modal-dialog-scrollable' : ''">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="customModalLabel">{{ title }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<slot name="body" />
</div>
<div class="modal-footer">
<slot name="footer"></slot>
<!-- button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
OK
</button-->
</div>
</div>
</div>
</div>`
};
@@ -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:`
<div class="modal fade " id="customModalDialog" tabindex="-1" aria-labelledby=""
aria-hidden="true" ref="modalConfirmEle">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="customModalDialogLabel">{{ title }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<slot name="body" />
</div>
<div class="modal-footer">
<slot name="footer"></slot>
<button type="button" class="btn btn-secondary" @click="cancel" data-bs-dismiss="modal">
{{ t('ui','abbrechen') }}
</button>
<button type="button" class="btn btn-primary" @click="ok" data-bs-dismiss="modal">
{{ t('ui','ok') }}
</button>
</div>
</div>
</div>
</div>`
};
@@ -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: `
<select id="orgHeadChooser" v-model="selected" @change="orgSelected" class="form-control-sm" aria-label=".form-select-sm " >
<option v-for="(item, index) in orgList" :value="item.oe_kurzbz" :key="item.oe_kurzbz">
{{ item.bezeichnung }}
</option>
</select>
`
}
+47
View File
@@ -0,0 +1,47 @@
export const Toast = {
name: 'Toast',
props: {
title: {
text: String,
default: "<<Text goes here>>",
},
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: `
<div class="toast align-items-center text-white border-0 " :class="backgroundColor" role="alert" aria-live="assertive" aria-atomic="true" ref="toastEle">
<div class="d-flex">
<div class="toast-body">
<slot name="body"></slot>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>`
}
@@ -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: `
<div class="stv-details-functions h-100 d-flex flex-column">
<table-functions
ref="tbl_functions"
:student="modelValue">
</table-functions>
<person-functions
:readonlyMode="false"
:personID="modelValue.person_id"
:personUID="modelValue.uid"
>
</person-functions>
</div>`
};
+24 -1
View File
@@ -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
);