Cleaned up commit from 'feature-27351/Digitalisierung_Formulare_Abmeldung_Unterbrechung_Wiederholung'

This commit is contained in:
cgfhtw
2023-07-04 10:34:14 +02:00
parent 5213ab44ff
commit 64ce3d1f6a
82 changed files with 12192 additions and 61 deletions
+79
View File
@@ -0,0 +1,79 @@
.accordion-button-primary {
background-color: #e7f1ff;
color: #0c63e4;
}
.accordion-button-primary:not(.collapsed) {
background-color: #cfe2ff;
color: #0a58ca;
}
.accordion-button-secondary {
background-color: #f0f1f2;
color: #616971;
}
.accordion-button-secondary:not(.collapsed) {
background-color: #e2e3e5;
color: #565e64;
}
.accordion-button-success {
background-color: #e8f3ee;
color: #177a4c;
}
.accordion-button-success:not(.collapsed) {
background-color: #d1e7dd;
color: #146c43;
}
.accordion-button-info {
background-color: #e7fafe;
color: #0cb6d8;
}
.accordion-button-info:not(.collapsed) {
background-color: #cff4fc;
color: #0aa2c0;
}
.accordion-button-warning {
background-color: #fff9e6;
color: #e6ae06;
}
.accordion-button-warning:not(.collapsed) {
background-color: #fff3cd;
color: #cc9a06;
}
.accordion-button-danger {
background-color: #fcebec;
color: #c6303e;
}
.accordion-button-danger:not(.collapsed) {
background-color: #f8d7da;
color: #b02a37;
}
.accordion-button-light {
background-color: #fefeff;
color: #dfe0e1;
}
.accordion-button-light:not(.collapsed) {
background-color: #fefefe;
color: #c6c7c8;
}
.accordion-button-dark {
background-color: #e9e9ea;
color: #1e2125;
}
.accordion-button-dark:not(.collapsed) {
background-color: #d3d3d4;
color: #1a1e21;
}
.tabulator-edit-list .tabulator-edit-list-item {
background-color: white;
}
.tabulator-edit-list .tabulator-edit-list-item:hover,
.tabulator-edit-list .tabulator-edit-list-item.active {
color: white;
}
+66
View File
@@ -0,0 +1,66 @@
:root {
--bs-body-font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
--bs-body-font-size: 14px;
--bs-body-line-height: 1.42857143;
--bs-body-color: #333;
}
html {
font-size: var(--bs-body-font-size);
}
h1, .h1 {
margin-top: 0;
font-size: 1.4rem;
}
h2, .h2 {
margin-top: 0;
font-size: 1.4rem;
}
h3, .h3 {
margin-top: 0;
font-size: 1.3rem;
}
h4, .h4 {
margin-top: 0;
font-size: 1.2rem;
}
h5, .h5 {
margin-top: 0;
font-size: 1.1rem;
}
h6, .h6 {
margin-top: 0;
font-size: 1rem;
}
.btn {
padding: 6px 12px;
}
.fhc-header {
display: flex;
justify-content: between;
flex: wrap;
align-items: center;
padding: 20px 15px 19px;
margin: 40px 0 20px;
border-bottom: solid 1px #eee;
}
@media (min-width:768px) { /* NOTE(chris): size "md" from bs5 */
.fhc-header {
flex-wrap: nowrap!important;
}
}
.fhc-header h1,
.fhc-header h2 {
font-size: 24px;
margin: 0;
}
.fhc-container {
padding: 0 15px;
}
.tabulator {
font-size: var(--bs-body-font-size);
}
+22
View File
@@ -0,0 +1,22 @@
import StudierendenantragAntrag from "../../components/Studierendenantrag/Antrag.js";
import StudierendenantragStatus from "../../components/Studierendenantrag/Status.js";
import StudierendenantragInfoblock from "../../components/Studierendenantrag/Infoblock.js";
import VueDatePicker from "../../components/vueDatepicker.js.php";
const app = Vue.createApp({
components: {
VueDatePicker,
StudierendenantragAntrag,
StudierendenantragStatus,
StudierendenantragInfoblock
},
data() {
return {
statusMsg: "",
statusSeverity: "",
infoArray: []
}
}
});
app.mount('#wrapper');
+8
View File
@@ -0,0 +1,8 @@
import StudierendenantragLeitung from '../../../components/Studierendenantrag/Leitung.js';
const app = Vue.createApp({
components: {
StudierendenantragLeitung
}
});
app.mount('#wrapper');
@@ -0,0 +1,8 @@
import LvZuweisung from '../../../components/Studierendenantrag/Lvzuweisung.js';
const app = Vue.createApp({
components: {
LvZuweisung
}
});
app.mount('#wrapper');
+8
View File
@@ -0,0 +1,8 @@
import LvPopup from '../../../components/Studierendenantrag/Leitung/LvPopup.js';
const app = Vue.createApp({
components: {
LvPopup
}
});
app.mount('#wrapper');
+44
View File
@@ -0,0 +1,44 @@
import BsModal from './Modal.js';
export default {
components: {
BsModal
},
mixins: [
BsModal
],
props: {
dialogClass: {
type: [String,Array,Object],
default: 'modal-dialog-centered'
},
/*
* NOTE(chris):
* Hack to expose in "emits" declared events to $props which we use
* in the v-bind directive to forward all events.
* @see: https://github.com/vuejs/core/issues/3432
*/
onHideBsModal: Function,
onHiddenBsModal: Function,
onHidePreventedBsModal: Function,
onShowBsModal: Function,
onShownBsModal: Function
},
data: () => ({
result: true
}),
mounted() {
this.modal = this.$refs.modalContainer.modal;
},
popup(msg, options) {
return BsModal.popup.bind(this)(msg, options);
},
template: `<bs-modal ref="modalContainer" class="bootstrap-alert" v-bind="$props">
<template v-slot:default>
<slot></slot>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</template>
</bs-modal>`
}
+22
View File
@@ -0,0 +1,22 @@
import BsAlert from './Alert';
export default {
mixins: [
BsAlert
],
data: () => ({
result: false
}),
popup(msg, options) {
return BsAlert.popup.bind(this)(msg, options);
},
template: `<bs-modal ref="modalContainer" class="bootstrap-confirm" v-bind="$props">
<template v-slot:default>
<slot></slot>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-primary" @click="result=true;this.hide()">OK</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</template>
</bs-modal>`
}
+109
View File
@@ -0,0 +1,109 @@
export default {
data: () => ({
modal: null
}),
props: {
backdrop: {
type: [Boolean,String],
default: true,
validator(value) {
return ['static', true, false].includes(value);
}
},
focus: {
type: Boolean,
default: true
},
keyboard: {
type: Boolean,
default: true
},
noCloseBtn: Boolean,
dialogClass: [String,Array,Object]
},
emits: [
"hideBsModal",
"hiddenBsModal",
"hidePreventedBsModal",
"showBsModal",
"shownBsModal"
],
methods: {
dispose() {
return this.modal.dispose();
},
handleUpdate() {
return this.modal.handleUpdate();
},
hide() {
return this.modal.hide();
},
show(relatedTarget) {
return this.modal.show(relatedTarget);
},
toggle() {
return this.modal.toggle();
}
},
mounted() {
if(this.$refs.modal)
{
this.modal = new bootstrap.Modal(this.$refs.modal, {
backdrop: this.backdrop,
focus: this.focus,
keyboard: this.keyboard
});
}
},
popup(body, options, title, footer) {
const BsModal = this,
slots = {};
if (body !== undefined)
slots.default = () => body;
if (title !== undefined)
slots.title = () => title;
if (footer !== undefined)
slots.footer = () => footer;
return new Promise((resolve,reject) => {
const instance = Vue.createApp({
setup() {
return () => Vue.h(BsModal, {...{
class: 'fade'
},...options, ...{
ref: 'modal',
'onHidden.bs.modal': instance.unmount
}}, slots);
},
mounted() {
this.$refs.modal.show();
},
beforeUnmount() {
if (this.$refs.modal)
this.$refs.modal.result !== false ? resolve(this.$refs.modal.result) : reject();
},
unmounted() {
wrapper.parentElement.removeChild(wrapper);
}
});
const wrapper = document.createElement("div");
instance.mount(wrapper);
document.body.appendChild(wrapper);
});
},
template: `<div ref="modal" class="bootstrap-modal modal" tabindex="-1" @[\`hide.bs.modal\`]="$emit('hideBsModal')" @[\`hidden.bs.modal\`]="$emit('hiddenBsModal')" @[\`hidePrevented.bs.modal\`]="$emit('hidePreventedBsModal')" @[\`show.bs.modal\`]="$emit('showBsModal')" @[\`shown.bs.modal\`]="$emit('shownBsModal')">
<div class="modal-dialog" :class="dialogClass">
<div class="modal-content">
<div v-if="$slots.title" class="modal-header">
<h5 class="modal-title"><slot name="title"/></h5>
<button v-if="!noCloseBtn" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div v-if="$slots.footer" class="modal-footer">
<slot name="footer"/>
</div>
</div>
</div>
</div>`
}
+37
View File
@@ -0,0 +1,37 @@
import BsAlert from './Alert';
export default {
mixins: [
BsAlert
],
props: {
placeholder: String,
default: String
},
data: () => ({
value: '',
result: false
}),
created() {
if (this.default)
this.value = this.default;
},
popup(msg, options) {
if (typeof options === 'string')
options = { default: options };
return BsAlert.popup.bind(this)(msg, options);
},
template: `<bs-modal ref="modalContainer" class="bootstrap-prompt" v-bind="$props">
<template v-slot:default>
<slot></slot>
<div>
<input ref="input" type="text" class="form-control" :placeholder="placeholder" v-model="value">
</div>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-primary" @click="result=value;this.hide()">OK</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</template>
</bs-modal>`
}
+11 -6
View File
@@ -53,7 +53,9 @@ export const CoreFetchCmpt = {
*
*/
fetchData: function() {
this.loading = true; // loader started
this.loading = true; // loader started
this.error = false;
this.errorMessage = null;
// Checks if the apifunction is a callable function
if (typeof this.apiFunction == "function")
@@ -91,13 +93,16 @@ export const CoreFetchCmpt = {
*
*/
successHandler: function(response) {
this.$emit('dataFetched', response.data); // trigger the event dataFetched
this.$emit('dataFetched', response ? response.data : undefined); // trigger the event dataFetched
},
/**
*
*/
errorHandler: function(error) {
this.setError(error.message);
if (error.response.data.retval)
this.setError(error.response.data.retval);
else
this.setError(error.message);
},
/**
*
@@ -107,12 +112,12 @@ export const CoreFetchCmpt = {
}
},
template: `
<slot v-if="loading">
<slot v-if="loading" name="loading">
<div class="fetch-loader">Loading...</div>
</slot>
<slot v-if="error">
<slot v-else-if="error" name="error" :error-message="errorMessage">
<div class="fetch-error">{{ errorMessage }}</div>
</slot>
<slot v-else></slot>
`
};
+62
View File
@@ -0,0 +1,62 @@
import BsModal from './Bootstrap/Modal.js';
export default {
components: {
BsModal
},
props: {
timeout: {
type: Number,
default: 300
}
},
data() {
return {
t: null,
state: 0
}
},
methods: {
show() {
switch (this.state) {
case 0:
if (this.timeout) {
this.state = 1;
this.t = window.setTimeout(() => this.$refs.modal.show(), this.timeout);
return;
} else
return this.$refs.modal.show();
case 4:
return window.setTimeout(() => this.show(), 1);
}
},
hide() {
switch (this.state) {
case 1:
return window.clearTimeout(this.t);
case 2:
return window.setTimeout(() => this.hide(), 1);
case 3:
this.$refs.modal.hide();
}
}
},
mounted() {
this.$refs.modal.$refs.modal.addEventListener('show.bs.modal', () => {
this.state = 2;
});
this.$refs.modal.$refs.modal.addEventListener('shown.bs.modal', () => {
this.state = 3;
});
this.$refs.modal.$refs.modal.addEventListener('hide.bs.modal', () => {
this.state = 4;
});
this.$refs.modal.$refs.modal.addEventListener('hidden.bs.modal', () => {
this.state = 0;
});
},
template: `
<bs-modal ref="modal" class="fade text-center" dialog-class="modal-dialog-centered" backdrop="static" :keyboard="false">
Loading...
</bs-modal>`
}
@@ -0,0 +1,60 @@
import StudierendenantragAbmeldung from './Form/Abmeldung.js';
import StudierendenantragUnterbrechung from './Form/Unterbrechung.js';
import StudierendenantragWiederholung from './Form/Wiederholung.js';
import Phrasen from '../../mixins/Phrasen.js';
export default {
components: {
StudierendenantragAbmeldung,
StudierendenantragUnterbrechung,
StudierendenantragWiederholung
},
mixins: [
Phrasen
],
emits: [
'update:infoArray',
'update:statusMsg',
'update:statusSeverity'
],
props: {
antragType: String,
prestudentId: Number,
studierendenantragId: Number,
infoArray: Array,
statusMsg: String,
statusSeverity: String
},
data() {
return {
status: ''
};
},
computed: {
typeComponent() {
return 'Studierendenantrag' + this.antragType;
},
infoText() {
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)}}
</div>
<div v-if="infoText && infoText.substr(0, 9) != '<< PHRASE'" class="alert alert-primary m-3" role="alert" v-html="infoText">
</div>
<component
:is="typeComponent"
class="card-body"
v-model:status="status"
:prestudent-id="prestudentId"
:studierendenantrag-id="studierendenantragId"
@setInfos="$emit('update:infoArray', $event)"
@setStatus="$emit('update:statusMsg', $event.msg);$emit('update:statusSeverity', $event.severity)"
>
</component>
</div>
`
}
@@ -0,0 +1,285 @@
import {CoreFetchCmpt} from '../../Fetch.js';
import Phrasen from '../../../mixins/Phrasen.js';
var _uuid = 0;
export default {
components: {
CoreFetchCmpt
},
mixins: [
Phrasen
],
emits: [
'setInfos',
'setStatus'
],
props: {
prestudentId: Number
},
data() {
return {
data: null,
saving: false,
errors: {
grund: [],
default: []
}
}
},
computed: {
statusSeverity() {
switch (this.data.status)
{
case 'Erstellt': return 'info';
case 'Genehmigt': return 'success';
default: return 'info';
}
}
},
methods: {
load() {
return axios.get(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Abmeldung/getDetailsForNewAntrag/' +
this.prestudentId
).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 result;
}
);
},
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')}),
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'
});
}
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.saving = false;
}
);
},
cancelAntrag() {
this.$emit('setStatus', {
msg: this.p.t_ref('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'
});
}
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.saving = false;
}
);
}
},
created() {
this.uuid = _uuid++;
},
template: `
<div class="studierendenantrag-form-abmeldung">
<core-fetch-cmpt :api-function="load">
<div class="row">
<div class="col-12">
<div v-for="error in errors.default" class="alert alert-danger" role="alert" v-html="error">
</div>
<table class="table">
<tr>
<th>{{p.t('lehre', 'studiengang')}}</th>
<td align="right">{{data.bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'organisationsform')}}</th>
<td align="right">{{data.orgform_bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('projektarbeitsbeurteilung', 'nameStudierende')}}</th>
<td align="right">{{data.name}}</td>
</tr>
<tr>
<th>{{p.t('person', 'personenkennzeichen')}}</th>
<td align="right">{{data.matrikelnr}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'studienjahr')}}</th>
<td align="right">{{data.studienjahr_kurzbz}}</td>
</tr>
<tr>
<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>
<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'"
rows="5"
:disabled="saving"
ref="grund"
required
></textarea>
<div v-if="errors.grund.length" class="invalid-feedback">
{{errors.grund.join(".")}}
</div>
</div>
<div class="col-12 text-end">
<button
v-if="data.studierendenantrag_id && data.canCancel"
type="button"
class="btn btn-danger"
@click="cancelAntrag"
:disabled="saving"
>
{{p.t('studierendenantrag', 'btn_cancel')}}
</button>
<button
v-else-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_Abmeldung')">
</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>
</div>
<template v-slot:error="{errorMessage}">
<div class="alert alert-danger m-0" role="alert">
{{ errorMessage }}
</div>
</template>
</core-fetch-cmpt>
</div>
`
}
@@ -0,0 +1,359 @@
import {CoreFetchCmpt} from '../../Fetch.js';
import VueDatepicker from '../../vueDatepicker.js.php';
import Phrasen from '../../../mixins/Phrasen.js';
var _uuid = 0;
export default {
components: {
CoreFetchCmpt,
VueDatepicker
},
mixins: [
Phrasen
],
emits: [
'setInfos',
'setStatus'
],
props: {
prestudentId: Number,
studierendenantragId: Number
},
data() {
return {
data: null,
saving: false,
errors: {
grund: [],
studiensemester: [],
datum_wiedereinstieg: [],
default: []
},
stsem: null,
siteUrl: FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router
}
},
computed: {
statusSeverity() {
switch (this.data.status)
{
case 'Erstellt': return 'info';
case 'Genehmigt': return 'success';
case 'Zurückgezogen': return 'danger';
default: return 'info';
}
},
loadUrl() {
if (this.studierendenantragId)
return '/components/Antrag/Unterbrechung/getDetailsForAntrag/'+
this.studierendenantragId;
return '/components/Antrag/Unterbrechung/getDetailsForNewAntrag/' +
this.prestudentId;
},
datumWsFormatted() {
if(!this.data.datum_wiedereinstieg)
return '';
let datum = new Date(this.data.datum_wiedereinstieg);
return datum.toLocaleDateString();
}
},
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 result;
}
);
},
createAntrag() {
this.$emit('setStatus', {
msg: this.p.t_ref('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.$refs.datum_wiedereinstieg && this.$refs.datum_wiedereinstieg.value);
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'
});
}
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.saving = false;
}
);
},
cancelAntrag() {
this.$emit('setStatus', {
msg: this.p.t_ref('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.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t('studierendenantrag', 'status_error')}),
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;
}
);
}
},
created() {
this.uuid = _uuid++;
},
template: `
<div class="studierendenantrag-form-unterbrechung">
<core-fetch-cmpt :api-function="load">
<div class="row">
<div class="col-12">
<div v-for="error in errors.default" class="alert alert-danger" role="alert" v-html="error">
</div>
<table class="table">
<tr>
<th>{{p.t('lehre', 'studiengang')}}</th>
<td align="right">{{data.bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'organisationsform')}}</th>
<td align="right">{{data.orgform_bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('projektarbeitsbeurteilung', 'nameStudierende')}}</th>
<td align="right">{{data.name}}</td>
</tr>
<tr>
<th>{{p.t('person', 'personenkennzeichen')}}</th>
<td align="right">{{data.matrikelnr}}</td>
</tr>
<tr>
<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>
<td align="right" v-if="data.studierendenantrag_id">{{data.semester}}</td>
<td align="right" v-else>{{stsem === null ? '' : data.studiensemester[stsem].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(".")}}
</div>
</div>
</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>
<select v-if="stsem === null" class="form-select" disabled>
<option value="" disabled selected>
{{p.t('ui/select_studiensemester')}}
</option>
</select>
<select v-else ref="datum_wiedereinstieg" class="form-select">
<option
v-for="sem in data.studiensemester[stsem].wiedereinstieg"
:key="sem.studiensemester_kurzbz"
:value="sem.start"
>
{{sem.studiensemester_kurzbz}}
</option>
</select>
</div>
<div v-if="errors.datum_wiedereinstieg.length" class="invalid-feedback d-block">
{{errors.datum_wiedereinstieg.join(".")}}
</div>
</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'"
rows="5"
:disabled="saving"
ref="grund"
required
></textarea>
<div v-if="errors.grund.length" class="invalid-feedback">
{{errors.grund.join(".")}}
</div>
</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">
</div>
</div>
<div class="col-12 text-end">
<button
v-if="!data.studierendenantrag_id"
type="button"
class="btn btn-primary"
@click="createAntrag"
:disabled="saving"
>
{{p.t('studierendenantrag', 'btn_create_Unterbrechung')}}
</button>
<button
v-else-if="data.status == 'Erstellt'"
type="button"
class="btn btn-danger"
@click="cancelAntrag"
:disabled="saving"
>
{{p.t('studierendenantrag', 'btn_cancel')}}
</button>
</div>
</div>
<template v-slot:error="{errorMessage}">
<div class="alert alert-danger m-0" role="alert">
{{ errorMessage }}
</div>
</template>
</core-fetch-cmpt>
</div>
`
}
@@ -0,0 +1,212 @@
import {CoreFetchCmpt} from '../../Fetch.js';
import VueDatepicker from '../../vueDatepicker.js.php';
import Phrasen from '../../../mixins/Phrasen.js';
var _uuid = 0;
export default {
components: {
CoreFetchCmpt,
VueDatepicker
},
mixins: [
Phrasen
],
emits: [
'setInfos',
'setStatus',
'update:status'
],
props: {
status: String,
prestudentId: Number,
studierendenantragId: Number
},
data() {
return {
data: null,
saving: false,
errors: {
grund: [],
default: []
},
siteUrl: FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router,
infos: []
}
},
computed: {
statusSeverity() {
switch (this.data.status)
{
case 'Erstellt': return 'info';
case 'Genehmigt': return 'success';
case 'Verzichtet': return 'danger';
default: return 'info';
}
},
loadUrl() {
return '/components/Antrag/Wiederholung/getDetailsForNewAntrag/' +
this.prestudentId;
},
datumPruefungFormatted() {
if(!this.data.pruefungsdatum)
return '';
let datum = new Date(this.data.pruefungsdatum);
return datum.toLocaleDateString();
}
},
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.data.status == 'ErsteAufforderungVersandt' || this.data.status == 'ZweiteAufforderungVersandt')
this.data.status = 'Offen';
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
});
return result;
}
);
},
createAntrag() {
this.createAntragWithStatus(true);
},
cancelAntrag() {
this.createAntragWithStatus(false);
},
createAntragWithStatus(repeat) {
let func = repeat ? 'createAntrag' : 'cancelAntrag';
let nextState = repeat ? 'Erstellt' : 'Verzichtet';
this.$emit('setStatus', {
msg: this.p.t_ref('studierendenantrag', 'status_x', {status: this.p.t_ref('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_ref('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.saving = false;
}
);
}
},
created() {
this.uuid = _uuid++;
},
mounted() {
this.infos = [...Array(5).keys()].map(n => ({
body: this.p.t_ref('studierendenantrag', 'infotext_Wiederholung_' + n)
}));
this.$emit('setInfos', this.infos);
},
template: `
<div class="studierendenantrag-form-wiederholung">
<core-fetch-cmpt :api-function="load">
<div class="row">
<div class="col-12">
<div v-for="error in errors.default" class="alert alert-danger" role="alert" v-html="error">
</div>
<table class="table">
<tr>
<th>{{p.t('lehre', 'studiengang')}}</th>
<td align="right">{{data.bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('lehre', 'organisationsform')}}</th>
<td align="right">{{data.orgform_bezeichnung}}</td>
</tr>
<tr>
<th>{{p.t('projektarbeitsbeurteilung', 'nameStudierende')}}</th>
<td align="right">{{data.name}}</td>
</tr>
<tr>
<th>{{p.t('person', 'personenkennzeichen')}}</th>
<td align="right">{{data.matrikelnr}}</td>
</tr>
<tr>
<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>
<td align="right">{{datumPruefungFormatted}}</td>
</tr>
</table>
</div>
<div class="col-12 d-flex justify-content-end gap-2">
<button
v-if="!data.studierendenantrag_id || data.status == 'Offen'"
type="button"
class="btn btn-primary"
@click="createAntrag"
:disabled="saving"
>
{{p.t('studierendenantrag/antrag_Wiederholung_button_yes')}}
</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>
</div>
</div>
<template v-slot:error="{errorMessage}">
<div class="alert alert-danger m-0" role="alert">
{{ errorMessage }}
</div>
</template>
</core-fetch-cmpt>
</div>
`
}
@@ -0,0 +1,19 @@
export default {
props: {
infos: Array,
},
template: `
<div class="studierendenantrag-infoblock">
<div v-for="(info, index) in infos" :key="index" class="alert d-flex align-items-center" :class="'alert-' + (info.severity || 'info')" role="alert">
<i class="fa fa-lg flex-shrink-0 me-3" :class="'fa-' + (info.icon || 'info-circle')" role="img" aria-label="Info:"></i>
<div v-if="info.title">
<h4 v-if="info.title" class="alert-heading">
{{info.title}}
</h4>
<p class="mb-0" v-html="info.body"></p>
</div>
<p v-else class="mb-0" v-html="info.body"></p>
</div>
</div>
`
}
@@ -0,0 +1,312 @@
import LeitungHeader from './Leitung/Header.js';
import LeitungActions from './Leitung/Actions.js';
import LeitungTable from './Leitung/Table.js';
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: {
LeitungHeader,
LeitungTable,
LeitungActions,
FhcLoader
},
mixins: [Phrasen],
props: {
stgL: Array,
stgA: Array
},
data() {
return {
filter: undefined,
selectedData: [],
columns: []
}
},
computed: {
stgs(){
if(!this.stgL || !this.stgA)
return[];
let undesiredOutput = [...this.stgL, ...this.stgA];
let desiredOutput = undesiredOutput.reduce(
(accumulator, currentValue)=>
{
accumulator[currentValue.studiengang_kz] = currentValue;
return accumulator;
},
{}
);
return Object.values(desiredOutput);
},
stgkzL()
{
if (!this.stgL)
return [];
return this.stgL.map(stg => stg.studiengang_kz);
},
stgkzA()
{
if (!this.stgA)
return [];
return this.stgA.map(stg => stg.studiengang_kz);
}
},
methods: {
changeFilter(evt) {
this.filter = evt.target.value || undefined;
this.reload();
},
reload() {
this.$refs.table.reload(this.filter);
},
download() {
this.$refs.table.download();
},
actionApprove(evt, oks) {
var antraege = evt || [...this.selectedData];
if (!oks) {
oks = [];
}
var currentAntrag = antraege.shift();
if (currentAntrag) {
if (currentAntrag.typ != 'Wiederholung')
{
oks.push(currentAntrag);
this.actionApprove(antraege, oks);
}
else
{
let countAntrage = 0;
LvPopup
.popup(this.p.t('studierendenantrag','title_show_lvs', currentAntrag), {
antragId: currentAntrag.studierendenantrag_id,
footer: true,
dialogClass: 'modal-lg',
countRemaining : antraege.filter(antrag => antrag.typ == 'Wiederholung').length
})
.then(result => {
if (result[0]) {
oks.push(currentAntrag);
if (result[1])
while (antraege.length)
oks.push(antraege.pop());
} else if (result[1]) {
while (antraege.length) {
currentAntrag = antraege.pop();
if (currentAntrag.typ != 'Wiederholung')
oks.push(currentAntrag);
}
}
this.actionApprove(antraege, oks);
})
.catch(() => {});
}
} 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);
}
},
actionReject(evt, gruende) {
var antraege = evt || this.selectedData;
if(!gruende)
{
gruende = [];
}
var currentAntrag = antraege.pop();
if(currentAntrag)
GrundPopup.popup(this.p.t('studierendenantrag', 'title_grund', {id: currentAntrag.studierendenantrag_id})).then(result =>
{
currentAntrag.grund = result[0];
gruende.push(currentAntrag);
if(result[1])
{
while (antraege.length)
{
currentAntrag = antraege.pop();
currentAntrag.grund = result[0];
gruende.push(currentAntrag);
}
}
this.actionReject(antraege, gruende);
})
.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);
}
},
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);
},
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);
},
actionoObjectionDeny(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/objectionDeny/',
{
studierendenantrag_id: antrag.studierendenantrag_id
}
)
)
)
.then(this.showValidation)
.catch(this.showError);
},
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);
},
showValidation(results) {
var errors = results.filter(res => res.data.error);
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'});
}
},
template: `
<div class="studierendenantrag-leitung fhc-table">
<leitung-header
:stgs="stgs"
@input="changeFilter"
>
</leitung-header>
<leitung-actions
:stg-a="stgkzA"
:stg-l="stgkzL"
:selectedData="selectedData"
:columns="columns"
@reload="reload"
@action:approve="actionApprove"
@action:reject="actionReject"
@action:reopen="actionReopen"
@download="download"
>
</leitung-actions>
<leitung-table
ref="table"
:stg-a="stgkzA"
:stg-l="stgkzL"
v-model:columnData="columns"
v-model:selectedData="selectedData"
@action:approve="actionApprove"
@action:reject="actionReject"
@action:reopen="actionReopen"
@action:object="actionObject"
@action:objectionDeny="actionoObjectionDeny"
@action:objectionApprove="actionObjectionApprove"
>
</leitung-table>
<fhc-loader ref="loader"></fhc-loader>
</div>
`
}
@@ -0,0 +1,87 @@
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,
stgL: Array,
stgA: Array
},
emits: [
'reload',
'download',
'action:approve',
'action:reject',
'action:reopen'
],
data() {
return {
currentStudent: ''
}
},
computed: {
selectedCanBeApproved() {
if (!this.selectedData.length)
return false;
if (!this.selectedData.every(val => this.stgL.includes(val.studiengang_kz)))
return false;
return this.selectedData.filter(row => {
return (row.typ == 'Wiederholung' && row.status == 'Lvszugewiesen') || (row.typ != 'Wiederholung' && (row.status == 'Erstellt' || row.status == 'ErstelltStgl'));
}).length == this.selectedData.length;
},
selectedCanBeRejected() {
if (!this.selectedData.length)
return false;
if (!this.selectedData.every(val => this.stgL.includes(val.studiengang_kz)))
return false;
return this.selectedData.filter(row => {
return (row.typ == 'Unterbrechung' && row.status == 'Erstellt');
}).length == this.selectedData.length;
},
selectedCanBeReopened() {
if (!this.selectedData.length)
return false;
if (!this.selectedData.every(val => this.stgA.includes(val.studiengang_kz)))
return false;
return this.selectedData.filter(row => {
return (row.typ == 'Wiederholung' && row.status == 'Verzichtet');
}).length == this.selectedData.length;
},
newUrl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + this.currentStudent;
}
},
methods: {
hideModal() {
bootstrap.Modal.getInstance(this.$refs.modal).hide();
}
},
template: `
<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')">
<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>
</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>
</div>
<div class="col-12">
<actions-columns id="columns" class="collapse" :columns="columns"></actions-columns>
</div>
</div>
`
}
@@ -0,0 +1,25 @@
export default {
props: {
columns: Array
},
methods: {
toggleColumn(col) {
col.visible = !col.visible;
col.original.toggle()
},
show() {
}
},
template: `
<div class="studierendenantrag-leitung-actions-columns">
<div class="card mt-3">
<div class="card-body d-flex flex-wrap gap-2 justify-content-center">
<a v-for="col in columns" class="btn" :class="col.visible ? 'btn-dark' : 'btn-outline-dark'" href="#" @click.prevent="toggleColumn(col)">
{{col.title}}
</a>
</div>
</div>
</div>
`
}
@@ -0,0 +1,118 @@
import BsAlert from '../../../Bootstrap/Alert.js';
import BsModal from '../../../Bootstrap/Modal.js';
import Phrasen from '../../../../mixins/Phrasen.js';
export default {
components: {
BsModal
},
mixins: [
Phrasen
],
emits: [
'reload'
],
data() {
return {
data: [],
student: '',
stg: ''
}
},
computed: {
newUrl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/lehre/Studierendenantrag/abmeldung/' + this.student;
},
students() {
if (!this.stg)
return [];
if (!this.data[this.stg])
return [];
return this.data[this.stg].studenten.sort(
(a, b) => a.nachname == b.nachname ?
a.vorname > b.vorname :
a.nachname > b.nachname
);
},
hasNoData() {
return !Object.values(this.data).length;
}
},
methods: {
openForm() {
bootstrap.Modal.getInstance(this.$refs.modal).hide();
BsModal.popup(Vue.h('iframe', {
src: this.newUrl,
class: 'position-absolute top-0 start-0 w-100 h-100'
}), {
dialogClass: 'modal-fullscreen'
}, this.p.t('studierendenantrag', 'antrag_header')).then(() => {
this.loadSelects();
this.$emit('reload');
});
},
loadSelects() {
return axios.get(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/components/Antrag/Abmeldung/getStudiengaengeAssistenz/'
).then(
result => {
if (result.data.error) {
BsAlert.popup(result.data.retval, {dialogClass: 'alert alert-danger'});
} else {
this.data = result.data.retval;
}
return result;
}
);
}
},
created() {
return this.loadSelects();
},
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" :disabled="hasNoData">
<i class="fa fa-plus"></i>
{{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>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="newAntragModalStg">{{p.t('lehre','studiengang')}}</label>
<select id="newAntragModalStg" class="form-select" v-model="stg">
<option v-for="(stg, stg_kz) in data" :value="stg_kz" :key="stg_kz">
{{stg.bezeichnung}} ({{stg.orgform}})
</option>
</select>
</div>
<div class="mb-3">
<label for="newAntragModalStudent">{{p.t('person','studentIn')}}</label>
<select v-model="student" id="newAntragModalStudent" class="form-select">
<option v-for="(stg, stg_kz) in students" :value="stg.prestudent_id" :key="stg.prestudent_id">
{{stg.nachname}} {{stg.vorname}}
</option>
</select>
</div>
</div>
<div class="modal-footer">
<a :href="newUrl"
class="btn btn-primary"
target="_blank"
@click.prevent="openForm">
{{p.t('studierendenantrag','btn_create')}}
</a>
</div>
</div>
</div>
</div>
</div>
`
}
@@ -0,0 +1,62 @@
import BsAlert from '../../Bootstrap/Alert.js';
import Phrasen from '../../../mixins/Phrasen.js';
export default {
mixins: [
BsAlert,
Phrasen
],
props: {
placeholder: String,
default: String
},
data: () => ({
value: '',
result: false,
check: false,
isInvalid: false
}),
methods: {
submit(){
if(!this.value) {
this.isInvalid = true;
}
else {
this.result = [this.value, this.check];
this.hide();
}
return
}
},
created() {
if (this.default)
this.value = this.default;
},
popup(msg, options) {
if (typeof options === 'string')
options = { default: options };
return BsAlert.popup.bind(this)(msg, options);
},
template: `<bs-modal ref="modalContainer" class="bootstrap-prompt" v-bind="$props">
<template v-slot:title>
<slot></slot>
</template>
<template v-slot: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')}}
</div>
</div>
<div class="form-check">
<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>
</div>
</template>
<template v-slot:footer>
<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>`
}
@@ -0,0 +1,24 @@
import Phrasen from '../../../mixins/Phrasen.js';
export default {
mixins: [Phrasen],
props: {
stgs: Array
},
emits: [
'input'
],
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>
</div>
</div>
`
}
@@ -0,0 +1,142 @@
import BsAlert from '../../Bootstrap/Alert.js';
import {CoreFetchCmpt} from "../../Fetch.js";
import Phrasen from '../../../mixins/Phrasen.js';
export default {
components: {
CoreFetchCmpt
},
mixins: [
BsAlert,
Phrasen
],
props: {
footer: Boolean,
antragId: Number,
countRemaining: Number
},
data: () => ({
lvs: null,
refresh: true,
result: false,
check: false
}),
computed: {
lvzugelassen() {
let zwischen = {};
for (let k in this.lvs){
zwischen[k] = this.lvs[k].filter(lv=>lv.antrag_zugelassen);
}
return zwischen;
},
lvzugelassenLength() {
return Object.values(this.lvzugelassen).reduce((result, current) => result + current.length, 0);
}
},
methods: {
setlvs(param) {
if(param.error)
{
this.$refs.fetchCompt.error = true;
this.$refs.fetchCompt.errorMessage = param.retval;
}
else
this.lvs = param.retval;
},
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);
},
submit(result) {
this.result = [result, this.check];
this.hide();
}
},
watch: {
antragId() {
Vue.nextTick(() => {
this.refresh = !this.refresh;
});
}
},
popup(msg, options) {
if (typeof options === 'string')
options = { default: options };
return BsAlert.popup.bind(this)(msg, options);
},
template: `<bs-modal ref="modalContainer" class="bootstrap-prompt" v-bind="$props">
<template v-slot:title>
<slot></slot>
</template>
<template v-slot:default>
<core-fetch-cmpt
ref="fetchCompt"
:refresh="refresh"
:api-function="loadlvs"
@data-fetched="setlvs">
<template #default>
<div v-if="lvzugelassenLength == 0">
{{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>{{sem.substr(1)}}</span>
</span>
</caption>
<thead>
<tr>
<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('global','anmerkung')}}
</th>
</tr>
</thead>
<tbody>
<tr v-for="lv in lv_arr">
<td>
<label class="w-100 m-1" :for="'flexSwitchCheckLv_' + lv.lehrveranstaltung_id">
{{lv.bezeichnung}}
</label>
</td>
<td>
<div class="m-1">
{{lv.lehrform_kurzbz}}
</div>
</td>
<td>
<div class="m-1">
{{lv.ects}}
</div>
</td>
<td>
<div class="m-1">
{{lv.note || '---'}}
</div>
</td>
<td>
<div class="m-1">
{{lv.antrag_anmerkung}}
</div>
</td>
</tr>
</tbody>
</table>
</template>
</core-fetch-cmpt>
</template>
<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>
</div>
<button type="button" class="btn btn-primary" @click="submit(true)">{{p.t('studierendenantrag','btn_approve')}}</button>
<button type="button" class="btn btn-secondary" @click="submit(false)">{{p.t('ui','skip')}}</button>
</template>
</bs-modal>`
}
@@ -0,0 +1,388 @@
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 {
components: {
BsModal,
CoreFetchCmpt,
LvPopup
},
mixins: [Phrasen],
props: {
selectedData: Array,
columnData: Array,
stgL: Array,
stgA: Array
},
emits: [
'update:columnData',
'update:selectedData',
'action:approve',
'action:reject',
'action:reopen',
'action:object',
'action:objectionDeny',
'action:objectionApprove'
],
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: [],
lvsData: null
}
},
methods: {
reload(stg) {
this.table.replaceData(this.ajaxUrl + (stg || ''));
},
download() {
this.table.download("csv", "data.csv");
},
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);
});
},
showHistoryGrund(grund) {
this.$refs.modalGrund.$el.addEventListener(
'hidden.bs.modal',
this.$refs.history.show,
{
once: true
}
);
this.$refs.modalGrundPre.innerHTML = grund;
},
showLVs(data) {
this.lvsData = data;
this.$refs.lvList.show();
}
},
mounted() {
function dateFormatter (cell) {
let val = cell.getValue();
if (!val)
return '';
let date = new Date(val);
return date.toLocaleDateString();
}
this.table = new Tabulator(this.$refs.table, {
placeholder:"Keine zu bearbeitenden Datensätze",
movableColumns: true,
maxHeight: '50vh',
layout: "fitDataStretch", // TODO(chris): wont work when changed
ajaxURL: this.ajaxUrl,
persistence: { // NOTE(chris): do not store column titles
sort: true, //persist column sorting
filter: true, //persist filters
headerFilter: true, //persist header filters
group: true, //persist row grouping
page: true, //persist page
columns: ["width", "visible"], //persist columns
},
persistenceID: 'studierendenantrag_leitung',
columns: [
{
formatter: 'rowSelection',
titleFormatter: 'rowSelection',
titleFormatterParams: {
rowRange: 'active'
},
hozAlign: 'center',
headerSort: false
},
{
field: 'studierendenantrag_id',
title: '#'
},
{
field: 'bezeichnung',
title: this.p.t('lehre', 'studiengang'),
headerFilter: 'list',
headerFilterParams: {
valuesLookup: true,
clearable: true,
autocomplete: true,
}
},
{
field: 'orgform',
title: this.p.t('lehre', 'organisationsform'),
headerFilter: 'list',
headerFilterParams: {
valuesLookup: true,
clearable: true,
autocomplete: true,
}
},
{
field: 'typ',
title: this.p.t('studierendenantrag', 'antrag_typ'),
headerFilter: 'list',
headerFilterParams: {
valuesLookup: true,
clearable: true,
autocomplete: true,
},
formatter: (cell, formatterParams, onRendered) => {
return this.p.t('studierendenantrag','antrag_typ_' + cell.getValue());
}
},
{
field: 'statustyp',
title: this.p.t('studierendenantrag', 'antrag_status'),
headerFilter: 'list',
headerFilterParams: {
valuesLookup: true,
clearable: true,
autocomplete: true,
}
},
{
field: 'matrikelnr',
title: this.p.t('person', 'personenkennzeichen'),
headerFilter: 'input'
},
{
field: 'prestudent_id',
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'),
headerFilter: 'input'
},
{
field: 'datum',
title: this.p.t('global', 'datum'),
formatter: dateFormatter,
headerFilterFunc: 'dates',
headerFilter: dateFilter
},
{
field: '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'),
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.addEventListener('click', () => {
this.$refs.modalGrundPre.innerHTML = val;
});
return val ? link : '';
}
},
{
field: 'dms_id',
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 +
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>';
}
},
{
field: 'actions',
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'))
{
// 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>';
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 == 'Abmeldung' && data.status == 'GenehmigtStgl')
{
// NOTE(chris): Object
let button = document.createElement('button');
button.innerHTML = this.p.t('studierendenantrag', 'btn_object');
button.className = "btn btn-outline-secondary";
button.addEventListener('click',() => this.$emit('action:object', [cell.getData()]));
container.append(button);
}
if(data.typ == 'Abmeldung' && data.status == 'Beeinsprucht')
{
// NOTE(chris): Deny Objection
let button = document.createElement('button');
button.innerHTML = 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.className = "btn btn-outline-secondary";
button.addEventListener('click',() => this.$emit('action:objectionApprove', [cell.getData()]));
container.append(button);
}
if (this.stgA.includes(data.studiengang_kz))
{
// NOTE(chris): Reopen
if (data.typ == 'Wiederholung' && data.status == 'Verzichtet') {
let button = document.createElement('button');
button.innerHTML = this.p.t('studierendenantrag', 'btn_reopen');
button.className = "btn btn-outline-secondary";
button.addEventListener('click',() => this.$emit('action:reopen', [cell.getData()]));
container.append(button);
}
// 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.className = "btn btn-outline-secondary";
button.href = FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/lehre/Antrag/Wiederholung/assistenz/' +
cell.getData().studierendenantrag_id + '/frame';
button.onclick = e => {
e.preventDefault();
BsModal.popup(Vue.h('iframe', {
src: button.href,
class: 'position-absolute top-0 start-0 w-100 h-100'
}), {
dialogClass: 'modal-fullscreen'
}, this.p.t('studierendenantrag', 'title_lvzuweisen', cell.getData())).then(() => {
this.$emit('reload');
});
};
container.append(button);
}
}
if (this.stgL.includes(data.studiengang_kz))
{
// NOTE(chris): Approve
if ((data.typ == 'Wiederholung' && data.status == 'Lvszugewiesen') || (data.typ != 'Wiederholung' && (data.status == 'Erstellt' || data.status == 'ErstelltStgl'))) {
let button = document.createElement('button');
button.innerHTML = this.p.t('studierendenantrag', 'btn_approve');
button.className = "btn btn-outline-secondary";
button.addEventListener('click', () => this.$emit('action:approve', [cell.getData()]));
container.append(button);
}
// 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.className = "btn btn-outline-secondary";
button.addEventListener('click', () => this.$emit('action:reject', [cell.getData()]));
container.append(button);
}
}
// 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.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);
});
return container;
}
}
]
});
this.table.on("tableBuilt", () => {
let columns = this.table.getColumns();
let columnData = [];
for (let col of columns) {
let def = col.getDefinition();
if (def.title) {
columnData.push({
title: def.title,
visible: col.isVisible(),
original: col
});
}
}
this.$emit('update:columnData', columnData);
});
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>
</bs-modal>
<bs-modal ref="history" class="fade">
<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">
<td>{{(new Date(status.insertamum)).toLocaleString()}}</td>
<td>{{status.insertvon}}</td>
<td>{{status.typ}}</td>
<td>
<a v-if="status.grund" href="#modal-grund" data-bs-toggle="modal" @click="showHistoryGrund(status.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>
</div>
`
}
@@ -0,0 +1,242 @@
import StudierendenantragStatus from './Status.js';
import Phrasen from '../../mixins/Phrasen.js';
export default {
components: {
StudierendenantragStatus
},
mixins: [Phrasen],
props: {
antragId: Number,
initialStatusCode: String,
initialStatusMsg: String,
disabled: Boolean
},
data() {
return {
lvs: [],
isloading: false,
statusCode: '',
statusMsg: ''
}
},
computed: {
lvs1() {
return this.lvs[Object.keys(this.lvs).filter(key => key.substr(0, 1) == 1)] || [];
},
lvs2() {
return this.lvs[Object.keys(this.lvs).filter(key => key.substr(0, 1) == 2)] || [];
},
lvs1sem(){
return (Object.keys(this.lvs).filter(key => key.substr(0, 1) == 1).pop() || "1").substr(1);
},
lvs2sem(){
return (Object.keys(this.lvs).filter(key => key.substr(0, 1) == 2).pop() || "2").substr(1);
},
statusSeverity() {
switch (this.statusCode) {
case 0: return 'danger';
default: return 'info';
}
}
},
methods: {
save() {
this.isloading = true;
const forbiddenLvs = this.lvs1.filter(lv => lv.antrag_zugelassen && !lv._children).map(lv => ({
studierendenantrag_id: this.antragId,
lehrveranstaltung_id: lv.lehrveranstaltung_id,
zugelassen: 0,
anmerkung: lv.antrag_anmerkung || "",
studiensemester_kurzbz: this.lvs1sem
}));
const mandatoryLvs = this.lvs2.filter(lv => !lv._children).map(lv => ({
studierendenantrag_id: this.antragId,
lehrveranstaltung_id: lv.lehrveranstaltung_id,
zugelassen:lv.antrag_zugelassen ? 1 : 2,
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})
.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.isloading = false;
}).catch(error => {
this.addAlert(error.message, 'alert-danger');
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() {
this.statusCode = this.initialStatusCode;
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) {
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,
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) => {
let data = cell.getData();
if(data._children || !data.zeugnis)
return "";
let input = document.createElement('input');
input.className = "form-check-input";
input.type = "checkbox";
input.role = "switch";
input.checked = cell.getValue();
input.addEventListener('input', () => {
lvs[data.studienplan_lehrveranstaltung_id].antrag_zugelassen = input.checked;
cell.getRow().reformat();
});
if (this.disabled) {
input.disabled = true;
}
let div = document.createElement('div');
div.className = 'form-check form-switch';
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();
});
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_zugelassen = input.value;
});
return input;
}
else
{
return "";
}
}
}
]
});
}
this.lvs = result.data.retval;
}
}
);
},
template: `
<div class="col-sm-8">
<div ref="alertbox"></div>
<span class="d-flex justify-content-between h4">
<span>{{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>{{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>
</div>
<div class="col-sm-4">
<studierendenantrag-status :msg="statusMsg" :severity="statusSeverity"></studierendenantrag-status>
</div>
`
}
@@ -0,0 +1,15 @@
export default {
props: {
msg: String,
severity: String
},
computed: {
severityClass() {
return 'alert-' + this.severity;
}
},
template: `
<div v-if="msg && severity" class="studierendenantrag-status alert text-center mb-3" :class="severityClass" role="alert" v-html="msg">
</div>
`
}
+12
View File
@@ -0,0 +1,12 @@
<?php
if(file_exists("../../../vendor/vuepic/vue-datepicker-js/vue-datepicker.iife.js"))
{
header('Content-Type: application/javascript');
echo file_get_contents("../../../vendor/vuepic/vue-datepicker-js/vue-datepicker.iife.js");
echo "export default VueDatePicker";
}
else
{
header('HTTP/1.0 404 Not Found');
}
+85
View File
@@ -0,0 +1,85 @@
const categories = {};
const loadingModules = {};
function extractCategory(obj, category) {
return obj.filter(e => e.category == category).reduce((res, elem) => {
if (!res[elem.phrase])
res[elem.phrase] = elem.text;
return res;
}, {});
}
function reloadRefs(category) {
while (loadingModules[category].length) {
var v = loadingModules[category].pop();
v[0].value = getValueForLoadedPhrase(category, v[1], v[2]);
Vue.triggerRef(v[0]);
/*Vue.unref(v);*/
}
}
function loadLazy(category, val, phrase, params) {
// NOTE(chris): load module if it's not loaded yet
if (loadingModules[category]) {
loadingModules[category].push([val, phrase, params]);
if (categories[category]) // NOTE(chris): this is for safety in case the loading finished the moment before the val was pushed into the array
reloadRefs(category);
return;
}
loadingModules[category] = [[val, phrase, params]];
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Phrasen/loadModule/' + category).then(res => {
if (res.data.retval)
categories[category] = extractCategory(res.data.retval, category);
else
categories[category] = {};
reloadRefs(category);
}).catch(err => console.error(err));
}
function getValueForLoadedPhrase(category, phrase, params) {
let result = categories[category][phrase];
if (!result)
return '<< PHRASE ' + phrase + '>>';
if (params)
result = result.replace(/\{([^}]*)\}/g, (match, p1) => params[p1] === undefined ? match : params[p1]);
return result;
}
const phrasen = {
t_ref(category, phrase, params) {
if (params === undefined && (
(Array.isArray(category) && category.length == 2) ||
(category.split && category.split('/').length == 2))
) {
params = phrase;
[category, phrase] = category.split ? category.split('/') : category;
}
if (phrase === undefined) {
console.error('invalid input');
return '';
}
if (!categories[category]) {
if (window.FHC_JS_PHRASES_STORAGE_OBJECT !== undefined)
categories[category] = extractCategory(FHC_JS_PHRASES_STORAGE_OBJECT, category);
if (!categories[category] || Object.keys(categories[category]).length === 0) {
categories[category] = undefined;
let val = Vue.ref('');
loadLazy(category, val, phrase, params);
return val;
}
}
var result = getValueForLoadedPhrase(category, phrase, params);
return Vue.ref(result);
},
t(category, phrase, params) {
return Vue.unref(this.t_ref(category, phrase, params));
}
};
export default {
data: () => {
return {
p: phrasen
}
}
}
+43
View File
@@ -0,0 +1,43 @@
if (!primevue) {
console.error('PrimeVue not loaded!');
}
// NOTE(chris): Click on clear button gives an error. This is a bug in primevue => fixed in current version
Tabulator.extendModule('filter', 'filters', {
"dates": (headerValue, rowValue) => {
if (!headerValue)
return true;
let v = new Date(rowValue);
if (Array.isArray(headerValue)) {
if (headerValue[1]) {
return v >= headerValue[0] && v <= headerValue[1].setHours(23, 59, 59, 999);
}
return v.toDateString() == headerValue[0].toDateString();
}
return v.toDateString() == headerValue.toDateString();
}
});
function dateFilter(cell, onRendered, success) {
let div = document.createElement('div');
Vue.createApp({
components: {
PrimevueCalendar: primevue.calendar
},
data() {
return {
val: null
}
},
watch: {
val(n) {
success(n);
}
},
template: `<primevue-calendar v-model="val" selection-mode="range" :manual-input="false" show-button-bar></primevue-calendar>`
}).use(primevue.config.default).mount(div);
return div;
}
export { dateFilter as 'dateFilter' };