Merge branch 'master' into feature-25999/C4_TEST

This commit is contained in:
SimonGschnell
2024-05-08 11:06:34 +02:00
399 changed files with 39165 additions and 6786 deletions
+3
View File
@@ -1,3 +1,5 @@
import Phrasen from '../../plugin/Phrasen.js';
export default {
data: () => ({
modal: null
@@ -88,6 +90,7 @@ export default {
}
});
const wrapper = document.createElement("div");
instance.use(Phrasen); // TODO(chris): find a more dynamic way
instance.mount(wrapper);
document.body.appendChild(wrapper);
});
+3 -1
View File
@@ -99,8 +99,10 @@ export const CoreFetchCmpt = {
*
*/
errorHandler: function(error) {
if (error.response.data.retval)
if (error.response?.data?.retval)
this.setError(error.response.data.retval);
else if (error.data?.message)
this.setError(error.data.message);
else
this.setError(error.message);
},
+124
View File
@@ -0,0 +1,124 @@
import FhcFragment from "../Fragment.js";
export default {
components: {
FhcFragment
},
provide() {
return {
$registerToForm: component => {
if (this.inputs.indexOf(component) < 0)
this.inputs.push(component);
},
$clearValidationForName: this.clearValidationForName
};
},
props: {
tag: {
type: String,
default: 'form'
}
},
data() {
return {
inputs: []
}
},
computed: {
sortedInputs() {
return this.inputs.reduce((a,c) => {
let name = c.name || '_default';
if (!a[name])
a[name] = [];
a[name].push(c);
if (c.lcType == 'checkbox' && name.substr(-1) == ']' && name.indexOf('[')) {
name = name.substr(0, name.lastIndexOf('['));
if (!a[name])
a[name] = [];
a[name].push(c);
}
return a;
}, {});
},
factory() {
const factory = Object.create(Object.getPrototypeOf(this.$fhcApi.factory), Object.getOwnPropertyDescriptors(this.$fhcApi.factory));
factory.$fhcApi = {
get: this.get,
post: this.post,
_defaultErrorHandlers: this.$fhcApi._defaultErrorHandlers
};
return factory;
}
},
methods: {
get(...args) {
if (typeof args[0] == 'object' && args[0].clearValidation && args[0].setFeedback)
args[0] = this;
else
args.unshift(this);
return this.$fhcApi.get(...args);
},
post(...args) {
if (typeof args[0] == 'object' && args[0].clearValidation && args[0].setFeedback)
args[0] = this;
else
args.unshift(this);
return this.$fhcApi.post(...args);
},
_sendFeedbackToInput(inputs, feedback, valid) {
if (inputs.length) {
inputs.forEach(input => input.setFeedback(valid, feedback));
return false;
}
if (this.$fhcAlert) {
this.$fhcAlert[valid ? 'alertSuccess' : 'alertError'](feedback);
return false;
}
return true;
},
setFeedback(valid, feedback) {
if (Array.isArray(feedback)) {
let remaining = feedback.filter(fb =>
this._sendFeedbackToInput(
this.sortedInputs['_default'] || [],
fb,
valid
)
);
return remaining.length ? remaining : null;
}
if (typeof feedback === 'object') {
let remaining = Object.entries(feedback).filter(([name, fb]) =>
this._sendFeedbackToInput(
this.sortedInputs[name.split('.')[0] + name.split('.').slice(1).map(p => `[${p}]`).join("")] || this.sortedInputs['_default'] || [],
fb,
valid
)
);
return remaining.length ? Object.fromEntries(remaining) : null;
}
let remaining = this._sendFeedbackToInput(
this.sortedInputs['_default'] || [],
feedback,
valid
);
return remaining ? feedback : null;
},
clearValidation() {
this.inputs.forEach(input => input.clearValidation());
},
clearValidationForName(name) {
(this.sortedInputs[name.split('.')[0] + name.split('.').slice(1).map(p => `[${p}]`).join("")] || this.sortedInputs['_default'] || [])
.forEach(input => input.clearValidation());
}
},
template: `
<component :is="tag || 'FhcFragment'" v-bind="$attrs">
<slot></slot>
</component>`
}
+315
View File
@@ -0,0 +1,315 @@
import FhcFragment from "../Fragment.js";
let _uuid = {};
export default {
inheritAttrs: false,
components: {
FhcFragment
},
inject: {
registerToForm: {
from: '$registerToForm',
default: null
},
clearValidationForName: {
from: '$clearValidationForName',
default: null
}
},
props: {
bsFeedback: Boolean,
noAutoClass: Boolean,
noFeedback: Boolean,
inputGroup: Boolean,
type: String,
name: String,
containerClass: [String, Array, Object]
},
data() {
return {
valid: undefined,
feedback: [],
modelValueDummy: undefined
}
},
computed: {
hasContainer() {
if (!this.bsFeedback)
return true;
if (this.containerClass)
return true;
for (const prop in this.autoContainerClass)
if (Object.hasOwn(this.autoContainerClass, prop))
return true;
return false;
},
acc() {
if (!this.containerClass)
return {};
if (typeof this.containerClass === 'string' || this.containerClass instanceof String)
return this.containerClass.split(' ').reduce((a,c) => {a[c] = true; return a}, {});
if (Array.isArray(this.containerClass))
return this.containerClass.reduce((a,c) => {a[c] = true; return a}, {});
return this.containerClass;
},
autoContainerClass() {
if (this.noAutoClass)
return this.acc;
const acc = {...this.acc};
if (this.inputGroup)
acc['input-group-item'] = true;
if (this.lcType == 'radio' || this.lcType == 'checkbox')
acc['form-check'] = true;
if (this.inputGroup && acc['form-check']) {
acc['input-group-item'] = false;
acc['form-check'] = false;
acc['input-group-text'] = true;
}
return acc;
},
lcType() {
if (!this.type)
return 'text';
return this.type.toLowerCase();
},
tag() {
switch (this.lcType) {
case 'textarea':
case 'select':
return this.lcType;
case 'datepicker':
return 'VueDatePicker';
case 'autocomplete':
return 'PvAutocomplete';
case 'uploadimage':
return 'UploadImage';
case 'uploadfile':
case 'uploaddms':
return 'UploadDms';
default:
return 'input';
}
},
validationClass() {
const classes = [];
if (this.valid)
classes.push('is-valid');
else if (this.valid === false)
classes.push('is-invalid');
if (!this.noAutoClass) {
let c = this.$attrs.class ? this.$attrs.class.split(' ') : [];
switch (this.lcType) {
// TODO(chris): complete list!
case 'select':
if (!c.includes('form-select'))
classes.push('form-select');
break;
case 'range':
if (!c.includes('form-range'))
classes.push('form-range');
break;
case 'radio':
case 'checkbox':
// TODO(chris): maybe different handling?
if (!c.includes('form-check-input') && !c.includes('btn-check'))
classes.push('form-check-input');
break;
case 'color':
if (!c.includes('form-control-color'))
classes.push('form-control-color');
if (!c.includes('form-control'))
classes.push('form-control');
break;
case 'autocomplete':
case 'datepicker':
classes.push('p-0');
classes.push('border-0');
if (!c.includes('form-control'))
classes.push('form-control');
break;
case 'text':
case 'number':
case 'password':
case 'textarea':
if (!c.includes('form-control'))
classes.push('form-control');
break;
}
}
return classes;
},
feedbackClass() {
if (!this.feedback || this.feedback === true)
return '';
if (!this.bsFeedback)
return {
'valid-tooltip': this.valid === true,
'invalid-tooltip': this.valid === false
};
return {
'valid-feedback': this.valid === true,
'invalid-feedback': this.valid === false
};
},
modelValueCmp: {
get() {
if (this.$attrs.modelValue === undefined)
return this.modelValueDummy;
return this.$attrs.modelValue;
},
set(v) {
if (this.$attrs.modelValue === undefined)
this.modelValueDummy = v;
this.$emit('update:modelValue', v);
}
},
idCmp() {
let uuid = this.$attrs.id;
if (this.lcType == 'datepicker')
uuid = this.$attrs.uid;
if (!uuid && this.$attrs.label)
uuid = 'fhc-form-input';
if (!uuid)
return undefined;
if (this.lcType == 'datepicker')
uuid = 'dp-input-' + uuid;
if (_uuid[uuid] === undefined)
_uuid[uuid] = 0;
return uuid + '-' + (_uuid[uuid]++);
}
},
methods: {
clearValidation() {
this.valid = undefined;
this.feedback = [];
},
clearValidationForThisName() {
if (this.valid === undefined)
return;
if (this.clearValidationForName && this.name)
this.clearValidationForName(this.name);
else
this.clearValidation();
},
setFeedback(valid, feedback) {
if (!feedback)
feedback = [];
if (!Array.isArray(feedback))
feedback = [feedback];
this.valid = valid;
// NOTE(chris): On a list of radios/checkboxes only add the feedback message to the last item
if (this.name && (this.lcType == 'radio' || this.lcType == 'checkbox')) {
const selector = 'input[type="' + this.lcType + '"][name="' + this.name + '"]';
if ([...this.$el.parentNode.querySelectorAll(selector)].pop() != this.$refs.input)
return;
}
this.feedback = feedback;
},
_loadComponents() {
if (this.tag == 'VueDatePicker' && !this._.components.VueDatePicker) {
this._.components.VueDatePicker = Vue.defineAsyncComponent(() => import("../vueDatepicker.js.php"));
} else if (this.tag == 'PvAutocomplete' && !this._.components.PvAutocomplete) {
this._.components.PvAutocomplete = Vue.defineAsyncComponent(() => import(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/public/js/components/primevue/autocomplete/autocomplete.esm.min.js"));
} else if (this.tag == 'UploadImage' && !this._.components.UploadImage) {
this._.components.UploadImage = Vue.defineAsyncComponent(() => import("./Upload/Image.js"));
} else if (this.tag == 'UploadDms' && !this._.components.UploadDms) {
this._.components.UploadDms = Vue.defineAsyncComponent(() => import("./Upload/Dms.js"));
}
}
},
beforeMount() {
this._loadComponents();
},
beforeUpdate() {
this._loadComponents();
},
mounted() {
if (this.registerToForm)
this.registerToForm(this);
},
template: `
<component :is="!hasContainer ? 'FhcFragment' : 'div'" class="position-relative" :class="autoContainerClass">
<label v-if="$attrs.label && lcType != 'radio' && lcType != 'checkbox'" :for="idCmp">{{$attrs.label}}</label>
<input v-if="tag == 'input'" :type="lcType" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)">
<textarea v-else-if="tag == 'textarea'" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)"></textarea>
<select v-else-if="tag == 'select'" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)">
<slot></slot>
</select>
<component
v-else-if="tag == 'VueDatePicker'"
ref="input"
:is="tag"
:type="type"
v-model="modelValueCmp"
v-bind="$attrs"
:uid="idCmp ? idCmp.substr(9) : idCmp"
:name="name"
:class="validationClass"
:input-class-name=
"[...Object.entries({'form-control': !noAutoClass, 'is-valid': valid === true, 'is-invalid': valid === false}).reduce((a,[k,v]) => {if(v) a.push(k);return a}, []), ...($attrs['input-class-name'] ? $attrs['input-class-name'].split(' ') : [])].join(' ')"
@update:model-value="clearValidationForThisName"
>
<slot></slot>
</component>
<component
v-else-if="tag == 'PvAutocomplete'"
ref="input"
:is="tag"
:type="type"
v-model="modelValueCmp"
v-bind="$attrs"
:id="idCmp"
:input-props="{name}"
:class="validationClass"
:input-class="[...Object.entries({'form-control': !noAutoClass, 'is-valid': valid === true, 'is-invalid': valid === false}).reduce((a,[k,v]) => {if(v) a.push(k);return a}, []), ...($attrs['input-class'] ? $attrs['input-class'].split(' ') : [])].join(' ')"
@update:model-value="clearValidationForThisName"
>
<slot></slot>
</component>
<component
v-else-if="tag == 'UploadDms'"
ref="input"
:is="tag"
:type="type"
v-model="modelValueCmp"
v-bind="$attrs"
:id="idCmp"
:name="name"
:class="validationClass"
:input-class="validationClass"
:no-list="inputGroup"
@update:model-value="clearValidationForThisName"
>
<slot></slot>
</component>
<component
v-else
ref="input"
:is="tag"
:type="type"
v-model="modelValueCmp"
v-bind="$attrs"
:id="idCmp"
:name="name"
:class="validationClass"
@update:model-value="clearValidationForThisName"
>
<slot></slot>
</component>
<label v-if="$attrs.label && (lcType == 'radio' || lcType == 'checkbox')" :for="idCmp" :class="!noAutoClass && 'form-check-label'">{{$attrs.label}}</label>
<div v-if="valid !== undefined && feedback.length && !noFeedback" :class="feedbackClass">
<template v-for="(msg, i) in feedback" :key="i">
<hr v-if="i" class="m-0">
{{msg}}
</template>
</div>
</component>
`
}
+87
View File
@@ -0,0 +1,87 @@
export default {
emits: [
'update:modelValue'
],
props: {
modelValue: {
type: [ FileList, Array ],
required: true
},
multiple: Boolean,
id: String,
name: String,
inputClass: [String, Array, Object],
noList: Boolean
},
methods: {
stringifyFile(file) {
return JSON.stringify({
lastModified: file.lastModified,
lastModifiedDate: file.lastModifiedDate,
name: file.name,
size: file.size,
type: file.type
});
},
addFiles(event) {
if (!this.multiple)
return this.$emit('update:modelValue', event.target.files);
const dt = new DataTransfer();
const doubles = [];
for (var file of this.modelValue) {
dt.items.add(file);
doubles.push(this.stringifyFile(file));
}
for (var file of event.target.files) {
// NOTE(chris): deep check (with FileReader) would require an async function so we only check the basic attributes
if (doubles.indexOf(this.stringifyFile(file)) < 0)
dt.items.add(file);
}
this.$emit('update:modelValue', dt.files);
},
removeFile(id) {
const fileToRemove = Array.from(this.modelValue)[id];
const dt = new DataTransfer();
for (var file of this.modelValue) {
if (file !== fileToRemove)
dt.items.add(file);
}
this.$emit('update:modelValue', dt.files);
}
},
watch: {
modelValue(n) {
if (n instanceof FileList)
return this.$refs.upload.files = n;
const dt = new DataTransfer();
const dms = [];
for (var file of n) {
if (file instanceof File) {
dt.items.add(file);
} else {
const dmsFile = new File([JSON.stringify(file)], file.name, {
type: 'application/x.fhc-dms+json'
});
dt.items.add(dmsFile);
}
}
this.$emit('update:modelValue', dt.files);
}
},
template: `
<div class="form-upload-dms">
<input ref="upload" class="form-control" :class="inputClass" :id="id" :name="name" :multiple="multiple" type="file" @change="addFiles">
<ul v-if="modelValue.length && multiple && !noList" class="list-unstyled m-0">
<li v-for="(file, index) in modelValue" :key="index" class="d-flex mx-1 mt-1 align-items-start">
<span class="col-auto"><i class="fa fa-file me-1"></i></span>
<span class="col">{{ file.name }}</span>
<button class="col-auto btn btn-outline-secondary btn-p-0" @click="removeFile(index)">
<i class="fa fa-close"></i>
</button>
</li>
</ul>
</div>`
}
+62
View File
@@ -0,0 +1,62 @@
export default {
emits: [
'update:modelValue'
],
props: {
modelValue: String
},
computed: {
valueAsBase64DataString() {
if (!this.modelValue || this.modelValue.substring(0, 10) == 'data:image')
return this.modelValue;
return 'data:image/jpeg;charset=utf-8;base64,' + this.modelValue;
}
},
methods: {
openUploadDialog() {
this.$refs.fileInput.click();
},
pickFile() {
let file = this.$refs.fileInput.files;
if (file && file[0]) {
let reader = new FileReader();
reader.onload = e => {
this.$emit('update:modelValue', e.target.result);
}
reader.readAsDataURL(file[0]);
}
},
deleteImage() {
this.$emit('update:modelValue', '');
}
},
template: `
<div class="form-upload-image">
<template v-if="modelValue">
<img class="img-thumbnail" :src="valueAsBase64DataString" />
<div class="fotobutton">
<div class="d-grid gap-2 d-md-flex">
<button type="button" class="btn btn-outline-dark btn-sm" @click="deleteImage">
<i class="fa fa-close"></i>
</button>
<button type="button" class="btn btn-outline-dark btn-sm" @click="openUploadDialog">
<i class="fa fa-pen"></i>
</button>
</div>
</div>
</template>
<template v-else>
<slot>
<svg class="bd-placeholder-img img-thumbnail" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A generic square placeholder image with a white border around it, making it resemble a photograph taken with an old instant camera: 200x200" preserveAspectRatio="xMidYMid slice" focusable="false"><title>A generic square placeholder image with a white border around it, making it resemble a photograph taken with an old instant camera</title><rect width="100%" height="100%" fill="#868e96"></rect><text x="50%" y="50%" fill="#dee2e6" dy=".3em"></text></svg>
</slot>
<div class="fotobutton-visible">
<div class="d-grid gap-2 d-md-flex">
<button type="button" class="btn btn-outline-dark btn-sm" @click="openUploadDialog">
<i class="fa fa-pen"></i>
</button>
</div>
</div>
</template>
<input :id="$attrs.id" class="d-none" type="file" ref="fileInput" @input="pickFile" accept="image/*">
</div>`
}
+41
View File
@@ -0,0 +1,41 @@
export default {
inject: [
'$registerToForm'
],
data() {
return {
feedback: {
success: [],
danger: []
}
};
},
methods: {
clearValidation() {
this.feedback = {
success: [],
danger: []
};
},
setFeedback(valid, feedback) {
if (!feedback)
feedback = [];
if (!Array.isArray(feedback))
feedback = [feedback];
const ts = Date.now();
this.feedback[valid ? 'success' : 'danger'] = feedback.map(msg => [msg, ts]);
}
},
mounted() {
if (this.$registerToForm)
this.$registerToForm(this);
},
template: `
<template v-for="(arr, key) in feedback" :key="key">
<div v-for="[msg, ts] in arr" :key="ts + msg" class="alert alert-dismissible fade show" :class="'alert-' + key" role="alert">
{{msg}}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</template>
`
};
+5
View File
@@ -0,0 +1,5 @@
export default {
render() {
return (this.$slots && this.$slots.default) ? this.$slots.default() : null;
}
};
@@ -2,7 +2,6 @@ import StudierendenantragAbmeldung from './Form/Abmeldung.js';
import StudierendenantragAbmeldungStgl from './Form/AbmeldungStgl.js';
import StudierendenantragUnterbrechung from './Form/Unterbrechung.js';
import StudierendenantragWiederholung from './Form/Wiederholung.js';
import Phrasen from '../../mixins/Phrasen.js';
export default {
components: {
@@ -11,9 +10,6 @@ export default {
StudierendenantragUnterbrechung,
StudierendenantragWiederholung
},
mixins: [
Phrasen
],
emits: [
'update:infoArray',
'update:statusMsg',
@@ -37,13 +33,13 @@ export default {
return 'Studierendenantrag' + this.antragType;
},
infoText() {
return this.p.t('studierendenantrag/info_' + this.antragType + '_' + this.status);
return this.$p.t('studierendenantrag/info_' + this.antragType + '_' + this.status);
}
},
template: `
<div class="studierendenantrag-antrag card">
<div class="card-header">
{{p.t('studierendenantrag', 'title_' + antragType)}}
{{$p.t('studierendenantrag', 'title_' + antragType)}}
</div>
<div v-if="infoText && infoText.substr(0, 9) != '<< PHRASE'" class="alert alert-primary m-3" role="alert" v-html="infoText">
</div>
@@ -1,15 +1,17 @@
import {CoreFetchCmpt} from '../../Fetch.js';
import Phrasen from '../../../mixins/Phrasen.js';
import CoreForm from '../../Form/Form.js';
import FormValidation from '../../Form/Validation.js';
import FormInput from '../../Form/Input.js';
var _uuid = 0;
export default {
components: {
CoreFetchCmpt
CoreFetchCmpt,
CoreForm,
FormValidation,
FormInput
},
mixins: [
Phrasen
],
emits: [
'setInfos',
'setStatus'
@@ -22,9 +24,8 @@ export default {
return {
data: null,
saving: false,
errors: {
grund: [],
default: []
formData: {
grund: ''
}
}
},
@@ -33,142 +34,111 @@ export default {
switch (this.data.status)
{
case 'Erstellt': return 'info';
case 'Pause':
case 'Zurueckgezogen': return 'danger';
case 'Genehmigt': return 'success';
default: return 'info';
default: return 'warning';
}
},
loadUrl() {
if (this.studierendenantragId)
return '/components/Antrag/Abmeldung/getDetailsForAntrag/'+
this.studierendenantragId;
return '/components/Antrag/Abmeldung/getDetailsForNewAntrag/' +
this.prestudentId;
}
},
methods: {
load() {
return axios.get(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
this.loadUrl
).then(
result => {
this.data = result.data.retval;
return this.$fhcApi.factory
.studstatus.abmeldung.getDetails(this.studierendenantragId, this.prestudentId)
.then(result => {
this.data = result.data;
if (this.data.status) {
const msg = (this.data.status == 'Pause' && this.data.status_insertvon == "Studienabbruch") ? Vue.computed(() => {
let status = this.$p.t('studierendenantrag/status_stop');
return this.$p.t('studierendenantrag', 'status_x', {status});
}) : Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.data.statustyp}));
this.$emit("setStatus", {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.data.statustyp}),
msg,
severity: this.statusSeverity
});
}
return result;
}
);
})
.catch(this.$fhcAlert.handleSystemError);
},
createAntrag() {
bootstrap.Modal.getOrCreateInstance(this.$refs.modal).hide();
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_saving')}),
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_saving')})),
severity: 'warning'
});
this.saving = true;
for(var k in this.errors)
this.errors[k] = [];
axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Abmeldung/createAntrag/', {
studiensemester: this.data.studiensemester_kurzbz,
prestudent_id: this.data.prestudent_id,
grund: this.$refs.grund.value
}
).then(
result => {
if (result.data.error)
{
for (var k in result.data.retval)
{
if (this.errors[k] !== undefined)
this.errors[k].push(result.data.retval[k]);
else
this.errors.default.push(result.data.retval[k]);
}
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_error')}),
severity: 'danger'
this.$refs.form.clearValidation();
this.$refs.form.factory
.studstatus.abmeldung.create(
this.data.studiensemester_kurzbz,
this.data.prestudent_id,
this.formData.grund
)
.then(result => {
if (result.data === true)
document.location += "";
this.data = result.data;
if (this.data.status)
this.$emit("setStatus", {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.data.statustyp})),
severity: this.statusSeverity
});
}
else
{
if (result.data.retval === true)
document.location += "";
this.data = result.data.retval;
if (this.data.status) {
this.$emit("setStatus", {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.data.statustyp}),
severity: this.statusSeverity
});
}
else
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_open')}),
severity:'success'
});
}
this.$emit('setStatus', {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_open')})),
severity:'success'
});
this.saving = false;
}
);
})
.catch(error => {
this.$emit('setStatus', {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_error')})),
severity: 'danger'
});
this.saving = false;
this.$fhcAlert.handleSystemError(error);
});
},
cancelAntrag() {
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_cancelling')}),
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_cancelling')})),
severity: 'warning'
});
this.saving = true;
for(var k in this.errors)
this.errors[k] = [];
axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Abmeldung/cancelAntrag/', {
antrag_id: this.data.studierendenantrag_id
}
).then(
result => {
if (result.data.error)
{
for (var k in result.data.retval)
{
if (this.errors[k] !== undefined)
this.errors[k].push(result.data.retval[k]);
else
this.errors.default.push(result.data.retval[k]);
}
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_error')}),
severity:'danger'
this.$refs.form.clearValidation();
this.$refs.form.factory
.studstatus.abmeldung.cancel(
this.data.studierendenantrag_id
)
.then(result => {
if (Number.isInteger(result.data))
document.location = document.location.replace(/abmeldung\/([0-9]*)\/[0-9]*[\/]?$/, 'abmeldung/$1') + "/" + result.data;
this.data = result.data;
if (this.data.status)
this.$emit("setStatus", {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.data.statustyp})),
severity: this.statusSeverity
});
}
else
{
if (Number.isInteger(result.data.retval)) {
document.location = document.location.replace(/abmeldung\/([0-9]*)\/[0-9]*[\/]?$/, 'abmeldung/$1') + "/" + result.data.retval;
}
this.data = result.data.retval;
if (this.data.status) {
this.$emit("setStatus", {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.data.statustyp}),
severity: this.statusSeverity
});
}
else
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_cancelled')}),
severity: 'danger'
});
}
this.$emit('setStatus', {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_cancelled')})),
severity: 'danger'
});
this.saving = false;
}
);
})
.catch(error => {
this.$emit('setStatus', {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_error')})),
severity: 'danger'
});
this.saving = false;
this.$fhcAlert.handleSystemError(error);
});
}
},
created() {
@@ -177,55 +147,55 @@ export default {
template: `
<div class="studierendenantrag-form-abmeldung">
<core-fetch-cmpt :api-function="load">
<div class="row">
<core-form ref="form" class="row">
<div class="col-12">
<div v-for="error in errors.default" class="alert alert-danger" role="alert" v-html="error">
</div>
<form-validation></form-validation>
<table class="table">
<tr>
<th>{{p.t('lehre', 'studiengang')}}</th>
<th>{{$p.t('lehre', 'studiengang')}}</th>
<td align="right">{{data.bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'organisationsform')}}</th>
<th>{{$p.t('lehre', 'organisationsform')}}</th>
<td align="right">{{data.orgform_bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('projektarbeitsbeurteilung', 'nameStudierende')}}</th>
<th>{{$p.t('projektarbeitsbeurteilung', 'nameStudierende')}}</th>
<td align="right">{{data.name}}</td>
</tr>
<tr>
<th>{{p.t('person', 'personenkennzeichen')}}</th>
<th>{{$p.t('person', 'personenkennzeichen')}}</th>
<td align="right">{{data.matrikelnr}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'studienjahr')}}</th>
<th>{{$p.t('lehre', 'studienjahr')}}</th>
<td align="right">{{data.studienjahr_kurzbz}}</td>
</tr>
<tr>
<th>{{$p.t('lehre', 'studiensemester')}}</th>
<td align="right">{{data.studiensemester_kurzbz}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'semester')}}</th>
<th>{{$p.t('lehre', 'semester')}}</th>
<td align="right">{{data.semester}}</td>
</tr>
</table>
</div>
<div v-if="data.grund" class="mb-3">
<h5>{{p.t('studierendenantrag', 'antrag_grund')}}:</h5>
<h5>{{$p.t('studierendenantrag', 'antrag_grund')}}:</h5>
<pre>{{data.grund}}</pre>
</div>
<div v-else class="col-sm-6 mb-3">
<label :for="'studierendenantrag-form-abmeldung-' + uuid + '-grund'" class="form-label">Grund:</label>
<textarea
class="form-control"
:class="{'is-invalid': errors.grund.length}"
:id="'studierendenantrag-form-abmeldung-' + uuid + '-grund'"
<form-input
type="textarea"
label="Grund:"
v-model="formData.grund"
name="grund"
rows="5"
:disabled="saving"
ref="grund"
required
></textarea>
<div v-if="errors.grund.length" class="invalid-feedback">
{{errors.grund.join(".")}}
</div>
>
</form-input>
</div>
<div class="col-12 text-end">
<button
@@ -235,7 +205,7 @@ export default {
@click="cancelAntrag"
:disabled="saving"
>
{{p.t('studierendenantrag', 'btn_cancel')}}
{{$p.t('studierendenantrag', 'btn_cancel')}}
</button>
<button
v-else-if="!data.studierendenantrag_id"
@@ -245,7 +215,7 @@ export default {
:data-bs-target="'#studierendenantrag-form-abmeldung-' + uuid + '-modal'"
:disabled="saving"
>
{{p.t('studierendenantrag', 'btn_create_Abmeldung')}}
{{$p.t('studierendenantrag', 'btn_create_Abmeldung')}}
</button>
<div
@@ -262,31 +232,31 @@ export default {
class="modal-title"
:id="'studierendenantrag-form-abmeldung-' + uuid + '-modal-label'"
>
{{p.t('studierendenantrag', 'title_Abmeldung')}}
{{$p.t('studierendenantrag', 'title_Abmeldung')}}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="p.t('ui', 'schliessen')"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$p.t('ui', 'schliessen')"></button>
</div>
<div class="modal-body" v-html="p.t('studierendenantrag', 'warning_Abmeldung')">
<div class="modal-body" v-html="$p.t('studierendenantrag', 'warning_Abmeldung')">
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-primary"
@click="createAntrag">
{{p.t('studierendenantrag', 'btn_create_Abmeldung')}}
{{$p.t('studierendenantrag', 'btn_create_Abmeldung')}}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</core-form>
<template v-slot:error="{errorMessage}">
<div class="alert alert-danger m-0" role="alert">
{{ errorMessage }}
</div>
</template>
</core-fetch-cmpt>
</div>
`
</div>`
}
@@ -1,99 +1,242 @@
import {CoreFetchCmpt} from '../../Fetch.js';
import Phrasen from '../../../mixins/Phrasen.js';
import CoreForm from '../../Form/Form.js';
import FormValidation from '../../Form/Validation.js';
import FormInput from '../../Form/Input.js';
var _uuid = 0;
export default {
components: {
CoreFetchCmpt
CoreFetchCmpt,
CoreForm,
FormValidation,
FormInput
},
mixins: [
Phrasen
],
emits: [
'setInfos',
'setStatus'
],
props: {
prestudentId: Number,
studierendenantragId: Number
},
data() {
return {
data: null
data: null,
saving: false,
formData: {
grund: ''
}
}
},
computed: {
statusSeverity() {
switch (this.data.status)
switch (this.data?.status)
{
case 'Erstellt': return 'info';
case 'Genehmigt': return 'success';
default: return 'info';
case 'Pause':
case 'Zurueckgezogen': return 'danger';
case 'EinspruchAbgelehnt':
case 'Abgemeldet': return 'success';
default: return 'warning';
}
}
},
methods: {
load() {
return axios.get(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Abmeldung/getDetailsForAntrag/' +
this.studierendenantragId
).then(
result => {
this.data = result.data.retval;
return this.$fhcApi.factory
.studstatus.abmeldung.getDetails(this.studierendenantragId, this.prestudentId)
.then(result => {
this.data = result.data;
if (this.data.status) {
const msg = (this.data.status == 'Pause' && this.data.status_insertvon == "Studienabbruch") ? Vue.computed(() => {
let status = this.$p.t('studierendenantrag/status_stop');
return this.$p.t('studierendenantrag', 'status_x', {status});
}) : Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.data.statustyp}));
this.$emit("setStatus", {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.data.statustyp}),
msg,
severity: this.statusSeverity
});
}
return result;
}
);
}
});
},
createAntrag() {
bootstrap.Modal.getOrCreateInstance(this.$refs.modal).hide();
this.$emit('setStatus', {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_saving')})),
severity: 'warning'
});
this.saving = true;
this.$refs.form.clearValidation();
this.$refs.form.factory
.studstatus.abmeldung.create(
this.data.studiensemester_kurzbz,
this.data.prestudent_id,
this.formData.grund
)
.then(result => {
if (result.data === true)
document.location += "";
this.data = result.data;
if (this.data.status)
this.$emit("setStatus", {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.data.statustyp})),
severity: this.statusSeverity
});
else
this.$emit('setStatus', {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_open')})),
severity:'success'
});
this.saving = false;
})
.catch(error => {
this.$emit('setStatus', {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_error')})),
severity: 'danger'
});
this.saving = false;
this.$fhcAlert.handleSystemError(error);
});
},
appendDropDownText(event) {
this.formData.grund = event.target.value
? this.$p.t('studierendenantrag', event.target.value)
: '';
},
},
created() {
this.uuid = _uuid++;
},
template: `
<div class="studierendenantrag-form-abmeldung">
<core-fetch-cmpt :api-function="load">
<div class="row">
<core-form ref="form" class="row">
<div class="col-12">
<form-validation></form-validation>
<table class="table">
<tr>
<th>{{p.t('lehre', 'studiengang')}}</th>
<th>{{$p.t('lehre', 'studiengang')}}</th>
<td align="right">{{data.bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'organisationsform')}}</th>
<th>{{$p.t('lehre', 'organisationsform')}}</th>
<td align="right">{{data.orgform_bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('projektarbeitsbeurteilung', 'nameStudierende')}}</th>
<th>{{$p.t('projektarbeitsbeurteilung', 'nameStudierende')}}</th>
<td align="right">{{data.name}}</td>
</tr>
<tr>
<th>{{p.t('person', 'personenkennzeichen')}}</th>
<th>{{$p.t('person', 'personenkennzeichen')}}</th>
<td align="right">{{data.matrikelnr}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'studienjahr')}}</th>
<th>{{$p.t('lehre', 'studienjahr')}}</th>
<td align="right">{{data.studienjahr_kurzbz}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'semester')}}</th>
<th>{{$p.t('lehre', 'studiensemester')}}</th>
<td align="right">{{data.studiensemester_kurzbz}}</td>
</tr>
<tr>
<th>{{$p.t('lehre', 'semester')}}</th>
<td align="right">{{data.semester}}</td>
</tr>
</table>
</div>
<div class="mb-3">
<h5>{{p.t('studierendenantrag', 'antrag_grund')}}:</h5>
<pre>{{data.grund}}</pre>
<div v-if="data.grund" class="mb-3">
<h5>{{$p.t('studierendenantrag', 'antrag_grund')}}:</h5>
<pre class="text-prewrap">{{data?.grund}}</pre>
</div>
<div v-else class="col-sm-6 mb-3">
<label :for="'studierendenantrag-form-abmeldung-' + uuid + '-grund'" class="form-label">Grund:</label>
<div class="mb-2">
<select name="grundAv" class="form-select" @change="appendDropDownText">
<option value="" > --- bitte auswählen, sofern zutreffend ---- </option>
<option value="textLong_NichtantrittStudium">{{$p.t('studierendenantrag', 'dropdown_NichtantrittStudium')}}
</option>
<option value="textLong_ungenuegendeLeistung">{{$p.t('studierendenantrag', 'dropdown_ungenuegendeLeistung')}}
</option>
<option value="textLong_studentNichtAnwesend">{{$p.t('studierendenantrag', 'dropdown_nichtAnwesend')}}
</option>
<option value="textLong_PruefunstermineNichtEingehalten">{{$p.t('studierendenantrag', 'dropdown_PruefunstermineNichtEingehalten')}}
</option>
<option value="textLong_studentNichtGezahlt">{{$p.t('studierendenantrag', 'dropdown_nichtGezahlt')}}
</option>
<option value="textLong_plageat">{{$p.t('studierendenantrag', 'dropdown_plageat')}}
</option>
<option value="textLong_MissingZgv">{{$p.t('studierendenantrag', 'dropdown_MissingZgv')}}
</option>
</select>
</div>
<form-input
type="textarea"
v-model="formData.grund"
name="grund"
:id="'studierendenantrag-form-abmeldung-' + uuid + '-grund'"
rows="5"
:disabled="saving"
required
>
</form-input>
</div>
</div>
<div class="col-12 text-end">
<button
v-if="!data?.studierendenantrag_id"
type="button"
class="btn btn-primary"
data-bs-toggle="modal"
:data-bs-target="'#studierendenantrag-form-abmeldung-' + uuid + '-modal'"
:disabled="saving"
>
{{$p.t('studierendenantrag', 'btn_create_Abmeldung')}}
</button>
<div
ref="modal"
class="modal fade text-start"
:id="'studierendenantrag-form-abmeldung-' + uuid + '-modal'"
tabindex="-1"
:aria-labelledby="'studierendenantrag-form-abmeldung-' + uuid + '-modal-label'"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5
class="modal-title"
:id="'studierendenantrag-form-abmeldung-' + uuid + '-modal-label'"
>
{{$p.t('studierendenantrag', 'title_Abmeldung')}}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$p.t('ui', 'schliessen')"></button>
</div>
<div class="modal-body" v-html="$p.t('studierendenantrag', 'warning_AbmeldungStgl')">
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-primary"
@click="createAntrag">
{{$p.t('studierendenantrag', 'btn_create_Abmeldung')}}
</button>
</div>
</div>
</div>
</div>
</div>
</core-form>
<template v-slot:error="{errorMessage}">
<div class="alert alert-danger m-0" role="alert">
{{ errorMessage }}
</div>
</template>
</core-fetch-cmpt>
</div>
`
</div>`
}
@@ -1,17 +1,16 @@
import {CoreFetchCmpt} from '../../Fetch.js';
import VueDatepicker from '../../vueDatepicker.js.php';
import Phrasen from '../../../mixins/Phrasen.js';
import CoreForm from '../../Form/Form.js';
import FormValidation from '../../Form/Validation.js';
import FormInput from '../../Form/Input.js';
var _uuid = 0;
export default {
components: {
CoreFetchCmpt,
VueDatepicker
CoreForm,
FormValidation,
FormInput
},
mixins: [
Phrasen
],
emits: [
'setInfos',
'setStatus'
@@ -24,13 +23,9 @@ export default {
return {
data: null,
saving: false,
errors: {
grund: [],
studiensemester: [],
datum_wiedereinstieg: [],
default: []
},
attachment: [],
stsem: null,
currentWiedereinstieg: '',
siteUrl: FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router
}
@@ -40,24 +35,19 @@ export default {
switch (this.data.status)
{
case 'Erstellt': return 'info';
case 'Genehmigt': return 'success';
case 'Zurückgezogen': return 'danger';
default: return 'info';
case 'Pause':
case 'Zurueckgezogen':
case 'Abgelehnt': return 'danger';
case 'Genehmigt':
case 'EmailVersandt': return 'success';
default: return 'warning';
}
},
loadUrl() {
if (this.studierendenantragId)
return '/components/Antrag/Unterbrechung/getDetailsForAntrag/'+
this.studierendenantragId;
return '/components/Antrag/Unterbrechung/getDetailsForNewAntrag/' +
this.prestudentId;
},
datumWsFormatted() {
let datumUnformatted = '';
if (this.studierendenantragId) {
if (this.data.datum_wiedereinstieg)
datumUnformatted = this.data.datum_wiedereinstieg;
if (this.data.datum_wiedereinstieg) {
datumUnformatted = this.data.datum_wiedereinstieg;
} else {
if (this.stsem !== null && this.data.studiensemester[this.stsem].wiedereinstieg)
datumUnformatted = this.data.studiensemester[this.stsem].wiedereinstieg;
@@ -66,265 +56,252 @@ export default {
return datumUnformatted;
let datum = new Date(datumUnformatted);
return datum.toLocaleDateString();
},
semesterOffsets() {
if (!this.data || !this.data.studiensemester)
return [];
return Object.values(this.data.studiensemester)
.filter(el => !el.disabled)
.map(el => el.studiensemester_kurzbz);
},
semester() {
if (!this.stsem)
return '';
return this.data.semester + this.semesterOffsets.indexOf(this.stsem);
}
},
methods: {
load() {
return axios.get(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
this.loadUrl
).then(
result => {
this.data = result.data.retval;
if (this.data.status) {
this.$emit("setStatus", {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.data.statustyp}),
severity: this.statusSeverity
});
return this.$fhcApi.factory
.studstatus.unterbrechung.getDetails(this.studierendenantragId, this.prestudentId)
.then(
result => {
this.data = result.data;
if (this.data.status) {
const msg = (this.data.status == 'Pause' && this.data.status_insertvon == "Studienabbruch") ? Vue.computed(() => {
let status = this.$p.t('studierendenantrag/status_stop');
return this.$p.t('studierendenantrag', 'status_x', {status});
}) : Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.data.statustyp}));
this.$emit("setStatus", {
msg,
severity: this.statusSeverity
});
}
return result;
}
return result;
}
);
);
},
createAntrag() {
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_saving')}),
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_saving')})),
severity: 'warning'
});
this.saving = true;
for(var k in this.errors)
this.errors[k] = [];
var formData = new FormData();
var attachment = this.$refs.attachment;
formData.append("attachment", attachment.files[0]);
formData.append("studiensemester", this.stsem !== null && this.data.studiensemester[this.stsem].studiensemester_kurzbz);
formData.append("prestudent_id", this.data.prestudent_id);
formData.append("grund", this.$refs.grund.value);
formData.append("datum_wiedereinstieg", this.stsem !== null && this.data.studiensemester[this.stsem].wiedereinstieg);
this.$refs.form.clearValidation();
this.$refs.form.factory
.studstatus.unterbrechung.create(
this.stsem !== null && this.data.studiensemester[this.stsem].studiensemester_kurzbz,
this.data.prestudent_id,
this.data.grund,
this.stsem !== null && this.currentWiedereinstieg,
this.attachment
)
.then(result => {
if (Number.isInteger(result.data))
document.location += "/" + result.data;
axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Unterbrechung/createAntrag/',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
).then(
result => {
if (result.data.error)
{
for (var k in result.data.retval)
{
if (this.errors[k] !== undefined)
this.errors[k].push(result.data.retval[k]);
else
this.errors.default.push(result.data.retval[k]);
}
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_error')}),
severity: 'danger'
this.data = result.data;
if (this.data.status)
this.$emit("setStatus", {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.data.statustyp})),
severity: this.statusSeverity
});
}
else
{
if (Number.isInteger(result.data.retval))
document.location += "/" + result.data.retval;
this.data = result.data.retval;
if (this.data.status) {
this.$emit("setStatus", {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.data.statustyp}),
severity: this.statusSeverity
});
}
else
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_created')}),
severity: 'info'
});
}
this.$emit('setStatus', {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_created')})),
severity: 'info'
});
this.saving = false;
}
);
})
.catch(error => {
this.$emit('setStatus', {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_error')})),
severity: 'danger'
});
this.saving = false;
this.$fhcAlert.handleSystemError(error);
});
},
cancelAntrag() {
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_cancelling')}),
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_cancelling')})),
severity: 'warning'
});
this.saving = true;
for(var k in this.errors)
this.errors[k] = [];
axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Unterbrechung/cancelAntrag/', {
antrag_id: this.data.studierendenantrag_id
}
).then(
result => {
if (result.data.error)
{
for (var k in result.data.retval)
{
if (this.errors[k] !== undefined)
this.errors[k].push(result.data.retval[k]);
else
this.errors.default.push(result.data.retval[k]);
}
this.$refs.form.clearValidation();
this.$refs.form.factory
.studstatus.unterbrechung.cancel(
this.data.studierendenantrag_id
)
.then(result => {
if (Number.isInteger(result.data))
document.location = document.location.replace(/unterbrechung\/([0-9]*)\/[0-9]*[\/]?$/, 'unterbrechung/$1') + "/" + result.data;
this.data = result.data;
if (this.data.status)
this.$emit("setStatus", {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.data.statustyp})),
severity: this.statusSeverity
});
else
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_error')}),
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_cancelled')})),
severity: 'danger'
});
}
else
{
if (Number.isInteger(result.data.retval)) {
document.location = document.location.replace(/unterbrechung\/([0-9]*)\/[0-9]*[\/]?$/, 'unterbrechung/$1') + "/" + result.data.retval;
}
this.data = result.data.retval;
if (this.data.status) {
this.$emit("setStatus", {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.data.statustyp}),
severity: this.statusSeverity
});
}
else
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_cancelled')}),
severity: 'danger'
});
}
this.saving = false;
}
);
})
.catch(error => {
this.$emit('setStatus', {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_error')})),
severity: 'danger'
});
this.saving = false;
this.$fhcAlert.handleSystemError(error);
});
}
},
created() {
this.uuid = _uuid++;
},
template: `
<div class="studierendenantrag-form-unterbrechung">
<core-fetch-cmpt :api-function="load">
<div class="row">
<core-form ref="form" class="row">
<div class="col-12">
<div v-for="error in errors.default" class="alert alert-danger" role="alert" v-html="error">
</div>
<form-validation></form-validation>
<table class="table">
<tr>
<th>{{p.t('lehre', 'studiengang')}}</th>
<th>{{$p.t('lehre', 'studiengang')}}</th>
<td align="right">{{data.bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'organisationsform')}}</th>
<th>{{$p.t('lehre', 'organisationsform')}}</th>
<td align="right">{{data.orgform_bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('projektarbeitsbeurteilung', 'nameStudierende')}}</th>
<th>{{$p.t('projektarbeitsbeurteilung', 'nameStudierende')}}</th>
<td align="right">{{data.name}}</td>
</tr>
<tr>
<th>{{p.t('person', 'personenkennzeichen')}}</th>
<th>{{$p.t('person', 'personenkennzeichen')}}</th>
<td align="right">{{data.matrikelnr}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'studienjahr')}}</th>
<th>{{$p.t('lehre', 'studienjahr')}}</th>
<td align="right" v-if="data.studierendenantrag_id">{{data.studienjahr_kurzbz}}</td>
<td align="right" v-else>{{stsem === null ? '' : data.studiensemester[stsem].studienjahr_kurzbz}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'semester')}}</th>
<th>{{$p.t('lehre', 'semester')}}</th>
<td align="right" v-if="data.studierendenantrag_id">{{data.semester}}</td>
<td align="right" v-else>{{stsem === null ? '' : data.studiensemester[stsem].semester}}</td>
<td align="right" v-else>{{semester}}</td>
</tr>
</table>
</div>
<div class="col-sm-6 mb-3">
<label :for="'studierendenantrag-form-abmeldung-' + uuid + '-stsem'" class="form-label">
{{p.t('lehre', 'studiensemester')}}
</label>
<div v-if="data.studierendenantrag_id">
{{data.studiensemester_kurzbz}}
</div>
<div v-else>
<select
class="form-select"
:class="{'is-invalid': errors.studiensemester.length}"
v-model="stsem"
required
:id="'studierendenantrag-form-abmeldung-' + uuid + '-stsem'"
>
<option v-for="(stsem, index) in data.studiensemester" :key="index" :value="index">
{{stsem.studiensemester_kurzbz}}
</option>
</select>
<div v-if="errors.studiensemester.length" class="invalid-feedback">
{{errors.studiensemester.join(".")}}
<label class="form-label">
{{$p.t('lehre', 'studiensemester')}}
</label>
<div>
{{data.studiensemester_kurzbz}}
</div>
</div>
<form-input
v-else
type="select"
v-model="stsem"
name="studiensemester"
:label="$p.t('lehre', 'studiensemester')"
required
@input="currentWiedereinstieg = ''"
>
<option v-for="(stsem, index) in data.studiensemester" :key="index" :value="index" :disabled="stsem.disabled">
{{stsem.studiensemester_kurzbz}}
</option>
</form-input>
</div>
<div class="col-sm-6 mb-3">
<label class="form-label">
{{p.t('studierendenantrag', 'antrag_datum_wiedereinstieg')}}
</label>
<div v-if="data.studierendenantrag_id">
{{datumWsFormatted}}
</div>
<div v-else-if="stsem === null" class="form-control">
{{p.t('ui/select_studiensemester')}}
</div>
<div v-else class="form-control">
{{datumWsFormatted}}
</div>
<div v-if="errors.datum_wiedereinstieg.length" class="invalid-feedback d-block">
{{errors.datum_wiedereinstieg.join(".")}}
<label class="form-label">
{{$p.t('studierendenantrag', 'antrag_datum_wiedereinstieg')}}
</label>
<div>
{{datumWsFormatted}}
</div>
</div>
<form-input
v-else-if="stsem === null"
type="select"
:label="$p.t('studierendenantrag', 'antrag_datum_wiedereinstieg')"
modelValue=""
name="datum_wiedereinstieg"
disabled
>
<template #default>
<option value="" selected disabled hidden>{{$p.t('ui/select_studiensemester')}}</option>
</template>
</form-input>
<form-input
v-else
type="select"
:label="$p.t('studierendenantrag', 'antrag_datum_wiedereinstieg')"
v-model="currentWiedereinstieg"
name="datum_wiedereinstieg"
>
<option v-for="sem in data.studiensemester[stsem].wiedereinstieg" :key="sem.studiensemester_kurzbz" :value="sem.start" :disabled="sem.disabled">
{{sem.studiensemester_kurzbz}}
</option>
</form-input>
</div>
<div v-if="data.studierendenantrag_id" class="mb-3">
<h5>{{p.t('studierendenantrag', 'antrag_grund')}}:</h5>
<pre>{{data.grund}}</pre>
</div>
<div v-else class="col-sm-6 mb-3">
<label :for="'studierendenantrag-form-abmeldung-' + uuid + '-grund'" class="form-label">Grund:</label>
<textarea
class="form-control"
:class="{'is-invalid': errors.grund.length}"
:id="'studierendenantrag-form-abmeldung-' + uuid + '-grund'"
<div class="col-sm-6 mb-3">
<form-input
v-if="data.studierendenantrag_id"
type="textarea"
:label="$p.t('studierendenantrag', 'antrag_grund') + ':'"
v-model="data.grund"
name="grund"
rows="5"
:disabled="saving"
readonly
>
</form-input>
<form-input
v-else
ref="grund"
type="textarea"
:label="$p.t('studierendenantrag', 'antrag_grund') + ':'"
v-model="data.grund"
name="grund"
:disabled="saving"
rows="5"
required
></textarea>
<div v-if="errors.grund.length" class="invalid-feedback">
{{errors.grund.join(".")}}
</div>
>
</form-input>
</div>
<div class="col-12 mb-3">
<div v-if="data.studierendenantrag_id">
<a v-if="data.dms_id" target="_blank" :href="siteUrl + '/lehre/Antrag/Attachment/Show/' + data.dms_id"> {{p.t('studierendenantrag', 'antrag_dateianhaenge')}} </a>
<span v-else>{{p.t('studierendenantrag', 'no_attachments')}}</span>
</div>
<div v-else>
<label
:for="'studierendenantrag-form-abmeldung-' + uuid + '-attachment'"
class="form-label">
{{p.t('studierendenantrag', 'antrag_dateianhaenge')}}
</label>
<input
class="form-control"
type="file"
ref="attachment"
:id="'studierendenantrag-form-abmeldung-' + uuid + '-attachment'"
name="attachment">
<a v-if="data.dms_id" target="_blank" :href="siteUrl + '/lehre/Antrag/Attachment/Show/' + data.dms_id"> {{$p.t('studierendenantrag', 'antrag_dateianhaenge')}} </a>
<span v-else>{{$p.t('studierendenantrag', 'no_attachments')}}</span>
</div>
<form-input
v-else
ref="attachment"
type="uploadfile"
:label="$p.t('studierendenantrag', 'antrag_dateianhaenge')"
v-model="attachment"
name="attachment"
>
</form-input>
</div>
<div class="col-12 text-end">
<button
@@ -334,7 +311,7 @@ export default {
@click="createAntrag"
:disabled="saving"
>
{{p.t('studierendenantrag', 'btn_create_Unterbrechung')}}
{{$p.t('studierendenantrag', 'btn_create_Unterbrechung')}}
</button>
<button
v-else-if="data.status == 'Erstellt'"
@@ -343,16 +320,15 @@ export default {
@click="cancelAntrag"
:disabled="saving"
>
{{p.t('studierendenantrag', 'btn_cancel')}}
{{$p.t('studierendenantrag', 'btn_cancel')}}
</button>
</div>
</div>
</core-form>
<template v-slot:error="{errorMessage}">
<div class="alert alert-danger m-0" role="alert">
{{ errorMessage }}
</div>
</template>
</core-fetch-cmpt>
</div>
`
</div>`
}
@@ -1,17 +1,14 @@
import {CoreFetchCmpt} from '../../Fetch.js';
import VueDatepicker from '../../vueDatepicker.js.php';
import Phrasen from '../../../mixins/Phrasen.js';
import CoreForm from '../../Form/Form.js';
import FormValidation from '../../Form/Validation.js';
var _uuid = 0;
export default {
components: {
CoreFetchCmpt,
VueDatepicker
CoreForm,
FormValidation
},
mixins: [
Phrasen
],
emits: [
'setInfos',
'setStatus',
@@ -26,12 +23,6 @@ export default {
return {
data: null,
saving: false,
errors: {
grund: [],
default: []
},
siteUrl: FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router,
infos: []
}
},
@@ -39,16 +30,16 @@ export default {
statusSeverity() {
switch (this.data.status)
{
case 'Erstellt': return 'info';
case 'Offen':
case 'Erstellt':
case 'ErsteAufforderungVersandt': return 'info';
case 'Genehmigt': return 'success';
case 'Verzichtet': return 'danger';
default: return 'info';
case 'Pause':
case 'Verzichtet':
case 'Abgemeldet': return 'danger';
default: return 'warning';
}
},
loadUrl() {
return '/components/Antrag/Wiederholung/getDetailsForNewAntrag/' +
this.prestudentId;
},
datumPruefungFormatted() {
if(!this.data.pruefungsdatum)
return '';
@@ -58,25 +49,27 @@ export default {
},
methods: {
load() {
return axios.get(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
this.loadUrl
).then(
result => {
this.data = result.data.retval;
return this.$fhcApi.factory
.studstatus.wiederholung.getDetails(
this.prestudentId
)
.then(result => {
this.data = result.data;
if (!this.data.status || this.data.status == 'ErsteAufforderungVersandt' || this.data.status == 'ZweiteAufforderungVersandt') {
this.data.status = 'Offen';
this.data.statustyp = this.p.t('studierendenantrag', 'status_open');
this.data.statustyp = this.$p.t('studierendenantrag', 'status_open');
}
this.$emit('update:status', this.data.status);
const msg = (this.data.status == 'Pause' && this.data.status_insertvon == "Studienabbruch") ? Vue.computed(() => {
let status = this.$p.t('studierendenantrag/status_stop');
return this.$p.t('studierendenantrag', 'status_x', {status});
}) : Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.data.statustyp}));
this.$emit("setStatus", {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.data.statustyp}),
msg,
severity: this.statusSeverity
});
return result;
}
);
});
},
createAntrag() {
this.createAntragWithStatus(true);
@@ -85,98 +78,79 @@ export default {
this.createAntragWithStatus(false);
},
createAntragWithStatus(repeat) {
let func = repeat ? 'createAntrag' : 'cancelAntrag';
let func = repeat ? 'create' : 'cancel';
let nextState = repeat ? 'Erstellt' : 'Verzichtet';
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_saving')}),
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_saving')})),
severity: 'warning'
});
this.saving = true;
for(var k in this.errors)
this.errors[k] = [];
axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Wiederholung/' + func + '/',
{
prestudent_id: this.data.prestudent_id,
studiensemester: this.data.studiensemester_kurzbz
}
).then(
result => {
if (result.data.error)
{
for (var k in result.data.retval)
{
if (this.errors[k] !== undefined)
this.errors[k].push(result.data.retval[k]);
else
this.errors.default.push(result.data.retval[k]);
}
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_error')}),
severity: 'danger'
});
}
else
{
if (result.data.retval === true)
document.location += "";
this.data = result.data.retval;
if (!this.data.status)
this.data.status = nextState;
this.$emit('update:status', this.data.status);
this.$emit("setStatus", {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.data.statustyp}),
severity: this.statusSeverity
});
}
this.$refs.form.factory
.studstatus.wiederholung[func](
this.data.prestudent_id,
this.data.studiensemester_kurzbz
)
.then(result => {
if (result.data === true)
document.location += "";
this.data = result.data;
if (!this.data.status)
this.data.status = nextState;
this.$emit('update:status', this.data.status);
this.$emit("setStatus", {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.data.statustyp})),
severity: this.statusSeverity
});
this.saving = false;
}
);
})
.catch(error => {
this.$emit('setStatus', {
msg: Vue.computed(() => this.$p.t('studierendenantrag', 'status_x', {status: this.$p.t('studierendenantrag', 'status_error')})),
severity: 'danger'
});
this.saving = false;
this.$fhcAlert.handleSystemError(error);
});
}
},
created() {
this.uuid = _uuid++;
},
mounted() {
this.infos = [...Array(5).keys()].map(n => ({
body: this.p.t_ref('studierendenantrag', 'infotext_Wiederholung_' + n)
body: Vue.computed(() => this.$p.t('studierendenantrag', 'infotext_Wiederholung_' + n))
}));
this.$emit('setInfos', this.infos);
},
template: `
<div class="studierendenantrag-form-wiederholung">
<core-fetch-cmpt :api-function="load">
<div class="row">
<core-form ref="form" class="row">
<div class="col-12">
<div v-for="error in errors.default" class="alert alert-danger" role="alert" v-html="error">
</div>
<form-validation></form-validation>
<table class="table">
<tr>
<th>{{p.t('lehre', 'studiengang')}}</th>
<th>{{$p.t('lehre', 'studiengang')}}</th>
<td align="right">{{data.bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'organisationsform')}}</th>
<th>{{$p.t('lehre', 'organisationsform')}}</th>
<td align="right">{{data.orgform_bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('projektarbeitsbeurteilung', 'nameStudierende')}}</th>
<th>{{$p.t('projektarbeitsbeurteilung', 'nameStudierende')}}</th>
<td align="right">{{data.name}}</td>
</tr>
<tr>
<th>{{p.t('person', 'personenkennzeichen')}}</th>
<th>{{$p.t('person', 'personenkennzeichen')}}</th>
<td align="right">{{data.matrikelnr}}</td>
</tr>
<tr>
<th>{{p.t('studierendenantrag', 'antrag_Wiederholung_pruefung')}}</th>
<th>{{$p.t('studierendenantrag', 'antrag_Wiederholung_pruefung')}}</th>
<td align="right">{{data.lvbezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('studierendenantrag', 'antrag_Wiederholung_pruefung_date')}}</th>
<th>{{$p.t('studierendenantrag', 'antrag_Wiederholung_pruefung_date')}}</th>
<td align="right">{{datumPruefungFormatted}}</td>
</tr>
</table>
@@ -190,19 +164,19 @@ export default {
@click="createAntrag"
:disabled="saving"
>
{{p.t('studierendenantrag/antrag_Wiederholung_button_yes')}}
{{$p.t('studierendenantrag/antrag_Wiederholung_button_yes')}}
</button>
<button
<!-- <button
v-if="!data.studierendenantrag_id || data.status == 'Offen'"
type="button"
class="btn btn-danger"
@click="cancelAntrag"
:disabled="saving"
>
{{p.t('studierendenantrag/antrag_Wiederholung_button_no')}}
</button>
{{$p.t('studierendenantrag/antrag_Wiederholung_button_no')}}
</button>-->
</div>
</div>
</core-form>
<template v-slot:error="{errorMessage}">
<div class="alert alert-danger m-0" role="alert">
{{ errorMessage }}
@@ -5,7 +5,6 @@ import GrundPopup from './Leitung/GrundPopup.js';
import LvPopup from './Leitung/LvPopup.js';
import BsAlert from '../Bootstrap/Alert.js';
import FhcLoader from '../Loader.js';
import Phrasen from '../../mixins/Phrasen.js';
export default {
components: {
@@ -14,7 +13,6 @@ export default {
LeitungActions,
FhcLoader
},
mixins: [Phrasen],
props: {
stgL: Array,
stgA: Array
@@ -31,32 +29,28 @@ export default {
stgkzL() {
if (!this.stgL)
return [];
return this.stgL.map(stg => stg.studiengang_kz);
return this.stgL.map(stg => parseInt(stg));
},
stgkzA() {
if (!this.stgA)
return [];
return this.stgA.map(stg => stg.studiengang_kz);
return this.stgA.map(stg => parseInt(stg));
}
},
methods: {
loadFilter() {
axios.get(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Leitung/getActiveStgs'
).then(result => {
this.stgs = Object.values(result.data.retval).sort((a,b) => a.bezeichnung == b.bezeichnung ? (a.orgform == b.orgform ? 0 : (a.orgform > b.orgform ? 1 : -1)) : (a.bezeichnung > b.bezeichnung ? 1 : -1));
}).catch(error => {
console.error(error);
});
this.$fhcApi.factory
.studstatus.leitung.getStgs()
.then(result => this.stgs = result.data)
.catch(this.$fhcAlert.handleSystemError);
},
changeFilter(evt) {
this.filter = evt.target.value || undefined;
changeFilter(filter) {
this.filter = filter || undefined;
this.reload();
},
reload() {
this.$refs.table.reload(this.filter);
if (this.$refs.table)
this.$refs.table.reload(this.filter);
this.loadFilter();
},
download() {
@@ -79,7 +73,7 @@ export default {
{
let countAntrage = 0;
LvPopup
.popup(this.p.t('studierendenantrag','title_show_lvs', currentAntrag), {
.popup(this.$p.t('studierendenantrag','title_show_lvs', currentAntrag), {
antragId: currentAntrag.studierendenantrag_id,
footer: true,
dialogClass: 'modal-lg',
@@ -104,21 +98,9 @@ export default {
}
} else {
this.$refs.loader.show();
axios
.all(
oks.map(
antrag => axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Leitung/approve' + antrag.typ,
{
studierendenantrag_id: antrag.studierendenantrag_id
}
)
)
)
.then(this.showValidation)
.catch(this.showError);
this.$fhcApi.factory
.studstatus.leitung.approve(oks)
.then(this.showResults);
}
},
actionReject(evt, gruende) {
@@ -128,7 +110,7 @@ export default {
var currentAntrag = antraege.pop();
if (currentAntrag) {
GrundPopup
.popup(this.p.t('studierendenantrag', 'title_grund', {id: currentAntrag.studierendenantrag_id}), {
.popup(this.$p.t('studierendenantrag', 'title_grund', {id: currentAntrag.studierendenantrag_id}), {
countRemaining: antraege.length
})
.then(result => {
@@ -148,61 +130,38 @@ export default {
.catch(() => {});
} else {
this.$refs.loader.show();
axios
.all(
gruende.map(
antrag => axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Leitung/reject' + antrag.typ,
{
studierendenantrag_id: antrag.studierendenantrag_id,
grund: antrag.grund
}
)
)
)
.then(this.showValidation)
.catch(this.showError);
this.$fhcApi.factory
.studstatus.leitung.reject(gruende)
.then(this.showResults);
}
},
actionReopen(evt) {
var antraege = evt || this.selectedData;
this.$refs.loader.show();
axios
.all(
antraege.map(
antrag => axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Leitung/reopenAntrag/',
{
studierendenantrag_id: antrag.studierendenantrag_id
}
)
)
)
.then(this.showValidation)
.catch(this.showError);
this.$fhcApi.factory
.studstatus.leitung.reopen(gruende)
.then(this.showResults);
},
actionPause(evt) {
var antraege = evt || this.selectedData;
this.$refs.loader.show();
this.$fhcApi.factory
.studstatus.leitung.pause(antraege)
.then(this.showResults);
},
actionUnpause(evt) {
var antraege = evt || this.selectedData;
this.$refs.loader.show();
this.$fhcApi.factory
.studstatus.leitung.unpause(antraege)
.then(this.showResults);
},
actionObject(evt) {
var antraege = evt || this.selectedData;
this.$refs.loader.show();
axios
.all(
antraege.map(
antrag => axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Leitung/objectAntrag/',
{
studierendenantrag_id: antrag.studierendenantrag_id
}
)
)
)
.then(this.showValidation)
.catch(this.showError);
this.$fhcApi.factory
.studstatus.leitung.object(antraege)
.then(this.showResults);
},
actionoObjectionDeny(evt, gruende) {
var antraege = evt || this.selectedData;
@@ -211,7 +170,7 @@ export default {
var currentAntrag = antraege.pop();
if (currentAntrag) {
GrundPopup
.popup(this.p.t('studierendenantrag', 'title_grund', {id: currentAntrag.studierendenantrag_id}), {
.popup(this.$p.t('studierendenantrag', 'title_grund', {id: currentAntrag.studierendenantrag_id}), {
countRemaining : antraege.length,
optional: true
})
@@ -230,84 +189,31 @@ export default {
.catch(() => {});
} else {
this.$refs.loader.show();
axios
.all(
gruende.map(
antrag => axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Leitung/objectionDeny/',
{
studierendenantrag_id: antrag.studierendenantrag_id,
grund: antrag.grund
}
)
)
)
.then(this.showValidation)
.catch(this.showError);
this.$fhcApi.factory
.studstatus.leitung.denyObjection(gruende)
.then(this.showResults);
}
},
actionObjectionApprove(evt, gruende) {
var antraege = evt || this.selectedData;
this.$refs.loader.show();
axios
.all(
antraege.map(
antrag => axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Leitung/objectionApprove/',
{
studierendenantrag_id: antrag.studierendenantrag_id
}
)
)
)
.then(this.showValidation)
.catch(this.showError);
this.$fhcApi.factory
.studstatus.leitung.approveObjection(antraege)
.then(this.showResults);
},
actionCancel(evt) {
var antraege = evt || this.selectedData;
this.$refs.loader.show();
axios
.all(
antraege.map(
antrag => axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Abmeldung/cancelAntrag/',
{
antrag_id: antrag.studierendenantrag_id
}
)
)
)
.then(this.showValidation)
.catch(this.showError);
this.$fhcApi.factory
.studstatus.abmeldung.cancel(antraege)
.then(this.showResults);
},
showValidation(results) {
var errors = results.filter(res => res.data.error);
showResults(results) {
let fulfilled = results.filter(res => res.status == 'fulfilled');
this.$refs.loader.hide();
if (errors.length) {
let errorMsg = errors.map(
error =>
'Antrag ' +
JSON.parse(error.config.data).studierendenantrag_id +
'\n' +
Object.values(error.data.retval).join('\n')
).join('\n');
BsAlert.popup(errorMsg, {dialogClass: 'alert alert-danger'});
}
this.reload();
},
showError(error) {
this.$refs.loader.hide();
let msg = error.response.data;
if (msg.replace(/^\s+/, '').substr(0, 9) == '<!DOCTYPE' || msg.replace(/^\s+/, '').substr(0, 4).toLowerCase() == '<div')
msg = error.message;
BsAlert.popup(msg, {dialogClass: 'alert alert-danger'});
//fulfilled.forEach(a => this.$fhcAlert.alertDefault('success', '#' + a.value.data, 'Approved, ...'));
if (fulfilled.length)
this.reload();
}
},
created() {
@@ -338,6 +244,7 @@ export default {
ref="table"
:stg-a="stgkzA"
:stg-l="stgkzL"
:filter="filter"
v-model:columnData="columns"
v-model:selectedData="selectedData"
@action:approve="actionApprove"
@@ -347,6 +254,9 @@ export default {
@action:objectionDeny="actionoObjectionDeny"
@action:objectionApprove="actionObjectionApprove"
@action:cancel="actionCancel"
@action:pause="actionPause"
@action:unpause="actionUnpause"
@reload="reload"
>
</leitung-table>
@@ -1,13 +1,11 @@
import ActionsNew from './Actions/New.js';
import ActionsColumns from './Actions/Columns.js';
import Phrasen from '../../../mixins/Phrasen.js';
export default {
components: {
ActionsNew,
ActionsColumns
},
mixins: [Phrasen],
props: {
selectedData: Array,
columns: Array,
@@ -67,17 +65,17 @@ export default {
<div class="studierendenantrag-leitung-actions fhc-table-actions d-flex flex-wrap justify-content-between mb-2">
<div class="d-flex align-items-center gap-2">
<actions-new @reload="$emit('reload')"></actions-new>
<button type="button" class="btn btn-outline-secondary" @click="$emit('reload')" :title="p.t('table','reload')">
<button type="button" class="btn btn-outline-secondary" @click="$emit('reload')" :title="$p.t('table','reload')">
<i class="fa-solid fa-rotate-right"></i>
</button>
<span>{{p.t('table', 'with_selected', {count: selectedData.length})}}</span>
<button v-if="stgL.length" :disabled="!selectedCanBeApproved" type="button" class="btn btn-outline-secondary" @click="$emit('action:approve')">{{p.t('studierendenantrag', 'btn_approve')}}</button>
<button v-if="stgL.length" :disabled="!selectedCanBeRejected" type="button" class="btn btn-outline-secondary" @click="$emit('action:reject')">{{p.t('studierendenantrag', 'btn_reject')}}</button>
<button v-if="stgA.length" :disabled="!selectedCanBeReopened" type="button" class="btn btn-outline-secondary" @click="$emit('action:reopen')">{{p.t('studierendenantrag', 'btn_reopen')}}</button>
<span>{{$p.t('table', 'with_selected', {count: selectedData.length})}}</span>
<button v-if="stgL.length" :disabled="!selectedCanBeApproved" type="button" class="btn btn-outline-secondary" @click="$emit('action:approve')">{{$p.t('studierendenantrag', 'btn_approve')}}</button>
<button v-if="stgL.length" :disabled="!selectedCanBeRejected" type="button" class="btn btn-outline-secondary" @click="$emit('action:reject')">{{$p.t('studierendenantrag', 'btn_reject')}}</button>
<button v-if="stgA.length" :disabled="!selectedCanBeReopened" type="button" class="btn btn-outline-secondary" @click="$emit('action:reopen')">{{$p.t('studierendenantrag', 'btn_reopen')}}</button>
</div>
<div>
<button type="button" class="btn btn-link" data-bs-toggle="collapse" href="#columns" :title="p.t('table','spaltenEinAusblenden')"><i class="fa fa-table-columns"></i></button>
<button type="button" class="btn btn-link" @click="$emit('download')" :title="p.t('table','download')"><i class="fa fa-download"></i></button>
<button type="button" class="btn btn-link" data-bs-toggle="collapse" href="#columns" :title="$p.t('table','spaltenEinAusblenden')"><i class="fa fa-table-columns"></i></button>
<button type="button" class="btn btn-link" @click="$emit('download')" :title="$p.t('table','download')"><i class="fa fa-download"></i></button>
</div>
<div class="col-12">
<actions-columns id="columns" class="collapse" :columns="columns"></actions-columns>
@@ -6,9 +6,6 @@ export default {
toggleColumn(col) {
col.visible = !col.visible;
col.original.toggle()
},
show() {
}
},
template: `
@@ -1,27 +1,24 @@
import BsAlert from '../../../Bootstrap/Alert.js';
import BsModal from '../../../Bootstrap/Modal.js';
import Phrasen from '../../../../mixins/Phrasen.js';
export default {
components: {
BsModal,
AutoComplete: primevue.autocomplete
},
mixins: [
Phrasen
],
emits: [
'reload'
],
data() {
return {
data: [],
student: ''
student: '',
abortController: null
}
},
computed: {
newUrl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/lehre/Studierendenantrag/abmeldung/' + this.student.prestudent_id;
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/lehre/Studierendenantrag/abmeldungStgl/' + this.student.prestudent_id;
}
},
methods: {
@@ -32,58 +29,74 @@ export default {
class: 'position-absolute top-0 start-0 w-100 h-100'
}), {
dialogClass: 'modal-fullscreen'
}, this.p.t('studierendenantrag', 'antrag_header')).then(() => {
}, this.$p.t('studierendenantrag', 'antrag_header')).then(() => {
this.$emit('reload');
this.student = '';
});
},
loadData(evt) {
axios.post(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Abmeldung/getStudiengaengeAssistenz/',
evt
).then(
result => {
if (result.data.error) {
BsAlert.popup(result.data.retval, {dialogClass: 'alert alert-danger'});
} else {
this.data = result.data.retval;
}
return result;
}
);
if( evt.query.length < 2 )
{
return false;
}
if (this.abortController instanceof AbortController
&& this.abortController.signal.aborted === false)
{
this.abortController.abort();
}
this.abortController = new AbortController();
this.$fhcApi.factory
.studstatus.leitung.getPrestudents(evt.query, this.abortController.signal)
.then(result => {
this.data = result.data;
this.abortController = null;
})
.catch(error => {
if (this.abortController instanceof AbortController
&& this.abortController.signal.aborted === false)
{
this.abortController.abort();
}
this.$fhcAlert.handleSystemError(error);
});
}
},
template: `
<div class="studierendenantrag-leitung-actions-new" v-if="data">
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#newAntragModal">
<i class="fa fa-plus"></i>
{{p.t('studierendenantrag','btn_new')}}
{{$p.t('studierendenantrag','btn_new')}}
</button>
<div ref="modal" class="modal fade" id="newAntragModal" tabindex="-1" aria-labelledby="newAntragModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="newAntragModalLabel">{{p.t('studierendenantrag','title_new_Abmeldung')}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="p.t('ui','schliessen')"></button>
<h5 class="modal-title" id="newAntragModalLabel">{{$p.t('studierendenantrag','title_new_Abmeldung')}}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$p.t('ui','schliessen')"></button>
</div>
<div class="modal-body">
<label for="newAntragModalAutoComplete">{{p.t('person','studentIn')}}</label>
<label for="newAntragModalAutoComplete">{{$p.t('person','studentIn')}}</label>
<div>
<auto-complete
class="w-100"
v-model="student"
:suggestions="data"
optionLabel = "name"
option-label = "name"
@complete="loadData"
inputId="newAntragModalAutoComplete"
input-id="newAntragModalAutoComplete"
dropdown
dropdown-mode="current"
>
<template #option="slotProps">
<div :title="slotProps.option.prestudent_id">
{{slotProps.option.name}}
{{slotProps.option.name}}
</div>
</template>
<template #empty>
<div class="text-muted px-3 py-2">
{{ $p.t('ui/keineEintraegeGefunden') }}
</div>
</template>
</auto-complete>
@@ -94,7 +107,7 @@ export default {
class="btn btn-primary"
:disabled="!this.student"
@click.prevent="openForm">
{{p.t('studierendenantrag','btn_create')}}
{{$p.t('studierendenantrag','btn_create')}}
</button>
</div>
</div>
@@ -1,11 +1,8 @@
import BsAlert from '../../Bootstrap/Alert.js';
import Phrasen from '../../../mixins/Phrasen.js';
export default {
mixins: [
BsAlert,
Phrasen
],
props: {
placeholder: String,
@@ -46,17 +43,17 @@ export default {
<div>
<textarea ref="input" class="form-control" :class="{'is-invalid' : isInvalid}" v-model="value"></textarea>
<div v-if="isInvalid" class="invalid-feedback">
{{p.t('kvp','new.error.required')}}
{{$p.t('kvp','new.error.required')}}
</div>
</div>
</template>
<template v-slot:footer>
<div v-if="countRemaining > 0" class="form-check flex-grow-1">
<input ref="check" type="checkbox" class="form-check-input" id="cbid" v-model="check">
<label class="form-check-label" for="cbid">{{p.t('studierendenantrag','fuer_alle_uebernehmen')}}</label>
<label class="form-check-label" for="cbid">{{$p.t('studierendenantrag','fuer_alle_uebernehmen')}}</label>
</div>
<button type="button" class="btn btn-primary" @click="submit">{{p.t('ui','ok')}}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{p.t('ui','cancel')}}</button>
<button type="button" class="btn btn-primary" @click="submit">{{$p.t('ui','ok')}}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{$p.t('ui','cancel')}}</button>
</template>
</bs-modal>`
}
@@ -1,23 +1,73 @@
import Phrasen from '../../../mixins/Phrasen.js';
const LOCAL_STORAGE_ID = 'studierendenantrag_leitung_2023-11-14_header_filter';
export default {
mixins: [Phrasen],
props: {
stgs: Array
},
emits: [
'input'
],
data() {
return {
todo_value: '',
stg_value: ''
}
},
computed: {
value() {
const a = [];
if (this.todo_value)
a.push(this.todo_value);
if (this.stg_value)
a.push(this.stg_value);
return a.join('/');
}
},
watch: {
value(n) {
window.localStorage.setItem(LOCAL_STORAGE_ID, n);
this.$emit('input', n);
}
},
created() {
var values = 'todo';
const savedPath = window.localStorage.getItem(LOCAL_STORAGE_ID);
if (savedPath !== null) {
values = savedPath;
}
values = values.split('/');
if (values.length) {
if (values.length == 1) {
if (values[0] == 'todo')
values.push('');
else
values.unshift('');
}
this.stg_value = values.pop();
this.todo_value = values.pop();
}
},
template: `
<div class="studierendenantrag-leitung-header fhc-table-header d-flex align-items-center mb-2 gap-2">
<h3 class="h5 col m-0">{{p.t('studierendenantrag', 'studierendenantraege')}}</h3>
<div v-if="stgs.length > 1" class="col-auto">
<select ref="stg_select" class="form-select" @input="$emit('input', $event)">
<option value="">{{p.t('global', 'alle')}}</option>
<option v-for="stg in stgs" :key="stg.studiengang_kz" :value="stg.studiengang_kz">
{{stg.bezeichnung}} ({{stg.orgform}})
</option>
</select>
<h3 class="h5 col m-0">{{$p.t('studierendenantrag', 'studierendenantraege')}}</h3>
<div class="col-auto row row-cols-lg-auto g-3 align-items-center">
<div class="col-12">
<select class="form-select" v-model="todo_value">
<option value="todo">{{$p.t('studierendenantrag', 'filter_todo')}}</option>
<option value="">{{$p.t('studierendenantrag', 'filter_all')}}</option>
</select>
</div>
<div class="col-12">
<select v-if="stgs.length > 1" class="form-select" v-model="stg_value">
<option value="">{{$p.t('global', 'alle')}}</option>
<option v-for="stg in stgs" :key="stg.studiengang_kz" :value="stg.studiengang_kz">
{{stg.bezeichnung}}
</option>
</select>
</div>
</div>
</div>
`
@@ -1,14 +1,12 @@
import BsAlert from '../../Bootstrap/Alert.js';
import {CoreFetchCmpt} from "../../Fetch.js";
import Phrasen from '../../../mixins/Phrasen.js';
export default {
components: {
CoreFetchCmpt
},
mixins: [
BsAlert,
Phrasen
BsAlert
],
props: {
footer: Boolean,
@@ -18,6 +16,7 @@ export default {
data(){
return {
lvs: null,
repeat_last: false,
refresh: true,
result: false,
check: false
@@ -37,18 +36,17 @@ export default {
},
methods: {
setlvs(param) {
if(param.error)
{
this.$refs.fetchCompt.error = true;
this.$refs.fetchCompt.errorMessage = param.retval;
this.repeat_last = !!param.repeat_last;
if (this.repeat_last) {
delete param.repeat_last;
}
else
this.lvs = param.retval;
this.lvs = param;
},
loadlvs() {
if (!this.antragId)
return new Promise(() => {});
return axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Antrag/Wiederholung/getLvs/' + this.antragId);
return this.$fhcApi.factory
.studstatus.wiederholung.getLvs(this.antragId);
},
submit(result) {
this.result = [result, this.check];
@@ -67,7 +65,7 @@ export default {
options = { default: options };
return BsAlert.popup.bind(this)(msg, options);
},
template: `<bs-modal ref="modalContainer" class="bootstrap-prompt" v-bind="$props">
template: `<bs-modal ref="modalContainer" class="bootstrap-prompt" v-bind="$props" dialog-class="modal-lg">
<template v-slot:title>
<slot></slot>
</template>
@@ -79,29 +77,29 @@ export default {
@data-fetched="setlvs">
<template #default>
<div v-if="lvzugelassenLength == 0">
{{p.t('studierendenantrag','error_no_lvs')}}
{{$p.t('studierendenantrag','error_no_lvs')}}
</div>
<table v-else class="table caption-top" v-for="(lv_arr, sem) in lvzugelassen" :key="sem">
<caption>
<span class="d-flex justify-content-between">
<span>{{ p.t('studierendenantrag',['title_lv_nicht_zugelassen', 'title_lv_wiederholen'][sem.substr(0,1)-1]) }}</span>
<span>{{ $p.t('studierendenantrag',['title_lv_nicht_zugelassen', 'title_lv_wiederholen'][repeat_last ? 1 : sem.substr(0,1)-1]) }}</span>
<span>{{sem.substr(1)}}</span>
</span>
</caption>
<thead v-if="lv_arr !== null">
<tr>
<th scope="col">{{p.t('ui','bezeichnung')}}</th>
<th scope="col">{{p.t('lehre','lehrform')}}</th>
<th scope="col">{{$p.t('ui','bezeichnung')}}</th>
<th scope="col">{{$p.t('lehre','lehrform')}}</th>
<th scope="col">ECTS</th>
<th scope="col">{{p.t('lehre','note')}}</th>
<th scope="col">{{$p.t('lehre','note')}}</th>
<th scope="col">
{{p.t('global','anmerkung')}}
{{$p.t('global','anmerkung')}}
</th>
</tr>
</thead>
<tbody>
<tr v-if="lv_arr === null" class="table-warning">
<td colspan="5">{{p.t('studierendenantrag/error_stg_last_semester')}}</td>
<td colspan="5">{{$p.t('studierendenantrag/error_stg_last_semester')}}</td>
</tr>
<template v-else>
<tr v-for="lv in lv_arr">
@@ -140,10 +138,10 @@ export default {
<template v-if="footer" v-slot:footer>
<div v-if="countRemaining > 0" class="form-check flex-grow-1">
<input ref="check" type="checkbox" class="form-check-input" id="cbid" v-model="check">
<label class="form-check-label" for="cbid">{{p.t('studierendenantrag','fuer_x_uebernehmen', {count: countRemaining})}}</label>
<label class="form-check-label" for="cbid">{{$p.t('studierendenantrag','fuer_x_uebernehmen', {count: countRemaining})}}</label>
</div>
<button type="button" class="btn btn-primary" @click="submit(true)">{{p.t('studierendenantrag','btn_approve')}}</button>
<button v-if="countRemaining > 0" type="button" class="btn btn-secondary" @click="submit(false)">{{p.t('ui','skip')}}</button>
<button type="button" class="btn btn-primary" @click="submit(true)">{{$p.t('studierendenantrag','btn_approve')}}</button>
<button v-if="countRemaining > 0" type="button" class="btn btn-secondary" @click="submit(false)">{{$p.t('ui','skip')}}</button>
</template>
</bs-modal>`
}
@@ -1,7 +1,6 @@
import BsModal from '../../Bootstrap/Modal.js';
import {CoreFetchCmpt} from '../../Fetch.js';
import LvPopup from './LvPopup.js';
import Phrasen from '../../../mixins/Phrasen.js';
import { dateFilter } from '../../../tabulator/filters/Dates.js';
export default {
@@ -10,12 +9,12 @@ export default {
CoreFetchCmpt,
LvPopup
},
mixins: [Phrasen],
props: {
selectedData: Array,
columnData: Array,
stgL: Array,
stgA: Array
stgA: Array,
filter: String
},
emits: [
'update:columnData',
@@ -26,13 +25,12 @@ export default {
'action:object',
'action:objectionDeny',
'action:objectionApprove',
'action:cancel'
'action:cancel',
'action:pause',
'action:unpause'
],
data() {
return {
ajaxUrl: FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Leitung/getAntraege/',
table: null,
lastHistoryClickedId: null,
historyData: [],
@@ -41,22 +39,46 @@ export default {
},
methods: {
reload(stg) {
this.table.replaceData(this.ajaxUrl + (stg || ''));
this.table.setData('/' + (stg || ''));
},
download() {
this.table.download("csv", "data.csv");
this.table.download("csv", "data.csv", {
delimiter: ';',
bom: true
});
},
getHistory() {
if (this.lastHistoryClickedId === null)
return null;
return axios.get(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Leitung/getHistory/' +
this.lastHistoryClickedId
).then(res => {
this.historyData = res.data.retval.sort((a, b) => a.insertamum > b.insertamum);
});
return this.$fhcApi.factory
.studstatus.leitung.getHistory(this.lastHistoryClickedId)
.then(res => {
this.historyData = res.data.sort((a, b) => a.insertamum > b.insertamum);
})
.catch(this.$fhcApi.handleSystemError);
},
getHistoryStatus(data, index) {
if (data.insertvon == 'Studienabbruch')
return this.$p.t('studierendenantrag/status_stop');
if (index > 0 && this.historyData[index-1].studierendenantrag_statustyp_kurzbz == 'Pause') {
if (index > 1 && this.historyData[index-2].studierendenantrag_statustyp_kurzbz != 'Pause') {
// NOTE(chris): this is a AbmeldungStgl Pause right after a manual Pause
if (data.studierendenantrag_statustyp_kurzbz == 'Pause')
return data.typ;
// NOTE(chris): this is a manual Pause resumed
else
return data.typ + ' (' + this.$p.t('studierendenantrag/status_unpaused') + ')';
}
// NOTE(chris): a series of pause stati always starts with a manual and alternate afterwards
let i = 2;
while (index-i > 0 && this.historyData[index-i].studierendenantrag_statustyp_kurzbz == 'Pause') i++;
if (data.studierendenantrag_statustyp_kurzbz == 'Pause')
i++;
return i%2
? data.typ
: data.typ + ' (' + this.$p.t('studierendenantrag/status_unpaused') + ')';
}
return data.typ;
},
showHistoryGrund(grund) {
this.$refs.modalGrund.$el.addEventListener(
@@ -73,12 +95,13 @@ export default {
this.$refs.lvList.show();
}
},
mounted() {
async mounted() {
await this.$p.loadCategory(['lehre', 'studierendenantrag', 'person', 'global', 'ui']);
function dateFormatter(cell)
{
let val = cell.getValue();
if (!val)
return '';
return '&nbsp;';
let date = new Date(val);
return date.toLocaleDateString();
}
@@ -86,9 +109,10 @@ export default {
this.table = new Tabulator(this.$refs.table, {
placeholder:"Keine zu bearbeitenden Datensätze",
movableColumns: true,
height: '50vh',
layout: "fitDataStretch", // TODO(chris): wont work when changed
ajaxURL: this.ajaxUrl,
height: '65vh',
layout: "fitDataFill",
ajaxURL: '/' + (this.filter || ''),
ajaxRequestFunc: this.$fhcApi.factory.studstatus.leitung.getAntraege,
persistence: { // NOTE(chris): do not store column titles
sort: true, //persist column sorting
filter: true, //persist filters
@@ -97,7 +121,7 @@ export default {
page: true, //persist page
columns: ["width", "visible"], //persist columns
},
persistenceID: 'studierendenantrag_leitung',
persistenceID: 'studierendenantrag_leitung_2023-11-14',
columns: [{
formatter: 'rowSelection',
titleFormatter: 'rowSelection',
@@ -108,10 +132,11 @@ export default {
headerSort: false
}, {
field: 'studierendenantrag_id',
title: '#'
title: '#',
sorter: 'number'
}, {
field: 'bezeichnung',
title: this.p.t('lehre', 'studiengang'),
title: this.$p.t('lehre', 'studiengang'),
headerFilter: 'list',
headerFilterParams: {
valuesLookup: true,
@@ -120,7 +145,7 @@ export default {
}
}, {
field: 'orgform',
title: this.p.t('lehre', 'organisationsform'),
title: this.$p.t('lehre', 'organisationsform'),
headerFilter: 'list',
headerFilterParams: {
valuesLookup: true,
@@ -129,7 +154,7 @@ export default {
}
}, {
field: 'typ',
title: this.p.t('studierendenantrag', 'antrag_typ'),
title: this.$p.t('studierendenantrag', 'antrag_typ'),
headerFilter: 'list',
headerFilterParams: {
valuesLookup: true,
@@ -137,90 +162,184 @@ export default {
autocomplete: true,
},
formatter: (cell, formatterParams, onRendered) => {
return this.p.t('studierendenantrag','antrag_typ_' + cell.getValue());
return this.$p.t('studierendenantrag','antrag_typ_' + cell.getValue());
}
}, {
field: 'statustyp',
title: this.p.t('studierendenantrag', 'antrag_status'),
title: this.$p.t('studierendenantrag', 'antrag_status'),
headerFilter: 'list',
headerFilterParams: {
valuesLookup: true,
clearable: true,
autocomplete: true,
},
formatter: (cell, formatterParams, onRendered) => {
let data = cell.getData();
let status = cell.getValue();
if (data.status_insertvon == 'Studienabbruch' && data.status == 'Pause')
status = this.$p.t('studierendenantrag/status_stop');
let link = document.createElement('a');
link.href = "#";
link.innerHTML = status;
link.addEventListener('click', e => {
e.preventDefault();
this.lastHistoryClickedId = cell.getData().studierendenantrag_id;
this.$refs.historyLoader.fetchData();
this.$refs.history.show();
});
return link;
}
}, {
field: 'matrikelnr',
title: this.p.t('person', 'personenkennzeichen'),
title: this.$p.t('person', 'personenkennzeichen'),
headerFilter: 'input'
}, {
field: 'prestudent_id',
title: this.p.t('lehre', 'prestudent'),
title: this.$p.t('lehre', 'prestudent'),
headerFilter: 'input'
}, {
field: 'name',
title: this.p.t('global', 'name'),
mutator: (value, data) => (data.vorname + ' ' + data.nachname).replace(/^\s*(.*)\s*$/, '$1'),
title: this.$p.t('global', 'name'),
mutator: (value, data) => (data.nachname + ' ' + data.vorname).replace(/^\s*(.*)\s*$/, '$1'),
headerFilter: 'input'
}, {
field: 'datum',
title: this.p.t('global', 'datum'),
title: this.$p.t('global', 'datum'),
formatter: dateFormatter,
headerFilterFunc: 'dates',
headerFilter: dateFilter
}, {
field: 'datum_wiedereinstieg',
title: this.p.t('studierendenantrag', 'antrag_datum_wiedereinstieg'),
title: this.$p.t('studierendenantrag', 'antrag_datum_wiedereinstieg'),
formatter: dateFormatter,
headerFilterFunc: 'dates',
headerFilter: dateFilter
}, {
field: 'grund',
title: this.p.t('studierendenantrag', 'antrag_grund'),
title: this.$p.t('studierendenantrag', 'antrag_grund'),
formatter: (cell, formatterParams, onRendered) => {
let link = document.createElement('a'),
val = cell.getValue();
link.href = "#modal-grund";
link.setAttribute('data-bs-toggle', 'modal');
link.innerHTML = this.p.t('studierendenantrag', 'antrag_grund');
link.innerHTML = this.$p.t('studierendenantrag', 'antrag_grund');
link.addEventListener('click', () => {
this.$refs.modalGrundPre.innerHTML = val;
});
return val ? link : '';
return val ? link : '&nbsp;';
}
}, {
field: 'dms_id',
title: this.p.t('studierendenantrag', 'antrag_dateianhaenge'),
title: this.$p.t('studierendenantrag', 'antrag_dateianhaenge'),
formatter: (cell, formatterParams, onRendered) => {
let val = cell.getValue();
if (!val)
return '';
return '<a href="' + FHC_JS_DATA_STORAGE_OBJECT.app_root +
return '&nbsp;';
let link = document.createElement('a');
link.href = FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/lehre/Antrag/Attachment/show/' + val + '" target="_blank"><i class="fa fa-paperclip" aria-hidden="true"></i> ' + this.p.t('studierendenantrag', 'antrag_anhang') + '</a>';
'/lehre/Antrag/Attachment/show/' +
val;
link.setAttribute('target', '_blank');
link.innerHTML = '<i class="fa fa-paperclip" aria-hidden="true"></i>';
link.append(this.$p.t('studierendenantrag/antrag_anhang'));
return link;
}
}, {
field: 'actions',
frozen: true,
title: this.$p.t('ui', 'aktion'),
headerFilter: false,
headerSort: false,
formatter: (cell, formatterParams, onRendered) => {
let container = document.createElement('div'),
data = cell.getData();
container.className = "d-flex gap-2";
if ((data.typ == 'Abmeldung' || data.typ == 'Unterbrechung') && (data.status == 'Genehmigt')) {
let allowed_status_for_download = [];
switch (data.typ) {
case 'Abmeldung':
allowed_status_for_download = ['Genehmigt'];
break;
case 'AbmeldungStgl':
allowed_status_for_download = ['Genehmigt', 'Beeinsprucht', 'EinspruchAbgelehnt', 'Abgemeldet'];
break;
case 'Unterbrechung':
allowed_status_for_download = ['Genehmigt', 'EmailVersandt'];
break;
case 'Wiederholung':
allowed_status_for_download = ['Abgemeldet'];
break;
}
if (allowed_status_for_download.includes(data.status)) {
// NOTE(chris): Download PDF
let button = document.createElement('a');
button.innerHTML = '<i class="fa-solid fa-download" title="' + this.p.t('studierendenantrag', 'btn_download_antrag') + '"></i>';
// NOTE(chris): phrasen in attribues don't work if they are not preloaded
// it work in this case because the category has already been loaded before
button.innerHTML = '<i class="fa-solid fa-download" title="' + this.$p.t('studierendenantrag', 'btn_download_antrag') + '"></i>';
button.className = "btn btn-outline-secondary";
button.target = "_blank";
button.href = FHC_JS_DATA_STORAGE_OBJECT.app_root +
'content/pdfExport.php?xml=Antrag' + data.typ + '.xml.php&xsl=Antrag' + data.typ + '&id=' + data.studierendenantrag_id + '&output=pdf';
container.append(button);
}
if (data.typ == 'Wiederholung' && (data.status == 'ErsteAufforderungVersandt' || data.status == 'ZweiteAufforderungVersandt')) {
// NOTE(chris): Pause
let button = document.createElement('button');
let icon = document.createElement('i');
let span = document.createElement('span');
icon.className = "fa-solid fa-pause";
icon.setAttribute('aria-hidden', true);
icon.setAttribute('title', this.$p.t('studierendenantrag', 'btn_pause'));
span.className = "fa-sr-only";
span.append(this.$p.t('studierendenantrag', 'btn_pause'));
button.append(icon);
button.append(span);
button.className = "btn btn-outline-secondary";
button.addEventListener('click', () => this.$emit('action:pause', [cell.getData()]));
container.append(button);
}
let canUnpause = data.status == 'Pause' && !['AbmeldungStgl', 'Studienabbruch'].includes(data.status_insertvon);
if (!canUnpause && data.status == 'Pause' && data.status_insertvon == 'AbmeldungStgl') {
canUnpause = cell.getTable().getData().filter(row =>
row.prestudent_id == data.prestudent_id
&& row.typ == 'AbmeldungStgl'
&& row.status == 'Zurueckgezogen'
&& row.status_insertamum == data.status_insertamum
).length;
}
if (canUnpause) {
// NOTE(chris): Unpause
let button = document.createElement('button');
let icon = document.createElement('i');
let span = document.createElement('span');
icon.className = "fa-solid fa-play";
icon.setAttribute('aria-hidden', true);
icon.setAttribute('title', this.$p.t('studierendenantrag', 'btn_unpause'));
span.className = "fa-sr-only";
span.append(this.$p.t('studierendenantrag', 'btn_unpause'));
button.append(icon);
button.append(span);
button.className = "btn btn-outline-secondary";
button.addEventListener('click', () => this.$emit('action:unpause', [cell.getData()]));
container.append(button);
}
if (data.typ == 'AbmeldungStgl' && data.status == 'Genehmigt') {
// NOTE(chris): Object
let button = document.createElement('button');
button.innerHTML = this.p.t('studierendenantrag', 'btn_object');
button.append(this.$p.t('studierendenantrag', 'btn_object'));
button.className = "btn btn-outline-secondary";
button.addEventListener('click', () => this.$emit('action:object', [cell.getData()]));
container.append(button);
@@ -229,14 +348,14 @@ export default {
if (data.typ == 'AbmeldungStgl' && data.status == 'Beeinsprucht') {
// NOTE(chris): Deny Objection
let button = document.createElement('button');
button.innerHTML = this.p.t('studierendenantrag', 'btn_objection_deny');
button.append(this.$p.t('studierendenantrag', 'btn_objection_deny'));
button.className = "btn btn-outline-secondary";
button.addEventListener('click', () => this.$emit('action:objectionDeny', [cell.getData()]));
container.append(button);
// NOTE(chris): Approve Objection
button = document.createElement('button');
button.innerHTML = this.p.t('studierendenantrag', 'btn_objection_approve');
button.append(this.$p.t('studierendenantrag', 'btn_objection_approve'));
button.className = "btn btn-outline-secondary";
button.addEventListener('click', () => this.$emit('action:objectionApprove', [cell.getData()]));
container.append(button);
@@ -246,7 +365,7 @@ export default {
// NOTE(chris): Reopen
if (data.typ == 'Wiederholung' && data.status == 'Verzichtet') {
let button = document.createElement('button');
button.innerHTML = this.p.t('studierendenantrag', 'btn_reopen');
button.append(this.$p.t('studierendenantrag', 'btn_reopen'));
button.className = "btn btn-outline-secondary";
button.addEventListener('click', () => this.$emit('action:reopen', [cell.getData()]));
container.append(button);
@@ -254,7 +373,7 @@ export default {
// NOTE(chris): Lv Zuweisen
if (data.typ == 'Wiederholung' && (data.status == 'Erstellt' || data.status == 'Lvszugewiesen')) {
let button = document.createElement('a');
button.innerHTML = this.p.t('studierendenantrag', 'btn_lvzuweisen');
button.append(this.$p.t('studierendenantrag', 'btn_lvzuweisen'));
button.className = "btn btn-outline-secondary";
button.href = FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
@@ -267,16 +386,16 @@ export default {
class: 'position-absolute top-0 start-0 w-100 h-100'
}), {
dialogClass: 'modal-fullscreen'
}, this.p.t('studierendenantrag', 'title_lvzuweisen', cell.getData())).then(() => {
}, this.$p.t('studierendenantrag', 'title_lvzuweisen', cell.getData())).then(() => {
this.$emit('reload');
});
};
container.append(button);
}
// NOTE(chris): Cancel
if (data.typ == 'AbmeldungStgl' && data.status == 'Erstellt') {
if (data.typ == 'AbmeldungStgl' && (data.status == 'Erstellt' || data.status == 'Genehmigt' )) {
let button = document.createElement('button');
button.innerHTML = this.p.t('studierendenantrag', 'btn_cancel');
button.append(this.$p.t('studierendenantrag', 'btn_cancel'));
button.className = "btn btn-outline-secondary";
button.addEventListener('click',() => this.$emit('action:cancel', [cell.getData()]));
container.append(button);
@@ -287,7 +406,7 @@ export default {
// NOTE(chris): Approve
if ((data.typ == 'Wiederholung' && data.status == 'Lvszugewiesen') || (data.typ != 'Wiederholung' && data.status == 'Erstellt')) {
let button = document.createElement('button');
button.innerHTML = this.p.t('studierendenantrag', 'btn_approve');
button.append(this.$p.t('studierendenantrag', 'btn_approve'));
button.className = "btn btn-outline-secondary";
button.addEventListener('click', () => this.$emit('action:approve', [cell.getData()]));
container.append(button);
@@ -295,7 +414,7 @@ export default {
// NOTE(chris): Reject (Unterbrechung braucht grund)
if (data.status == 'Erstellt' && data.typ == 'Unterbrechung') {
let button = document.createElement('button');
button.innerHTML = this.p.t('studierendenantrag', 'btn_reject');
button.append(this.$p.t('studierendenantrag', 'btn_reject'));
button.className = "btn btn-outline-secondary";
button.addEventListener('click', () => this.$emit('action:reject', [cell.getData()]));
container.append(button);
@@ -305,16 +424,14 @@ export default {
// NOTE(chris): Show LVs
if (data.typ == 'Wiederholung' && (data.status == 'Lvszugewiesen' || data.status == 'Genehmigt')) {
let button = document.createElement('button');
button.innerHTML = this.p.t('studierendenantrag', 'btn_show_lvs');
button.append(this.$p.t('studierendenantrag', 'btn_show_lvs'));
button.className = "btn btn-outline-secondary";
button.addEventListener('click', () => this.showLVs(cell.getData()));
container.append(button);
}
// TODO(chris): not yet perfect
onRendered(() => {
cell.getColumn().setWidth(true);
});
if (container.innerHTML == '')
container.innerHTML = '&nbsp;';
return container;
}
@@ -325,7 +442,7 @@ export default {
let columnData = [];
for (let col of columns) {
let def = col.getDefinition();
if (def.title) {
if (def.title && !def.frozen) {
columnData.push({
title: def.title,
visible: col.isVisible(),
@@ -338,40 +455,33 @@ export default {
this.table.on("rowSelectionChanged", data => {
this.$emit('update:selectedData', data);
});
this.table.on("cellClick", (e, cell) => {
if (cell.getColumn().getField() == 'statustyp') {
this.lastHistoryClickedId = cell.getData().studierendenantrag_id;
this.$refs.historyLoader.fetchData();
this.$refs.history.show();
}
});
},
template: `
<div class="studierendenantrag-leitung-table">
<div ref="table"></div>
<bs-modal ref="modalGrund" id="modal-grund" class="fade">
<template #title>{{p.t('studierendenantrag', 'antrag_grund')}}</template>
<pre ref="modalGrundPre"></pre>
<template #title>{{$p.t('studierendenantrag', 'antrag_grund')}}</template>
<textarea class="form-control" ref="modalGrundPre" style="width: 100%; height: 250px;" readonly></textarea>
</bs-modal>
<bs-modal ref="history" class="fade">
<template #title>{{p.t('studierendenantrag', 'title_history', {id: lastHistoryClickedId})}}</template>
<template #title>{{$p.t('studierendenantrag', 'title_history', {id: lastHistoryClickedId})}}</template>
<core-fetch-cmpt ref="historyLoader" :api-function="getHistory">
<table v-if="historyData.length" class="table">
<tr v-for="status in historyData" :key="status.studierendenantrag_status_id">
<tr v-for="(status, index) in historyData" :key="status.studierendenantrag_status_id">
<td>{{(new Date(status.insertamum)).toLocaleString()}}</td>
<td>{{status.insertvon}}</td>
<td>{{status.typ}}</td>
<td>{{getHistoryStatus(status, index)}}</td>
<td>
<a v-if="status.grund" href="#modal-grund" data-bs-toggle="modal" @click="showHistoryGrund(status.grund)">
{{p.t('studierendenantrag', 'antrag_grund')}}
{{$p.t('studierendenantrag', 'antrag_grund')}}
</a>
</td>
</tr>
</table>
</core-fetch-cmpt>
</bs-modal>
<lv-popup ref="lvList" class="fade" :antrag-id="lvsData ? lvsData.studierendenantrag_id : null" dialog-class="modal-lg">
{{p.t('studierendenantrag', 'title_show_lvs', lvsData ? lvsData : {name: ''}) }}
<lv-popup ref="lvList" class="fade" :antrag-id="lvsData ? lvsData.studierendenantrag_id : null">
{{$p.t('studierendenantrag', 'title_show_lvs', lvsData ? lvsData : {name: ''}) }}
</lv-popup>
</div>
`
@@ -1,11 +1,9 @@
import StudierendenantragStatus from './Status.js';
import Phrasen from '../../mixins/Phrasen.js';
export default {
components: {
StudierendenantragStatus
},
mixins: [Phrasen],
props: {
antragId: Number,
initialStatusCode: String,
@@ -43,10 +41,11 @@ export default {
methods: {
save() {
this.isloading = true;
const forbiddenLvs = this.lvs1.filter(lv => lv.antrag_zugelassen && !lv._children).map(lv => ({
const forbiddenLvs = this.lvs1.filter(lv => (lv.antrag_zugelassen || this.lvs.repeat_last)
&& !lv._children).map(lv => ({
studierendenantrag_id: this.antragId,
lehrveranstaltung_id: lv.lehrveranstaltung_id,
zugelassen: 0,
zugelassen: this.lvs.repeat_last ? (lv.antrag_zugelassen ? 1 : 2) : 0,
anmerkung: lv.antrag_anmerkung || "",
studiensemester_kurzbz: this.lvs1sem
}));
@@ -57,39 +56,19 @@ export default {
anmerkung: lv.antrag_anmerkung || "",
studiensemester_kurzbz: this.lvs2sem
}));
axios.post(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Antrag/Wiederholung/saveLvs/', {forbiddenLvs, mandatoryLvs})
this.$fhcApi.factory
.studstatus.wiederholung.saveLvs(forbiddenLvs, mandatoryLvs)
.then(response => {
if(!response.data.error) {
this.addAlert('Speichern erfolgreich', 'alert-success');
this.statusCode = response.data.retval[0].studierendenantrag_statustyp_kurzbz;
this.statusMsg = response.data.retval[0].typ;
} else {
this.addAlert(response.data.retval, 'alert-danger');
this.statusCode = 0;
this.statusMsg = 'Error';
}
this.$fhcAlert.alertSuccess('Speichern erfolgreich');
this.statusCode = response.data[0].studierendenantrag_statustyp_kurzbz;
this.statusMsg = response.data[0].typ;
this.isloading = false;
}).catch(error => {
this.addAlert(error.message, 'alert-danger');
})
.catch(error => {
this.statusCode = 0;
this.statusMsg = 'Error';
this.isloading = false;
}).finally(() => {
window.scrollTo(0, 0);
});
},
addAlert(text, type) {
const para = document.createElement("p");
para.innerText = text;
para.className = "alert " + type + " alert-dismissible fade show";
const btn = document.createElement("button");
btn.className = "btn-close";
btn.type = "button";
btn.setAttribute("aria-label", "Close");
btn.setAttribute("data-bs-dismiss", "alert");
para.appendChild(btn);
this.$refs.alertbox.appendChild(para);
}
},
created() {
@@ -97,57 +76,59 @@ export default {
this.statusMsg = this.initialStatusMsg;
},
mounted() {
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Antrag/Wiederholung/getLvs/' + this.antragId).then(
result => {
if(result.data.error)
{
this.addAlert(result.data.retval, 'alert-danger');
this.isloading = true;
}
else
{
let res = {};
for (var k in result.data.retval) {
if (result.data.retval[k] === null) {
const alert = document.createElement('div');
alert.innerHTML = this.p.t('studierendenantrag', 'error_stg_last_semester');
alert.className = 'alert alert-warning';
alert.role = 'alert';
this.$refs["lvtable" + k.substr(0,1)].append(alert);
this.$p
.loadCategory(['ui', 'lehre', 'studierendenantrag', 'global'])
.then(() => this.antragId)
.then(this.$fhcApi.factory.studstatus.wiederholung.getLvs)
.then(result => {
let res = {};
for (var k in result.data) {
if (k === 'repeat_last')
continue;
if (result.data[k] === null) {
const alert = document.createElement('div');
alert.innerHTML = this.$p.t('studierendenantrag', 'error_stg_last_semester');
alert.className = 'alert alert-warning';
alert.role = 'alert';
this.$refs["lvtable" + k.substr(0,1)].append(alert);
continue;
}
let lvs = result.data[k].reduce((obj,lv) => {
obj[lv.studienplan_lehrveranstaltung_id] = lv;
return obj;
}, {});
for (var lv of Object.values(lvs)) {
if (!lv.studienplan_lehrveranstaltung_id_parent)
continue;
if (!lvs[lv.studienplan_lehrveranstaltung_id_parent])
console.error('parent not available');
else {
if (!lvs[lv.studienplan_lehrveranstaltung_id_parent]._children)
lvs[lv.studienplan_lehrveranstaltung_id_parent]._children = [];
lvs[lv.studienplan_lehrveranstaltung_id_parent]._children.push(lv);
}
let lvs = result.data.retval[k].reduce((obj,lv) => {
obj[lv.studienplan_lehrveranstaltung_id] = lv;
return obj;
}, {});
for (var lv of Object.values(lvs)) {
if (!lv.studienplan_lehrveranstaltung_id_parent)
continue;
if (!lvs[lv.studienplan_lehrveranstaltung_id_parent])
console.error('parent not available');
else {
if (!lvs[lv.studienplan_lehrveranstaltung_id_parent]._children)
lvs[lv.studienplan_lehrveranstaltung_id_parent]._children = [];
lvs[lv.studienplan_lehrveranstaltung_id_parent]._children.push(lv);
}
}
res[k] = Object.values(lvs).filter(lv => !lv.studienplan_lehrveranstaltung_id_parent);
let current = res[k];
let index = k.substr(0,1);
var table = new Tabulator(this.$refs["lvtable" + k.substr(0,1)], {
data: current,
dataTree: true,
dataTreeStartExpanded: true, //start with an expanded tree
dataTreeChildIndent: 15,
layout: "fitDataStretch",
columns: [
{title: this.p.t('ui','bezeichnung'), field: "bezeichnung"},
{title: this.p.t('lehre','lehrform'), field: "lehrform_kurzbz"},
{title: "ECTS", field: "ects"},
{title: this.p.t('lehre','note'), field: "note", formatter:(cell, formatterParams, onRendered)=>cell.getValue() || "---"},
{title: (index==1) ? this.p.t('studierendenantrag','lv_nicht_zulassen') : this.p.t('studierendenantrag','lv_wiederholen'), field: "antrag_zugelassen", formatter: (cell, formatterParams, onRendered) => {
}
res[k] = Object.values(lvs).filter(lv => !lv.studienplan_lehrveranstaltung_id_parent);
let current = res[k];
let index = k.substr(0,1);
const options = {
data: current,
dataTree: true,
dataTreeStartExpanded: true, //start with an expanded tree
dataTreeChildIndent: 15,
layout: "fitDataStretch",
columns: [
{title: this.$p.t('ui', 'bezeichnung'), field: "bezeichnung"},
{title: this.$p.t('lehre','lehrform'), field: "lehrform_kurzbz"},
{title: "ECTS", field: "ects"},
{title: this.$p.t('lehre','note'), field: "note", formatter:(cell, formatterParams, onRendered) => cell.getValue() || "---"},
{
title: index == 1 && !result.data.repeat_last ? this.$p.t('studierendenantrag','lv_nicht_zulassen') : this.$p.t('studierendenantrag','lv_wiederholen'),
field: "antrag_zugelassen",
formatter: (cell, formatterParams, onRendered) => {
let data = cell.getData();
if(data._children || !data.zeugnis)
if (data._children || !data.zeugnis)
return "";
let input = document.createElement('input');
input.className = "form-check-input";
@@ -167,84 +148,86 @@ export default {
div.append(input);
return div;
}},
{
title: this.p.t('global','anmerkung'),
field: "antrag_anmerkung",
headerSort:false,
titleFormatter:(cell, formatterParams, onRendered)=>{
let link = document.createElement('a');
link.addEventListener('click', (e) => {
e.preventDefault();
}
},
{
title: this.$p.t('global','anmerkung'),
field: "antrag_anmerkung",
headerSort:false,
titleFormatter:(cell, formatterParams, onRendered)=>{
let link = document.createElement('a');
link.addEventListener('click', (e) => {
e.preventDefault();
});
link.href ="#";
link.title = this.$p.t('studierendenantrag','anmerkung_tooltip');
new bootstrap.Tooltip(link);
let tooltip = document.createElement('span');
tooltip.innerHTML = this.$p.t('global','anmerkung') + " ";
tooltip.append(link);
let icon = document.createElement('i');
link.append(icon);
icon.className = "fa fa-info-circle";
icon.setAttribute("aria-hidden", "true");
icon.style.minWidth = '1em';
return tooltip;
},
formatter: (cell, formatterParams, onRendered) => {
if (this.disabled) {
return cell.getValue() || "";
}
var data = cell.getData();
if (lvs[data.studienplan_lehrveranstaltung_id].antrag_zugelassen)
{
let input = document.createElement('input');
input.className = "form-control";
input.type = "text";
input.value = cell.getValue() || "";
input.addEventListener('input', () => {
lvs[data.studienplan_lehrveranstaltung_id].antrag_anmerkung = input.value;
});
link.href ="#";
link.title = this.p.t('studierendenantrag','anmerkung_tooltip');
new bootstrap.Tooltip(link);
let tooltip = document.createElement('span');
tooltip.innerHTML = this.p.t('global','anmerkung') + " ";
tooltip.append(link);
let icon = document.createElement('i');
link.append(icon);
icon.className = "fa fa-info-circle";
icon.setAttribute("aria-hidden", "true");
icon.style.minWidth = '1em';
return tooltip;
},
formatter: (cell, formatterParams, onRendered) => {
if (this.disabled) {
return cell.getValue() || "";
}
var data = cell.getData();
if (lvs[data.studienplan_lehrveranstaltung_id].antrag_zugelassen)
{
let input = document.createElement('input');
input.className = "form-control";
input.type = "text";
input.value = cell.getValue() || "";
input.addEventListener('input', () => {
lvs[data.studienplan_lehrveranstaltung_id].antrag_anmerkung = input.value;
});
return input;
}
else
{
return "";
}
return input;
}
else
{
return "";
}
}
]
});
}
this.lvs = result.data.retval;
}
]
};
var table = new Tabulator(this.$refs["lvtable" + k.substr(0,1)], options);
}
}
);
this.lvs = result.data;
})
.catch(error => {
this.$fhcAlert.handleSystemError(error);
this.isloading = true;
});
},
template: `
<div class="col-sm-8">
<div ref="alertbox"></div>
<div class="col-sm-10">
<span class="d-flex justify-content-between h4">
<span>{{p.t('studierendenantrag', 'title_lv_nicht_zugelassen')}}</span>
<span>{{lvs.repeat_last ? $p.t('studierendenantrag', 'title_lv_wiederholen') : $p.t('studierendenantrag', 'title_lv_nicht_zugelassen')}}</span>
<span>{{lvs1sem}}</span>
</span>
<div ref="lvtable1" class="mb-3">
</div>
<span class="d-flex justify-content-between h4">
<span>{{p.t('studierendenantrag', 'title_lv_wiederholen')}}</span>
<span>{{$p.t('studierendenantrag', 'title_lv_wiederholen')}}</span>
<span>{{lvs2sem}}</span>
</span>
<div ref="lvtable2">
</div>
<button type="button" @click="save" :disabled="isloading || disabled" class="btn btn-primary my-3">{{p.t('studierendenantrag', 'btn_save_lvs')}}</button>
<button type="button" @click="save" :disabled="isloading || disabled" class="btn btn-primary my-3">{{$p.t('studierendenantrag', 'btn_save_lvs')}}</button>
</div>
<div class="col-sm-4">
<div class="col-sm-2">
<studierendenantrag-status :msg="statusMsg" :severity="statusSeverity"></studierendenantrag-status>
</div>
`
+122
View File
@@ -0,0 +1,122 @@
import accessibility from "../directives/accessibility.js";
export default {
directives: {
accessibility
},
emits: [
'update:modelValue',
'change',
'changed'
],
props: {
config: {
type: [String, Array, Object, Promise],
required: true
},
default: String,
modelValue: [String, Number, Boolean, Array, Object, Date, Function, Symbol],
vertical: Boolean,
border: Boolean
},
data() {
return {
current: null,
tabs: {}
}
},
computed: {
currentTab() {
if (this.tabs[this.current])
return this.tabs[this.current];
return { component: 'div' };
},
value: {
get() {
return this.modelValue;
},
set(v) {
this.$emit('update:modelValue', v);
}
}
},
watch: {
config(n) {
this.initConfig(n);
}
},
methods: {
change(key) {
this.$emit("change", key)
this.current = key;
this.$nextTick(() => this.$emit("changed", key));
},
initConfig(config) {
if (!config)
return;
if (config instanceof Promise)
return config
.then(result => result.data)
.then(this.initConfig)
.catch(this.$fhcAlert.handleSystemError);
if (typeof config === 'string' || config instanceof String)
return this.$fhcApi
.get(config)
.then(result => result.data)
.then(this.initConfig)
.catch(this.$fhcAlert.handleSystemError);
const tabs = {};
function _addToTabs(key, item) {
if (!item.component)
return console.error('Component missing for ' + key);
tabs[key] = {
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(item.component))),
title: item.title || key,
config: item.config,
key
}
}
if (Array.isArray(config))
config.forEach((item, key) => _addToTabs(key, item));
else
Object.entries(config).forEach(([key, item]) => _addToTabs(key, item));
if (this.current === null || !tabs[this.current]) {
if (tabs[this.default])
this.current = this.default;
else
this.current = Object.keys(tabs)[0];
}
this.tabs = tabs;
}
},
created() {
this.initConfig(this.config);
},
template: `
<div class="fhc-tabs d-flex" :class="vertical ? 'align-items-stretch gap-3' : (border ? 'flex-column' : 'flex-column gap-3')" v-if="Object.keys(tabs).length">
<div class="nav" :class="vertical ? 'nav-pills flex-column' : 'nav-tabs'">
<div
v-for="tab in tabs"
:key="tab.key"
class="nav-item nav-link"
:class="{active: tab.key == current}"
@click="change(tab.key)"
:aria-current="tab.key == current ? 'page' : ''"
v-accessibility:tab.[vertical]
>
{{tab.title}}
</div>
</div>
<div :style="vertical ? '' : 'flex: 1 1 0%; height: 0%'" class="overflow-auto flex-grow-1" :class="vertical || !border ? '' : 'p-3 border-bottom border-start border-end'">
<keep-alive>
<component ref="current" :is="currentTab.component" v-model="value" :config="currentTab.config"></component>
</keep-alive>
</div>
</div>`
};
+1 -1
View File
@@ -21,7 +21,7 @@ import {CoreRESTClient} from '../../RESTClient.js';
const CORE_FILTER_CMPT_TIMEOUT = 7000;
/**
*
* TODO(chris): deprecated
*/
export const CoreFilterAPIs = {
/**
+202 -387
View File
@@ -15,9 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {CoreFilterAPIs} from './API.js';
import {CoreRESTClient} from '../../RESTClient.js';
import {CoreFetchCmpt} from '../../components/Fetch.js';
import FilterConfig from './Filter/Config.js';
import FilterColumns from './Filter/Columns.js';
import TableDownload from './Table/Download.js';
//
const FILTER_COMPONENT_NEW_FILTER = 'Filter Component New Filter';
@@ -29,11 +30,18 @@ var _uuid = 0;
*
*/
export const CoreFilterCmpt = {
emits: ['nwNewEntry'],
components: {
CoreFetchCmpt
CoreFetchCmpt,
FilterConfig,
FilterColumns,
TableDownload
},
emits: [
'nwNewEntry',
'click:new'
],
props: {
onNwNewEntry: Function, // NOTE(chris): Hack to get the nwNewEntry listener into $props
title: String,
sideMenu: {
type: Boolean,
@@ -45,7 +53,16 @@ export const CoreFilterCmpt = {
},
tabulatorOptions: Object,
tabulatorEvents: Array,
tableOnly: Boolean
tableOnly: Boolean,
reload: Boolean,
download: {
type: [Boolean, String, Function, Array, Object],
default: false
},
newBtnShow: Boolean,
newBtnClass: [String, Array, Object],
newBtnDisabled: Boolean,
newBtnLabel: String
},
data: function() {
return {
@@ -60,6 +77,7 @@ export const CoreFilterCmpt = {
filterFields: null,
availableFilters: null,
selectedFilter: null,
// FetchCmpt binded properties
fetchCmptRefresh: false,
@@ -68,7 +86,9 @@ export const CoreFilterCmpt = {
fetchCmptDataFetched: null,
tabulator: null,
tableBuilt: false
tableBuilt: false,
tabulatorHasSelector: false,
selectedData: []
};
},
computed: {
@@ -114,7 +134,9 @@ export const CoreFilterCmpt = {
for (let col of columns)
{
// If the column has to be displayed or not
col.visible = selectedFields.indexOf(col.field) >= 0;
/* fields.indexOf(col.field) == -1; ensures displaying formatter colums
e.g. column with rowSelection checkboxes or with custom formatted action buttons */
col.visible = selectedFields.indexOf(col.field) >= 0 || fields.indexOf(col.field) == -1;
if (col.hasOwnProperty('resizable'))
col.resizable = col.visible;
@@ -123,6 +145,14 @@ export const CoreFilterCmpt = {
return columns;
},
fieldIdsForVisibilty() {
if (!this.tableBuilt)
return [];
return this.tabulator.getColumns().filter(col => {
let def = col.getDefinition();
return !def.frozen && def.title;
}).map(col => col.getField());
},
fieldNames() {
if (!this.tableBuilt)
return {};
@@ -135,28 +165,41 @@ export const CoreFilterCmpt = {
if (!this.uuid)
return '';
return '-' + this.uuid;
},
columnsForFilter() {
if (!this.filteredColumns || !this.datasetMetadata)
return [];
const filterTitles = this.filteredColumns.reduce((a,c) => {
a[c.field] = c.title;
return a;
}, {});
return this.datasetMetadata.map(el => ({...el, ...{title: filterTitles[el.name]}}));
}
},
beforeCreate() {
if (!this.tableOnly == !this.filterType)
alert('You can not have a filter-type in table-only mode!');
},
created() {
this.uuid = _uuid++;
if (!this.tableOnly)
this.getFilter(); // get the filter data
},
mounted() {
this.initTabulator();
},
methods: {
initTabulator() {
reloadTable() {
if (this.tableOnly)
this.tabulator.setData();
else
this.getFilter();
},
async initTabulator() {
let placeholder = '< Phrasen Plugin not loaded! >';
if (this.$p) {
await this.$p.loadCategory('ui');
placeholder = this.$p.t('ui/keineDatenVorhanden');
}
// Define a default tabulator options in case it was not provided
let tabulatorOptions = {...{
height: 500,
layout: "fitColumns",
layout: "fitDataStretch",
movableColumns: true,
reactiveData: true
columnDefaults:{
tooltip: true,
},
placeholder,
reactiveData: true,
persistence: true
}, ...(this.tabulatorOptions || {})};
if (!this.tableOnly) {
@@ -164,6 +207,9 @@ export const CoreFilterCmpt = {
tabulatorOptions.columns = this.filteredColumns;
}
if (tabulatorOptions.columns && tabulatorOptions.columns.filter(el => el.formatter == 'rowSelection').length)
this.tabulatorHasSelector = true;
// Start the tabulator with the build options
this.tabulator = new Tabulator(
this.$refs.table,
@@ -177,6 +223,9 @@ export const CoreFilterCmpt = {
this.tabulator.on(evt.event, evt.handler);
}
this.tabulator.on('tableBuilt', () => this.tableBuilt = true);
this.tabulator.on("rowSelectionChanged", data => {
this.selectedData = data;
});
if (this.tableOnly) {
this.tabulator.on('tableBuilt', () => {
const cols = this.tabulator.getColumns();
@@ -194,67 +243,69 @@ export const CoreFilterCmpt = {
}
},
_updateTabulator() {
this.tabulator.setData(this.filteredData);
this.tabulatorHasSelector = this.filteredColumns.filter(el => el.formatter == 'rowSelection').length;
this.tabulator.setColumns(this.filteredColumns);
this.tabulator.setData(this.filteredData);
},
/**
*
*/
getFilter: function() {
//
this.startFetchCmpt(CoreFilterAPIs.getFilter, null, this.render);
},
/**
*
*/
render: function(response) {
if (CoreRESTClient.hasData(response))
{
let data = CoreRESTClient.getData(response);
this.filterName = data.filterName;
this.dataset = data.dataset;
this.datasetMetadata = data.datasetMetadata;
this.fields = data.fields;
this.selectedFields = data.selectedFields;
this.notSelectedFields = this.fields.filter(x => this.selectedFields.indexOf(x) === -1);
this.filterFields = [];
for (let i = 0; i < data.datasetMetadata.length; i++)
{
for (let j = 0; j < data.filters.length; j++)
getFilter() {
if (this.selectedFilter === null)
this.startFetchCmpt(this.$fhcApi.factory.filter.getFilter, null, this.render);
else
this.startFetchCmpt(
this.$fhcApi.factory.filter.getFilterById,
{
if (data.datasetMetadata[i].name == data.filters[j].name)
{
let filter = data.filters[j];
filter.type = data.datasetMetadata[i].type;
filterId: this.selectedFilter
},
this.render
);
},
/**
*
*/
render(response) {
let data = response;
this.filterName = data.filterName;
this.dataset = data.dataset;
this.datasetMetadata = data.datasetMetadata;
this.filterFields.push(filter);
break;
}
this.fields = data.fields;
this.selectedFields = data.selectedFields;
this.notSelectedFields = this.fields.filter(x => this.selectedFields.indexOf(x) === -1);
this.filterFields = [];
for (let i = 0; i < data.datasetMetadata.length; i++)
{
for (let j = 0; j < data.filters.length; j++)
{
if (data.datasetMetadata[i].name == data.filters[j].name)
{
let filter = data.filters[j];
filter.type = data.datasetMetadata[i].type;
this.filterFields.push(filter);
//break;
}
}
}
// If the side menu is active
if (this.sideMenu === true)
{
this.setSideMenu(data);
}
else // otherwise use the dropdown in the filter options
{
this.setDropDownMenu(data);
}
this.updateTabulator();
}
else
// If the side menu is active
if (this.sideMenu === true)
{
console.error(CoreRESTClient.getError(response));
this.setSideMenu(data);
}
else // otherwise use the dropdown in the filter options
{
this.setDropDownMenu(data);
}
this.updateTabulator();
},
/**
* Set the menu
*/
setSideMenu: function(data) {
setSideMenu(data) {
let filters = data.sideMenu.filters;
let personalFilters = data.sideMenu.personalFilters;
let filtersArray = [];
@@ -266,6 +317,7 @@ export const CoreFilterCmpt = {
if (link == null) link = '#';
filtersArray[filtersArray.length] = {
id: filters[filtersCount].filter_id,
link: link + filters[filtersCount].filter_id,
description: filters[filtersCount].desc,
sort: filtersCount,
@@ -280,6 +332,7 @@ export const CoreFilterCmpt = {
if (link == null) link = '#';
filtersArray[filtersArray.length] = {
id: personalFilters[filtersCount].filter_id,
link: link + personalFilters[filtersCount].filter_id,
description: personalFilters[filtersCount].desc,
subscriptDescription: personalFilters[filtersCount].subscriptDescription,
@@ -306,7 +359,7 @@ export const CoreFilterCmpt = {
/**
* Set the drop down menu
*/
setDropDownMenu: function(data) {
setDropDownMenu(data) {
let filters = data.sideMenu.filters;
let personalFilters = data.sideMenu.personalFilters;
let filtersArray = [];
@@ -318,6 +371,7 @@ export const CoreFilterCmpt = {
if (link == null) link = '#';
filtersArray[filtersArray.length] = {
id: filters[filtersCount].filter_id,
option: filters[filtersCount].filter_id,
description: filters[filtersCount].desc
};
@@ -330,6 +384,7 @@ export const CoreFilterCmpt = {
if (link == null) link = '#';
filtersArray[filtersArray.length] = {
id: personalFilters[filtersCount].filter_id,
option: personalFilters[filtersCount].filter_id,
description: personalFilters[filtersCount].desc
};
@@ -340,7 +395,7 @@ export const CoreFilterCmpt = {
/**
* Used to start/refresh the FetchCmpt
*/
startFetchCmpt: function(apiFunction, apiFunctionParameters, dataFetchedCallback) {
startFetchCmpt(apiFunction, apiFunctionParameters, dataFetchedCallback) {
// Assign the function api of the FetchCmpt binded property
this.fetchCmptApiFunction = apiFunction;
@@ -366,12 +421,13 @@ export const CoreFilterCmpt = {
/**
*
*/
handlerSaveCustomFilter: function(event) {
handlerSaveCustomFilter(customFilterName) {
this.selectedFilter = null;
//
this.startFetchCmpt(
CoreFilterAPIs.saveCustomFilter,
this.$fhcApi.factory.filter.saveCustomFilter,
{
customFilterName: this.$refscustomFilterName.value
customFilterName
},
this.getFilter
);
@@ -379,160 +435,23 @@ export const CoreFilterCmpt = {
/**
*
*/
handlerRemoveCustomFilter: function(event) {
handlerRemoveCustomFilter(event) {
let filterId = event.currentTarget.getAttribute("href").substring(1);
if (filterId === this.selectedFilter)
this.selectedFilter = null;
//
this.startFetchCmpt(
CoreFilterAPIs.removeCustomFilter,
this.$fhcApi.factory.filter.removeCustomFilter,
{
filterId: event.currentTarget.getAttribute("href").substring(1)
filterId: filterId
},
this.getFilter
);
},
/**
*
*/
handlerApplyFilterFields: function(event) {
let filterFields = [];
let filterFieldDivRows = document.getElementById('filterFields').getElementsByClassName('row');
for (let i = 0; i< filterFieldDivRows.length; i++)
{
let filterField = {};
for (let j = 0; j< filterFieldDivRows[i].children.length; j++)
{
let filterColumn = filterFieldDivRows[i].children[j];
let filterColumnElement = filterColumn.children[0];
// If the first column then search for the fields dropdown
if (j == 0) filterColumnElement = filterColumnElement.querySelector('select[name=fieldName]');
// If the filter name is _not_ null and it is _not_ a new filter
if (filterColumnElement.name != null && filterColumnElement.name != FILTER_COMPONENT_NEW_FILTER)
{
// Condition
if (filterColumnElement.name == 'condition' && filterColumnElement.value == "")
{
alert("Please fill all the filter options");
return;
}
// Name
if (filterColumnElement.name == 'fieldName')
{
filterField.name = filterColumnElement.value;
}
// Operation
if (filterColumnElement.name == 'operation')
{
filterField.operation = filterColumnElement.value;
}
// Condition
if (filterColumnElement.name == 'condition')
{
filterField.condition = filterColumnElement.value;
}
// Option
if (filterColumnElement.name == 'option')
{
filterField.option = filterColumnElement.value;
}
}
}
if (Object.entries(filterField).length > 0) filterFields.push(filterField);
}
//
this.startFetchCmpt(
CoreFilterAPIs.applyFilterFields,
{
filterFields: filterFields
},
this.getFilter
);
},
/**
*
*/
handlerChangeFilterField: function(oldValue, newValue) {
// If an old filter has been changed
if (oldValue != "")
{
for (let i = 0; i < this.filterFields.length; i++)
{
if (this.filterFields[i].name == oldValue)
{
this.filterFields.splice(i, 1);
break;
}
}
}
// Then add the new filter
for (let i = 0; i < this.datasetMetadata.length; i++)
{
if (this.datasetMetadata[i].name == newValue)
{
let filter = {
name: this.datasetMetadata[i].name,
type: this.datasetMetadata[i].type
};
this.filterFields.push(filter);
break;
}
}
},
/**
*
*/
handlerAddNewFilter: function(event) {
// Adds a new empty filter
this.filterFields.push({
name: FILTER_COMPONENT_NEW_FILTER,
type: FILTER_COMPONENT_NEW_FILTER_TYPE
});
},
/*
*
*/
handlerToggleSelectedField(field) {
// If it is a selected field
if (this.selectedFields.indexOf(field) != -1)
{
// then hide it
this.tabulator.hideColumn(field);
// and remove it from the this.selectedFields property
this.selectedFields.splice(this.selectedFields.indexOf(field), 1);
}
else // otherwise
{
// show it
this.tabulator.showColumn(field);
// and add it to the this.selectedFields property
this.selectedFields.push(field);
}
},
/**
*
*/
handlerRemoveFilterField: function(event) {
//
this.startFetchCmpt(
CoreFilterAPIs.removeFilterField,
{
filterField: event.currentTarget.getAttribute('field-to-remove')
},
this.getFilter
);
},
/**
*
*/
handlerGetFilterById: function(event) {
let filterId = null;
@@ -550,16 +469,37 @@ export const CoreFilterCmpt = {
filterId = attr.substring(1);
}
// Ajax call
this.switchFilter(filterId);
},
switchFilter(filterId) {
this.selectedFilter = filterId;
this.getFilter();
},
applyFilterConfig(filterFields) {
this.selectedFilter = null;
this.startFetchCmpt(
CoreFilterAPIs.getFilterById,
this.$fhcApi.factory.filter.applyFilterFields,
{
filterId: filterId
filterFields
},
this.render
this.getFilter
);
}
},
beforeCreate() {
if (!this.tableOnly == !this.filterType)
alert('You can not have a filter-type in table-only mode!');
},
created() {
if (this.sideMenu && (!this.$props.onNwNewEntry || !(this.$props.onNwNewEntry instanceof Function)))
alert('"nwNewEntry" listener is mandatory when sideMenu is true');
this.uuid = _uuid++;
if (!this.tableOnly)
this.getFilter(); // get the filter data
},
mounted() {
this.initTabulator();
},
template: `
<!-- Load filter data -->
<core-fetch-cmpt
@@ -580,178 +520,53 @@ export const CoreFilterCmpt = {
<div :id="'filterCollapsables' + idExtra">
<div class="filter-header-title">
<span v-if="!tableOnly" class="filter-header-title-span-filter">[ {{ filterName }} ]</span>
<span v-if="!tableOnly" data-bs-toggle="collapse" :data-bs-target="'#collapseFilters' + idExtra" class="filter-header-title-span-icon fa-solid fa-filter fa-xl"></span>
<span data-bs-toggle="collapse" :data-bs-target="'#collapseColumns' + idExtra" class="filter-header-title-span-icon fa-solid fa-table-columns fa-xl"></span>
</div>
<div :id="'collapseColumns' + idExtra" class="card-body collapse" :data-bs-parent="'#filterCollapsables' + idExtra">
<div class="card">
<!-- Filter fields options -->
<div class="row card-body filter-options-div">
<div class="filter-fields-area">
<template v-for="fieldToDisplay in fields">
<div
class="filter-fields-field"
v-bind:class="selectedFields.indexOf(fieldToDisplay) != -1 ? 'text-light bg-dark' : '' "
@click="handlerToggleSelectedField(fieldToDisplay)"
>
{{ fieldNames[fieldToDisplay] || fieldToDisplay }}
</div>
</template>
</div>
</div>
<div class="d-flex flex-row justify-content-between flex-wrap">
<div v-if="newBtnShow || reload || $slots.actions" class="d-flex gap-2 align-items-baseline flex-wrap">
<button v-if="newBtnShow" class="btn btn-primary" :class="newBtnClass" :title="newBtnLabel ? undefined : 'New'" :aria-label="newBtnLabel ? undefined : 'New'" @click="$emit('click:new', $event)" :disabled="newBtnDisabled">
<span class="fa-solid fa-plus" aria-hidden="true"></span>
{{ newBtnLabel }}
</button>
<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>
<slot name="actions" v-bind="tabulatorHasSelector ? selectedData : []"></slot>
</div>
<div class="d-flex gap-1 align-items-baseline flex-grow-1 justify-content-end">
<span v-if="!tableOnly">[ {{ filterName }} ]</span>
<a v-if="!tableOnly" href="#" class="btn btn-link px-0 text-dark" data-bs-toggle="collapse" :data-bs-target="'#collapseFilters' + idExtra">
<span class="fa-solid fa-xl fa-filter"></span>
</a>
<a href="#" class="btn btn-link px-0 text-dark" data-bs-toggle="collapse" :data-bs-target="'#collapseColumns' + idExtra">
<span class="fa-solid fa-xl fa-table-columns"></span>
</a>
<table-download class="btn btn-link px-0 text-dark" :tabulator="tabulator" :config="download"></table-download>
</div>
</div>
<div v-if="!tableOnly" :id="'collapseFilters' + idExtra" class="card-body collapse" :data-bs-parent="'#filterCollapsables' + idExtra">
<div class="card">
<!-- Filter options -->
<div class="card-body" v-if="!sideMenu">
<select
class="form-select"
@change="handlerGetFilterById"
>
<option value="">Bitte auswählen...</option>
<template v-for="availableFilter in availableFilters">
<option v-bind:value="availableFilter.option">{{ availableFilter.description }}</option>
</template>
</select>
</div>
<div class="card-body filter-options-div">
<div>
<span>
Neuer Filter
</span>
<span>
<button class="btn btn-outline-dark" type="button" @click=handlerAddNewFilter>+</button>
</span>
</div>
<div :id="'filterFields' + idExtra" class="filter-filter-fields">
<template v-for="(filterField, index) in filterFields">
<div class="row">
<filter-columns
:id="'collapseColumns' + idExtra"
class="card-body collapse"
:data-bs-parent="'#filterCollapsables' + idExtra"
:fields="fieldIdsForVisibilty"
:selected="selectedFields"
:names="fieldNames"
@hide="tabulator.hideColumn($event)"
@show="tabulator.showColumn($event)"
></filter-columns>
<div class="col-5">
<div class="input-group">
<span class="input-group-text">Filter {{ index + 1 }}</span>
<select
class="form-select"
name="fieldName"
v-bind:value="filterField.name"
@change="handlerChangeFilterField(filterField.name, $event.target.value)"
>
<option value="">Feld zum Filter hinzufügen...</option>
<template v-for="columnAlias in filteredColumns">
<option v-bind:value="columnAlias.field">{{ columnAlias.title }}</option>
</template>
</select>
</div>
</div>
<!-- Numeric -->
<template
v-if="filterField.type.toLowerCase().indexOf('int') >= 0">
<div class="col-2">
<select class="form-select" name="operation" v-model="filterField.operation">
<option value="equal">Gleich</option>
<option value="nequal">Nicht gleich</option>
<option value="gt">Größer als</option>
<option value="lt">Weniger als</option>
</select>
</div>
<div class="col-3">
<input type="number" class="form-control" v-bind:value="filterField.condition" name="condition">
</div>
<div class="col">
<button
class="btn btn-outline-dark"
type="button"
v-bind:field-to-remove="filterField.name"
@click=handlerRemoveFilterField>
&emsp;X&emsp;
</button>
</div>
</template>
<!-- Text -->
<template
v-if="filterField.type.toLowerCase().indexOf('varchar') >= 0
|| filterField.type.toLowerCase().indexOf('text') >= 0
|| filterField.type.toLowerCase().indexOf('bpchar') >= 0">
<div class="col-2">
<select class="form-select" name="operation" v-model="filterField.operation">
<option value="equal">Gleich</option>
<option value="nequal">Nicht gleich</option>
<option value="contains">Enthält</option>
<option value="ncontains">Enthält nicht</option>
</select>
</div>
<div class="col-3">
<input type="text" class="form-control" v-bind:value="filterField.condition" name="condition">
</div>
<div class="col">
<button
class="btn btn-outline-dark"
type="button"
v-bind:field-to-remove="filterField.name"
@click=handlerRemoveFilterField>
&emsp;X&emsp;
</button>
</div>
</template>
<!-- Timestamp and date -->
<template
v-if="filterField.type.toLowerCase().indexOf('timestamp') >= 0
|| filterField.type.toLowerCase().indexOf('date') >= 0">
<div class="col-2">
<select class="form-select" name="operation" v-model="filterField.operation">
<option value="gt">Größer als</option>
<option value="lt">Weniger als</option>
<option value="set">Eingestellt ist</option>
<option value="nset">Eingestellt nicht ist</option>
</select>
</div>
<div class="col-1">
<input type="number" class="form-control" v-bind:value="filterField.condition" name="condition">
</div>
<div class="col-2">
<select class="form-select" name="option" v-model="filterField.option">
<option value="minutes">Minuten</option>
<option value="hours">Stunden</option>
<option value="days">Tage</option>
<option value="months">Monate</option>
</select>
</div>
<div class="col">
<button
class="btn btn-outline-dark"
type="button"
v-bind:field-to-remove="filterField.name"
@click=handlerRemoveFilterField
> - </button>
</div>
</template>
</div>
</template>
</div>
<!-- Filter save options -->
<div class="row">
<div class="col-7">
<div class="input-group">
<input ref="customFilterName" type="text" class="form-control" placeholder="Filternamen eingeben..." :id="'customFilterName' + idExtra">
<button type="button" class="btn btn-outline-secondary" @click=handlerSaveCustomFilter>Filter speichern</button>
</div>
</div>
<div class="col">
<button type="button" class="btn btn-outline-dark" @click=handlerApplyFilterFields>Filter anwenden</button>
</div>
</div>
</div>
</div>
</div>
<filter-config
v-if="!tableOnly"
:id="'collapseFilters' + idExtra"
class="card-body collapse"
:data-bs-parent="'#filterCollapsables' + idExtra"
:filters="!sideMenu ? (availableFilters || []) : []"
:columns="columnsForFilter"
:fields="filterFields || []"
@switch-filter="switchFilter"
@apply-filter-config="applyFilterConfig"
@save-custom-filter="handlerSaveCustomFilter"
></filter-config>
</div>
<!-- Tabulator -->
@@ -0,0 +1,78 @@
/**
* Copyright (C) 2022 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
*
*/
export default {
props: {
fields: Array,
selected: {
type: Array,
default: []
},
names: {
type: Object,
default: {}
}
},
emits: {
hide: ['fieldName'],
show: ['fieldName']
},
data: function() {
return {
selectedFields: []
};
},
watch: {
selected(n) {
this.selectedFields = n;
}
},
methods: {
toggle(field) {
if (this.selectedFields.indexOf(field) != -1)
{
this.selectedFields.splice(this.selectedFields.indexOf(field), 1);
this.$emit('hide', field);
}
else
{
this.selectedFields.push(field);
this.$emit('show', field);
}
}
},
template: `
<div class="filter-columns">
<div class="card">
<div class="d-flex flex-wrap gap-2 p-3 justify-content-center">
<div
v-for="fieldToDisplay in fields"
class="btn"
:class="selectedFields.indexOf(fieldToDisplay) != -1 ? 'btn-dark' : 'btn-outline-dark' "
@click="toggle(fieldToDisplay)"
>
{{ names[fieldToDisplay] || fieldToDisplay }}
</div>
</div>
</div>
</div>
`
};
@@ -0,0 +1,215 @@
/**
* Copyright (C) 2022 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const FILTER_COMPONENT_NEW_FILTER = 'Filter Component New Filter';
/**
*
*/
export default {
props: {
filters: {
type: Array,
default: []
},
columns: {
type: Array,
default: []
},
fields: {
type: Array,
default: []
}
},
emits: {
switchFilter: ['filterId'],
applyFilterConfig: ['filterFields'],
saveCustomFilter: ['customFilterName']
},
data: function() {
return {
currentFields: []
};
},
computed: {
types() {
return this.columns.reduce((a,c) => {
let type = c.type.toLowerCase();
if (type.indexOf('int') >= 0)
a[c.name] = 'Numeric';
else if (
type.indexOf('varchar') >= 0 ||
type.indexOf('text') >= 0 ||
type.indexOf('bpchar') >= 0
)
a[c.name] = 'Text';
else if (
type.indexOf('timestamp') >= 0 ||
type.indexOf('date') >= 0
)
a[c.name] = 'Date';
else
a[c.name] = '';
return a;
}, {});
}
},
watch: {
fields(n) {
this.currentFields = n;
}
},
methods: {
switchFilter(evt) {
this.$emit('switchFilter', evt.currentTarget.value);
},
applyFilterConfig() {
const filteredFields = this.currentFields.filter(el => el.name != FILTER_COMPONENT_NEW_FILTER);
if (filteredFields.filter(el => el.condition == "").length)
alert("Please fill all the filter options");
else
this.$emit('applyFilterConfig', filteredFields);
},
addField(evt) {
this.currentFields.push({
name: FILTER_COMPONENT_NEW_FILTER
});
},
removeField(index) {
this.currentFields.splice(index, 1);
}
},
template: `
<div class="filter-config">
<div class="card">
<div class="card-body">
<div v-if="filters.length" class="mb-4">
<select class="form-select" @change="switchFilter">
<option value="">Bitte auswählen...</option>
<option v-for="filter in filters" :value="filter.id">
{{ filter.description }}
</option>
</select>
</div>
<div>
<button class="btn btn-outline-dark" type="button" @click=addField>
<span class="fa-solid fa-plus" aria-hidden="true"></span> Neuer Filter
</button>
</div>
<div class="filter-config-fields my-3">
<div v-for="(filterField, index) in currentFields" class="filter-config-field row">
<div class="col-5">
<div class="input-group">
<span class="input-group-text">Filter {{ index + 1 }}</span>
<select
class="form-select"
v-model="filterField.name"
>
<option value="">Feld zum Filter hinzufügen...</option>
<option v-for="col in columns" :value="col.name">
{{ col.title }}
</option>
</select>
</div>
</div>
<!-- Numeric -->
<template v-if="types[filterField.name] == 'Numeric'">
<div class="col-2">
<select class="form-select" v-model="filterField.operation">
<option value="equal">Gleich</option>
<option value="nequal">Nicht gleich</option>
<option value="gt">Größer als</option>
<option value="lt">Weniger als</option>
</select>
</div>
<div class="col-3">
<input type="number" class="form-control" v-model="filterField.condition">
</div>
</template>
<!-- Text -->
<template v-if="types[filterField.name] == 'Text'">
<div class="col-2">
<select class="form-select" v-model="filterField.operation">
<option value="equal">Gleich</option>
<option value="nequal">Nicht gleich</option>
<option value="contains">Enthält</option>
<option value="ncontains">Enthält nicht</option>
</select>
</div>
<div class="col-3">
<input type="text" class="form-control" v-model="filterField.condition">
</div>
</template>
<!-- Timestamp and date -->
<template v-if="types[filterField.name] == 'Date'">
<div class="col-2">
<select class="form-select" v-model="filterField.operation">
<option value="gt">Größer als</option>
<option value="lt">Weniger als</option>
<option value="set">Eingestellt ist</option>
<option value="nset">Eingestellt nicht ist</option>
</select>
</div>
<div class="col-1">
<input type="number" class="form-control" v-model="filterField.condition">
</div>
<div class="col-2">
<select class="form-select" v-model="filterField.option">
<option value="minutes">Minuten</option>
<option value="hours">Stunden</option>
<option value="days">Tage</option>
<option value="months">Monate</option>
</select>
</div>
</template>
<div class="col text-end">
<button
class="btn btn-outline-dark"
type="button"
@click="removeField(index)"
title="Filter entfernen"
aria-title="Filter entfernen"
>
<span class="fa-solid fa-minus" aria-hidden="true"></span>
</button>
</div>
</div>
</div>
<!-- Filter save options -->
<div class="row">
<div class="col-7">
<div class="input-group">
<input ref="filterName" type="text" class="form-control" placeholder="Filternamen eingeben...">
<button type="button" class="btn btn-outline-secondary" @click="$emit('saveCustomFilter', $refs.filterName.value)">Filter speichern</button>
</div>
</div>
<div class="col">
<button type="button" class="btn btn-outline-dark" @click="applyFilterConfig">Filter anwenden</button>
</div>
</div>
</div>
</div>
</div>
`
};
@@ -0,0 +1,247 @@
/**
* Copyright (C) 2022 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const DEFAULT_ICONS = {
jsonLines: 'fa-file-lines',
xlsx: 'fa-file-excel',
pdf: 'fa-file-pdf',
html: 'fa-file-code',
json: 'fa-file',
csv: 'fa-file-csv'
};
const DEFAULT_LABELS = {
jsonLines: 'Download as JSONLINES',
xlsx: 'Download as XLSX',
pdf: 'Download as PDF',
html: 'Download as HTML',
json: 'Download as JSON',
csv: 'Download as CSV '
};
/**
*
*/
export default {
props: {
tabulator: Object,
config: {
type: [Boolean, String, Function, Array, Object],
default: false
},
iconClass: [String, Array, Object]
},
computed: {
currentConfig() {
if (!this.config)
return false;
let config = this.config;
if (config instanceof Function)
return [config];
if (config === null)
return [];
if (this.config === true)
config = ['csv'];
if (Object.prototype.toString.call(config) === "[object String]")
config = config.split(',');
if (typeof config === 'object' && !Array.isArray(config)) {
let newConfig = [];
for (var k in config) {
var v = config[k], type;
if (!v)
continue;
if (Object.prototype.toString.call(v) === "[object String]") {
type = this.stringToFileFormatter(v);
if (type !== null) {
newConfig.push({
icon: 'fa-solid ' + DEFAULT_ICONS[type],
label: v === k ? DEFAULT_LABELS[type] : k,
formatter: type
});
} else {
type = this.stringToFileFormatter(k);
if(type !== null) {
newConfig.push({
icon: 'fa-solid ' + DEFAULT_ICONS[type],
label: v,
formatter: type
});
} else {
alert('neither ' + k + ' nor ' + v + ' are supported download file types');
}
}
} else if (typeof v === 'object' && !Array.isArray(v)) {
type = this.stringToFileFormatter(k);
if (type !== null) {
if (v.formatter === undefined)
v.formatter = type;
if (v.label === undefined)
v.label = DEFAULT_LABELS[type];
if (v.icon === undefined)
v.icon = DEFAULT_ICONS[type];
newConfig.push(v);
} else {
if (v.label === undefined)
v.label = k;
newConfig.push(v);
}
} else {
type = this.stringToFileFormatter(k);
if (type !== null) {
newConfig.push({
icon: 'fa-solid ' + DEFAULT_ICONS[type],
label: DEFAULT_LABELS[type],
formatter: type
});
} else {
alert(k + ' is not a supported download file type');
}
}
}
config = newConfig;
}
if (Array.isArray(config))
{
config = config.map(el => {
if (Object.prototype.toString.call(el) === "[object String]") {
let formatter = this.stringToFileFormatter(el);
if (formatter === null)
return null;
return {
icon: 'fa-solid ' + DEFAULT_ICONS[formatter],
label: DEFAULT_LABELS[formatter],
formatter
};
}
if (el instanceof Function)
return {
formatter: el
}
if (typeof el === 'object' && !Array.isArray(el) && el !== null) {
if (el.formatter instanceof Function)
return el;
if (this.validateFileFormatter(el.formatter))
return el;
}
return null;
}).filter(el => el !== null);
if (config.length < 2)
return config;
if (config.filter(el => el.label || el.icon).length == config.length)
return config;
alert('Config not valid');
}
return [];
}
},
methods: {
stringToFileFormatter(input) {
let lcInput = input.toLowerCase();
if (lcInput == 'jsonlines')
return 'jsonLines';
if (['xlsx', 'pdf', 'html', 'json', 'csv'].includes(lcInput))
return lcInput;
return null;
},
validateFileFormatter(input) {
let formatter = this.stringToFileFormatter(input);
if (!formatter) {
alert(input + ' is not a supported file formatter');
return false;
}
if (formatter == 'xlsx') {
if (!window.XLSX) {
alert('XLSX Library not loaded');
return false;
}
}
if (formatter == 'pdf') {
if (!window.jspdf) {
alert('jsPDF Library not loaded');
return false;
}
var doc = new jspdf.jsPDF({});
if (!doc.autoTable) {
alert('jsPDF-AutoTable Plugin not loaded');
return false;
}
}
return true;
},
download(config) {
this.tabulator.download(config.formatter, config.file, config.options)
}
},
template: `
<template v-if="currentConfig">
<template v-if="currentConfig.length == 1">
<a
href="#"
class="table-download"
v-bind="$attrs"
title="Download"
aria-title="Download"
@click.prevent="download(currentConfig[0])"
>
<span :class="iconClass || 'fa-solid fa-xl fa-download'" aria-hidden="true"></span>
</a>
</template>
<div v-else class="dropdown d-inline">
<a
href="#"
class="table-download"
v-bind="$attrs"
title="Download"
aria-title="Download"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span :class="iconClass || 'fa-solid fa-xl fa-download'" aria-hidden="true"></span>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li v-for="(conf, i) in currentConfig" :key="i">
<a class="dropdown-item" href="#" @click.prevent="download(conf)">
<span v-if="conf.icon" :class="conf.icon" aria-hidden="true"></span>
{{conf.label}}
</a>
</li>
</ul>
</div>
</template>
`
};
+54
View File
@@ -0,0 +1,54 @@
/**
* Copyright (C) 2022 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
props: {
title: '',
subtitle: '',
mainCols: {
type: Array,
default: []
},
asideCols: {
type: Array,
default: []
},
},
computed: {
mainGridCols() {
return this.mainCols.length > 0 ? `col-md-${this.mainCols[0]}` : "col-md-12";
},
asideGridCols() {
return this.asideCols.length > 0 ? `col-md-${this.asideCols[0]}` : "";
}
},
template: `
<div class="overflow-hidden">
<header v-if="title">
<h1 class="h2 mb-5">{{ title }}<span class="fhc-subtitle">{{ subtitle }}</span></h1>
</header>
<div class="row gx-5">
<main :class="mainGridCols">
<slot name="main">{{ mainGridCols }}</slot>
</main>
<aside v-if="asideCols.length > 0" :class="asideGridCols">
<slot name="aside">{{ asideGridCols }}</slot>
</aside>
</div>
</div>
`
};
+2 -2
View File
@@ -18,10 +18,10 @@
import {CoreRESTClient} from '../../RESTClient.js';
//
const CORE_NAVIGATION_CMPT_TIMEOUT = 2000;
const CORE_NAVIGATION_CMPT_TIMEOUT = 5000;
/**
*
* TODO(chris): deprecated
*/
export const CoreNavigationAPIs = {
/**
+24 -24
View File
@@ -1,5 +1,5 @@
/**
* Copyright (C) 2022 fhcomplete.org
* Copyright (C) 2024 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
@@ -15,8 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {CoreNavigationAPIs} from './API.js';
import {CoreRESTClient} from '../../RESTClient.js';
import {CoreFetchCmpt} from '../../components/Fetch.js';
/**
@@ -30,12 +28,12 @@ export const CoreNavigationCmpt = {
addHeaderMenuEntries: Object, // property used to add new header menu entries from another app/component
addSideMenuEntries: Object, // property used to add new side menu entries from another app/component
hideTopMenu: Boolean,
leftNavCssClasses: {
type: String,
default: 'navbar navbar-left-side'
}
leftNavCssClasses: {
type: String,
default: 'navbar navbar-left-side'
}
},
data: function() {
data() {
return {
headerMenu: {}, // header menu entries
sideMenu: {} // side menu entries
@@ -45,61 +43,63 @@ export const CoreNavigationCmpt = {
/**
*
*/
headerMenuEntries: function() {
headerMenuEntries() {
//
let hm = this.headerMenu ? {...this.headerMenu} : {};
if (this.headerMenu != null && this.addHeaderMenuEntries != null && Object.keys(this.addHeaderMenuEntries).length > 0)
{
this.headerMenu[this.addHeaderMenuEntries.description] = this.addHeaderMenuEntries;
hm[this.addHeaderMenuEntries.description] = this.addHeaderMenuEntries;
}
return this.headerMenu;
return hm;
},
/**
*
*/
sideMenuEntries: function() {
sideMenuEntries() {
//
let sm = this.sideMenu ? {...this.sideMenu} : {};
if (this.sideMenu != null && this.addSideMenuEntries != null && Object.keys(this.addSideMenuEntries).length > 0)
{
this.sideMenu[this.addSideMenuEntries.description] = this.addSideMenuEntries;
sm[this.addSideMenuEntries.description] = this.addSideMenuEntries;
}
return this.sideMenu;
return sm;
}
},
methods: {
/**
*
*/
getNavigationPage: function() {
getNavigationPage() {
return FHC_JS_DATA_STORAGE_OBJECT.called_path + "/" + FHC_JS_DATA_STORAGE_OBJECT.called_method;
},
/**
*
*/
fetchCmptApiFunctionHeader: function() {
return CoreNavigationAPIs.getHeader(this.getNavigationPage());
fetchCmptApiFunctionHeader() {
return this.$fhcApi.factory.navigation.getHeader(this.getNavigationPage());
},
/**
*
*/
fetchCmptApiFunctionSideMenu: function() {
return CoreNavigationAPIs.getMenu(this.getNavigationPage());
fetchCmptApiFunctionSideMenu() {
return this.$fhcApi.factory.navigation.getMenu(this.getNavigationPage());
},
/**
*
*/
fetchCmptDataFetchedHeader: function(data) {
if (CoreRESTClient.hasData(data)) this.headerMenu = CoreRESTClient.getData(data);
fetchCmptDataFetchedHeader(data) {
this.headerMenu = data || {};
},
/**
*
*/
fetchCmptDataFetchedMenu: function(data) {
if (CoreRESTClient.hasData(data)) this.sideMenu = CoreRESTClient.getData(data);
fetchCmptDataFetchedMenu(data) {
this.sideMenu = data || {};
},
/**
*
*/
getDataBsToggle: function(header) {
getDataBsToggle(header) {
return !header.children ? null : 'dropdown';
}
},
+1 -1
View File
@@ -15,7 +15,7 @@ export default {
<div class="searchbar_icon">
<action :res="this.res" :action="this.actions.defaultaction" @actionexecuted="$emit('actionexecuted')">
<img v-if="(typeof res.photo_url !== 'undefined') && (res.photo_url !== null)" :src="res.photo_url"
class="rounded-circle" height="100" />
class="rounded" style="max-height: 120px; max-width: 90px;" />
<i v-else class="fas fa-user-circle fa-5x"></i>
</action>
</div>
+1 -1
View File
@@ -15,7 +15,7 @@ export default {
<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="res.foto"
class="rounded-circle" height="100" />
class="rounded" style="max-height: 120px; max-width: 90px;" />
<i v-else class="fas fa-user-circle fa-5x"></i>
</action>
</div>
+12 -10
View File
@@ -27,13 +27,13 @@ export default {
organisationunit: organisationunit
},
template: `
<form ref="searchform" class="d-flex me-3" action="javascript:void(0);"
<form ref="searchform" class="d-flex me-3 position-relative" action="javascript:void(0);"
@focusin="this.searchfocusin" @focusout="this.searchfocusout">
<div class="input-group me-2 bg-white">
<input ref="searchbox" @keyup="this.search" @focus="this.showsearchresult"
v-model="this.searchsettings.searchstr" class="form-control"
type="search" placeholder="Search" aria-label="Search">
<button ref="settingsbutton" @click="this.togglesettings" class="btn btn-outline-secondary" type="button" id="search-filter"><i class="fas fa-cog"></i></button>
v-model="this.searchsettings.searchstr" class="form-control"
type="search" placeholder="Suche..." aria-label="Search">
<button ref="settingsbutton" @click="this.togglesettings" class="btn btn-light border-start" type="button" id="search-filter"><i class="fas fa-cog"></i></button>
</div>
<div v-show="this.showresult" ref="result"
@@ -46,6 +46,7 @@ export default {
<template v-else="" v-for="res in this.searchresult">
<person v-if="res.type === 'person'" :res="res" :actions="this.searchoptions.actions.person" @actionexecuted="this.hideresult"></person>
<employee v-else-if="res.type === 'mitarbeiter'" :res="res" :actions="this.searchoptions.actions.employee" @actionexecuted="this.hideresult"></employee>
<employee v-else-if="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>
<raum v-else-if="res.type === 'raum'" :res="res" :actions="this.searchoptions.actions.raum" @actionexecuted="this.hideresult"></raum>
<div v-else="">Unbekannter Ergebnistyp: '{{ res.type }}'.</div>
@@ -78,16 +79,13 @@ export default {
calcSearchResultExtent: function() {
var rect = this.$refs.searchbox.getBoundingClientRect();
//console.log(window.innerWidth + ' ' + window.innerHeight + ' ' + JSON.stringify(rect));
this.$refs.result.style.top = Math.floor(rect.bottom + 3) + 'px';
this.$refs.result.style.right = Math.floor(window.innerWidth - rect.right) + 'px';
this.$refs.result.style.width = Math.floor(window.innerWidth * 0.75) + 'px';
this.$refs.result.style.height = Math.floor(window.innerHeight * 0.75) + 'px';
this.$refs.result.style.height = Math.floor(window.innerHeight * 0.80) + 'px';
},
search: function() {
if( this.searchtimer !== null ) {
clearTimeout(this.searchtimer);
}
if( this.searchsettings.searchstr.length >= 3 ) {
if( this.searchsettings.searchstr.length >= 2 ) {
this.calcSearchResultExtent();
this.searchtimer = setTimeout(
this.callsearchapi,
@@ -105,7 +103,11 @@ export default {
this.showsearchresult();
this.searchfunction(this.searchsettings)
.then(function(response) {
that.searchresult = response.data.data;
if( response.data?.error === 1 ) {
that.error = 'Bei der Suche ist ein Fehler aufgetreten.';
} else {
that.searchresult = response.data.data;
}
})
.catch(function(error) {
that.error = 'Bei der Suche ist ein Fehler aufgetreten.'
@@ -23,13 +23,13 @@ export default {
:class="this.topOrBottomClass" @mousedown="this.dragStart">
<div class="splitactions" :class="this.topOrBottomClass">
<span @click="this.collapseTop" class="splitaction">
<i class="fas fa-angle-up"></i>
<i class="fas fa-angle-up text-muted"></i>
</span>
<span @dblclick="this.showBoth" class="splitaction resize">
<i class="fas fa-grip-horizontal"></i>
<i class="fas fa-grip-lines text-muted"></i>
</span>
<span @click="this.collapseBottom" class="splitaction">
<i class="fas fa-angle-down"></i>
<i class="fas fa-angle-down text-muted"></i>
</span>
</div>
</div>
+3 -2
View File
@@ -1,9 +1,10 @@
<?php
$path = "../../../vendor/vuejs/vuedatepicker_js/vue-datepicker.iife.js";
if(file_exists("../../../vendor/vuepic/vue-datepicker-js/vue-datepicker.iife.js"))
if(file_exists($path))
{
header('Content-Type: application/javascript');
echo file_get_contents("../../../vendor/vuepic/vue-datepicker-js/vue-datepicker.iife.js");
echo file_get_contents($path);
echo "export default VueDatePicker";
}
else