mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-07-21 17:02:19 +00:00
Merge branch 'master' into feature-25999/C4_cleanup
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
.searchbar_results_scroller {
|
||||
@@ -109,3 +110,7 @@
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE/Edge */
|
||||
}
|
||||
|
||||
.searchbar_inaktiv {
|
||||
opacity: .6;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
margin-right: 5px;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
font-size: .75em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.tag:hover
|
||||
{
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.tag_rot {
|
||||
background-color: #ff0000ff;
|
||||
}
|
||||
|
||||
.tag_gelb {
|
||||
background-color: #ffff00ff;
|
||||
color: black;
|
||||
}
|
||||
.tag_gruen {
|
||||
background-color: #008000ff;
|
||||
}
|
||||
|
||||
.tag_rosa {
|
||||
background-color: #FFC1C1;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.tag_aprikose {
|
||||
background-color: #4f7596;
|
||||
}
|
||||
|
||||
.tag_pfirsich {
|
||||
background-color: #a15f95;
|
||||
}
|
||||
|
||||
.tag_orange {
|
||||
background-color: #ffA500ff;
|
||||
}
|
||||
|
||||
|
||||
.tag_braun {
|
||||
background-color: #6d4c41;
|
||||
}
|
||||
|
||||
.tag_blau {
|
||||
background-color: #508498;
|
||||
}
|
||||
|
||||
.tag_lavendel {
|
||||
background-color: #C7A3FF;
|
||||
}
|
||||
|
||||
.tag_limette {
|
||||
background-color: #D3FFCE;
|
||||
}
|
||||
|
||||
.tag_done {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.display_all {
|
||||
background-color: darkgrey !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dropdown_list {
|
||||
list-style: none;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
padding: 10px 10px 10px;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.plus_button_container.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.modificationdate {
|
||||
font-style: italic;
|
||||
font-size: 0.7em;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ const app = Vue.createApp({
|
||||
calcheightonly: true,
|
||||
types: [
|
||||
"mitarbeiter",
|
||||
"student",
|
||||
"raum",
|
||||
"organisationunit"
|
||||
],
|
||||
@@ -33,6 +34,17 @@ const app = Vue.createApp({
|
||||
},
|
||||
childactions: []
|
||||
},
|
||||
student: {
|
||||
defaultaction: {
|
||||
type: "link",
|
||||
action: function (data) {
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router +
|
||||
"/Cis/Profil/View/" + data.uid;
|
||||
|
||||
}
|
||||
},
|
||||
childactions: []
|
||||
},
|
||||
raum: {
|
||||
defaultaction: {
|
||||
type: "link",
|
||||
|
||||
@@ -70,7 +70,7 @@ export default {
|
||||
cssclass: "position-relative",
|
||||
calcheightonly: true,
|
||||
types: [
|
||||
"student",
|
||||
"studentStv",
|
||||
"prestudent"
|
||||
],
|
||||
actions: {
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import CoreForm from '../Form/Form.js';
|
||||
import FormInput from "../Form/Input.js";
|
||||
import BsModal from '../Bootstrap/Modal.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CoreForm,
|
||||
FormInput,
|
||||
BsModal,
|
||||
},
|
||||
emits: [
|
||||
'added',
|
||||
'updated',
|
||||
'deleted',
|
||||
],
|
||||
props: {
|
||||
endpoint: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
zuordnung_typ: String,
|
||||
savepoint: {},
|
||||
values: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
confirmLimit: {
|
||||
type: Number,
|
||||
default: 20
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showList: false,
|
||||
selectedTagId: null,
|
||||
tagData: {
|
||||
beschreibung: "",
|
||||
tag_typ_kurzbz: "",
|
||||
notiz: "",
|
||||
style: "",
|
||||
zuordnung_typ: "",
|
||||
id: "",
|
||||
insertamum: "",
|
||||
insertvon: "",
|
||||
updateamum: "",
|
||||
updatevon: "",
|
||||
response: ""
|
||||
},
|
||||
mode: "create"
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.init();
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
init() {
|
||||
if (!this.endpoint)
|
||||
return;
|
||||
this.endpoint.getTags()
|
||||
.then(response => response.data)
|
||||
.then(response => {
|
||||
this.tags = response
|
||||
})
|
||||
},
|
||||
hideList() {
|
||||
this.showList = false;
|
||||
},
|
||||
async editTag(tag_id) {
|
||||
let getData = {
|
||||
'id': tag_id
|
||||
};
|
||||
|
||||
this.endpoint.getTag(getData)
|
||||
.then(result => result.data)
|
||||
.then(result => this.openModal(result))
|
||||
},
|
||||
openModal(item = null)
|
||||
{
|
||||
this.tagData.beschreibung = item.bezeichnung;
|
||||
this.tagData.tag_typ_kurzbz = item.tag_typ_kurzbz;
|
||||
this.tagData.style = item.style;
|
||||
this.tagData.zuordnung_typ = this.zuordnung_typ;
|
||||
this.tagData.done = item.done;
|
||||
this.tagData.insertamum = item.insertamum;
|
||||
this.tagData.updateamum = item.updateamum;
|
||||
this.tagData.updatevon = item.updatevon;
|
||||
this.tagData.insertvon = item.insertvon;
|
||||
|
||||
if (item && item.notiz_id)
|
||||
{
|
||||
this.selectedTagId = item.notiz_id;
|
||||
this.tagData.notiz = item.text;
|
||||
this.mode = "edit";
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selectedTagId = null;
|
||||
this.tagData.notiz = "";
|
||||
this.mode = "create";
|
||||
}
|
||||
|
||||
if (this.mode === "create" && item.tag)
|
||||
this.saveTag()
|
||||
else
|
||||
this.$refs.tagModal.show();
|
||||
},
|
||||
async saveTag()
|
||||
{
|
||||
let postData = {
|
||||
tag_typ_kurzbz: this.tagData.tag_typ_kurzbz,
|
||||
notiz: this.tagData.notiz,
|
||||
zuordnung_typ: this.tagData.zuordnung_typ,
|
||||
values: this.values
|
||||
}
|
||||
|
||||
if (this.mode === "edit")
|
||||
{
|
||||
postData.id = this.selectedTagId;
|
||||
this.tagData.id = this.selectedTagId;
|
||||
this.endpoint.updateTag(postData);
|
||||
this.$emit("updated", this.tagData);
|
||||
this.$refs.tagModal.hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.$fhcAlert && postData.values.length >= this.confirmLimit)
|
||||
{
|
||||
if (await this.$fhcAlert.confirm({message: `Der Tag wird für ${postData.values.length} Einträge gesetzt. Sind Sie sicher?`}) === false)
|
||||
return;
|
||||
}
|
||||
|
||||
this.endpoint.addTag(postData)
|
||||
.then(response => response.data)
|
||||
.then(response => {
|
||||
if (typeof response === 'number') {
|
||||
this.tagData.id = response;
|
||||
} else {
|
||||
this.tagData.response = response;
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.$emit("added", this.tagData);
|
||||
})
|
||||
.then(() => {
|
||||
this.$refs.tagModal.hide();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
async doneTag()
|
||||
{
|
||||
this.tagData.id = this.selectedTagId;
|
||||
this.tagData.done = !this.tagData.done;
|
||||
|
||||
let postData = {
|
||||
id: this.selectedTagId,
|
||||
done: !this.tagData.done
|
||||
}
|
||||
this.endpoint.doneTag(postData)
|
||||
this.$emit("updated", this.tagData);
|
||||
this.$refs.tagModal.hide();
|
||||
},
|
||||
async deleteTag()
|
||||
{
|
||||
let postData = {
|
||||
id: this.selectedTagId
|
||||
}
|
||||
this.endpoint.deleteTag(postData)
|
||||
this.$emit("deleted", this.selectedTagId)
|
||||
this.$refs.tagModal.hide();
|
||||
},
|
||||
reset() {
|
||||
this.tagData = {
|
||||
beschreibung: "",
|
||||
tag_typ_kurzbz: "",
|
||||
notiz: "",
|
||||
style: "",
|
||||
zuordnung_typ: "",
|
||||
id: "",
|
||||
done: false,
|
||||
insertamum: "",
|
||||
insertvon: "",
|
||||
updateamum: "",
|
||||
updatevon: "",
|
||||
response: ""
|
||||
};
|
||||
this.selectedTagId = null;
|
||||
this.mode = "create";
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="plus_button_container" @mouseleave="hideList">
|
||||
<button @mouseover="showList = true"
|
||||
:disabled="!values || values.length === 0"
|
||||
class="btn btn-sm">
|
||||
<i class="fa-solid fa-tag fa-xl"></i>
|
||||
</button>
|
||||
<ul v-if="showList" class="dropdown_list">
|
||||
<li v-for="(item, index) in tags" :key="index" @click="openModal(item)" :title="item.bezeichnung">
|
||||
{{ item.bezeichnung }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<bs-modal
|
||||
ref="tagModal"
|
||||
class="fade text-center"
|
||||
dialog-class="modal-dialog-centered"
|
||||
@hidden-bs-modal="reset"
|
||||
>
|
||||
<template #title>
|
||||
<span :class="['tag', tagData.style]">{{ tagData.beschreibung }}</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="col">
|
||||
<form-input
|
||||
v-model="tagData.notiz"
|
||||
type="textarea"
|
||||
field="notiz"
|
||||
placeholder="Notiz..."
|
||||
></form-input>
|
||||
<div class="modificationdate">angelegt von {{ tagData.insertvon }} am {{ tagData.insertamum }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="d-flex justify-content-between w-100">
|
||||
<div>
|
||||
<button
|
||||
v-if="mode === 'edit'"
|
||||
class="btn btn-success me-2"
|
||||
@click="doneTag"
|
||||
>
|
||||
{{ tagData.done ? 'Rückgängig' : 'Erledigt' }}
|
||||
</button>
|
||||
<button v-if="mode === 'edit'" class="btn btn-danger" @click="deleteTag">Löschen</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" @click="saveTag">
|
||||
{{ mode === "edit" ? $p.t('ui', 'bearbeiten') : $p.t('studierendenantrag', 'btn_create') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</bs-modal>`,
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import FilterConfig from './Filter/Config.js';
|
||||
import FilterColumns from './Filter/Columns.js';
|
||||
import TableDownload from './Table/Download.js';
|
||||
import collapseAutoClose from '../../directives/collapseAutoClose.js';
|
||||
import { defaultHeaderFilter } from '../../tabulator/filters/defaultHeaderFilter.js';
|
||||
|
||||
//
|
||||
const FILTER_COMPONENT_NEW_FILTER = 'Filter Component New Filter';
|
||||
@@ -72,7 +73,8 @@ export const CoreFilterCmpt = {
|
||||
uniqueId: String,
|
||||
// TODO soll im master kommen?
|
||||
idField: String,
|
||||
parentIdField: String
|
||||
parentIdField: String,
|
||||
countOnly: Boolean
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
@@ -206,6 +208,7 @@ export const CoreFilterCmpt = {
|
||||
movableColumns: true,
|
||||
columnDefaults:{
|
||||
tooltip: true,
|
||||
headerFilterFunc: defaultHeaderFilter,
|
||||
},
|
||||
placeholder,
|
||||
reactiveData: true,
|
||||
@@ -275,11 +278,11 @@ export const CoreFilterCmpt = {
|
||||
const cols = this.tabulator.getColumns();
|
||||
this.fields = cols.map(col => col.getField());
|
||||
this.selectedFields = cols.filter(col => col.isVisible()).map(col => col.getField());
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
updateTabulator() {
|
||||
if (this.tabulator) {
|
||||
@@ -610,7 +613,10 @@ export const CoreFilterCmpt = {
|
||||
<button v-if="reload" class="btn btn-outline-secondary" aria-label="Reload" @click="reloadTable">
|
||||
<span class="fa-solid fa-rotate-right" aria-hidden="true"></span>
|
||||
</button>
|
||||
<span v-if="$slots.actions && tabulatorHasSelector">Mit {{selectedData.length}} ausgewählten:</span>
|
||||
<span v-if="$slots.actions && tabulatorHasSelector">
|
||||
<span v-if="countOnly">{{ selectedData.length }} ausgewählt</span>
|
||||
<span v-else> Mit {{ selectedData.length }} ausgewählten:</span>
|
||||
</span>
|
||||
<slot name="actions" v-bind="{selected: tabulatorHasSelector ? selectedData : []}"></slot>
|
||||
<slot name="search"></slot>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
<div v-else-if="searchresult.length < 1">Es wurden keine Ergebnisse gefunden.</div>
|
||||
<template v-else="" v-for="res in searchresult">
|
||||
<person v-if="res.type === 'person'" :res="res" :actions="this.searchoptions.actions.person" @actionexecuted="this.hideresult"></person>
|
||||
<student v-else-if="res.type === 'student'" :res="res" :actions="this.searchoptions.actions.student" @actionexecuted="this.hideresult"></student>
|
||||
<student v-else-if="res.type === 'student' || res.type === 'studentStv'" :res="res" :actions="this.searchoptions.actions.student" @actionexecuted="this.hideresult"></student>
|
||||
<prestudent v-else-if="res.type === 'prestudent'" :res="res" :actions="this.searchoptions.actions.prestudent" @actionexecuted="this.hideresult"></prestudent>
|
||||
<employee v-else-if="res.type === 'mitarbeiter' || res.type === 'mitarbeiter_ohne_zuordnung'" :res="res" :actions="this.searchoptions.actions.employee" @actionexecuted="this.hideresult"></employee>
|
||||
<organisationunit v-else-if="res.type === 'organisationunit'" :res="res" :actions="this.searchoptions.actions.organisationunit" @actionexecuted="this.hideresult"></organisationunit>
|
||||
|
||||
@@ -9,14 +9,14 @@ export default {
|
||||
},
|
||||
emits: [ 'actionexecuted' ],
|
||||
template: `
|
||||
<div class="searchbar_result searchbar_student">
|
||||
<div class="searchbar_result searchbar_student" :class="(!res?.aktiv) ? 'searchbar_inaktiv' : ''">
|
||||
|
||||
<div class="searchbar_grid">
|
||||
<div class="searchbar_icon">
|
||||
<action :res="this.res" :action="this.actions.defaultaction" @actionexecuted="$emit('actionexecuted')">
|
||||
<img v-if="(typeof res.foto !== 'undefined') && (res.foto !== null)"
|
||||
:src="'data:image/jpeg;base64,' + res.foto"
|
||||
class="rounded-circle" height="100"/>
|
||||
:src="studentImage"
|
||||
class="rounded" style="max-height: 120px; max-width: 90px;" />
|
||||
<i v-else class="fas fa-user-circle fa-5x"></i>
|
||||
</action>
|
||||
</div>
|
||||
@@ -29,18 +29,25 @@ export default {
|
||||
<div class="mb-3"></div>
|
||||
|
||||
<div class="searchbar_table">
|
||||
|
||||
|
||||
<div class="searchbar_tablerow">
|
||||
<div class="searchbar_tablecell">Student_uid</div>
|
||||
<div class="searchbar_tablecell">Student UID</div>
|
||||
<div class="searchbar_tablecell">
|
||||
{{ res.uid }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="searchbar_tablerow">
|
||||
<div class="searchbar_tablecell">Person_id</div>
|
||||
<div class="searchbar_tablecell">Studiengang</div>
|
||||
<div class="searchbar_tablecell">
|
||||
{{ res.person_id }}
|
||||
{{ res.studiengang }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="searchbar_tablerow">
|
||||
<div class="searchbar_tablecell">Verband</div>
|
||||
<div class="searchbar_tablecell">
|
||||
{{ res.verband }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,6 +82,10 @@ export default {
|
||||
computed: {
|
||||
mailtourl: function() {
|
||||
return 'mailto:' + this.res.email;
|
||||
}
|
||||
},
|
||||
studentImage: function () {
|
||||
if (!this.res.foto) return;
|
||||
return 'data:image/jpeg;base64,'.concat(this.res.foto);
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
function parseFilterExpression(expression){
|
||||
const includeGroups = [];
|
||||
const excludeTerms = [];
|
||||
const comparisons = [];
|
||||
|
||||
const andParts = expression.split('&&').map(part => part.trim());
|
||||
|
||||
andParts.forEach(part => {
|
||||
const orTerms = part.split('||').map(p => p.trim());
|
||||
const orRegexes = [];
|
||||
|
||||
orTerms.forEach(term => {
|
||||
|
||||
const comparisonMatch = term.match(/^(<=|>=|<|>|=|!=)\s*(\d+)$/);
|
||||
|
||||
if (comparisonMatch)
|
||||
{
|
||||
const operator = comparisonMatch[1];
|
||||
const number = parseFloat(comparisonMatch[2]);
|
||||
|
||||
comparisons.push({ operator, number });
|
||||
}
|
||||
else if (term.startsWith('!'))
|
||||
{
|
||||
const excludeTerm = term.substring(1).trim().replace(/\*/g, '.*');
|
||||
excludeTerms.push(new RegExp(excludeTerm, 'i'));
|
||||
}
|
||||
else
|
||||
{
|
||||
const includeTerm = term.replace(/\*/g, '.*');
|
||||
orRegexes.push(new RegExp(includeTerm, 'i'));
|
||||
}
|
||||
});
|
||||
|
||||
if (orRegexes.length > 0)
|
||||
{
|
||||
includeGroups.push(orRegexes);
|
||||
}
|
||||
});
|
||||
|
||||
return { includeGroups, excludeTerms, comparisons };
|
||||
}
|
||||
|
||||
export function defaultHeaderFilter(headerValue, rowValue)
|
||||
{
|
||||
const { includeGroups, excludeTerms, comparisons } = parseFilterExpression(headerValue);
|
||||
|
||||
const includes = includeGroups.every(group =>
|
||||
group.some(regex => regex.test(rowValue))
|
||||
);
|
||||
|
||||
const excludes = excludeTerms.every(regex => !regex.test(rowValue));
|
||||
|
||||
const comparisonCheck = comparisons.every(({ operator, number }) => {
|
||||
let value = rowValue;
|
||||
|
||||
if (!isNaN(number))
|
||||
{
|
||||
value = parseFloat(rowValue);
|
||||
if (isNaN(value)) return false;
|
||||
}
|
||||
|
||||
switch (operator) {
|
||||
case '<':
|
||||
return value < number;
|
||||
case '>':
|
||||
return value > number;
|
||||
case '<=':
|
||||
return value <= number;
|
||||
case '>=':
|
||||
return value >= number;
|
||||
case '=':
|
||||
return value === number;
|
||||
case '!=':
|
||||
return value !== number;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return includes && excludes && comparisonCheck;
|
||||
}
|
||||
Reference in New Issue
Block a user