Merge branch 'feature-30660/FHC4_StudierendenGUI_Prototyp' of github.com:FH-Complete/FHC-Core into feature-30660/FHC4_StudierendenGUI_Prototyp

This commit is contained in:
ma0068
2024-01-18 09:46:39 +01:00
10 changed files with 517 additions and 363 deletions
@@ -30,7 +30,7 @@ class Noten extends Auth_Controller
return $this->outputJson($result);
}
public function getZeugnis($prestudent_id)
public function getZeugnis($prestudent_id, $all = null)
{
$this->load->model('crm/Student_model', 'StudentModel');
$this->load->model('education/Zeugnisnote_model', 'ZeugnisnoteModel');
@@ -47,7 +47,7 @@ class Noten extends Auth_Controller
$student_uid = current(getData($result))->student_uid;
$studiensemester_kurzbz = $this->variablelib->getVar('semester_aktuell');
$studiensemester_kurzbz = ($all === null) ? $this->variablelib->getVar('semester_aktuell') : null;
$result = $this->ZeugnisnoteModel->getZeugnisnoten($student_uid, $studiensemester_kurzbz);
if (isError($result)) {
@@ -77,6 +77,7 @@ class Student extends FHC_Controller
$this->form_validation->set_rules('semester', 'Semester', 'integer');
if ($this->form_validation->run() == false) {
$this->output->set_status_header(REST_Controller::HTTP_BAD_REQUEST);
return $this->outputJsonError($this->form_validation->error_array());
}
@@ -202,7 +203,11 @@ class Student extends FHC_Controller
}
}
$this->outputJsonSuccess(true);
$this->outputJson(success(array_fill_keys(array_merge(
array_keys($update_lehrverband),
array_keys($update_person),
array_keys($update_student)
), ''), 1));
}
public function check()
+4 -76
View File
@@ -2,82 +2,10 @@
white-space: pre-line;
}
.accordion-button-primary {
background-color: #e7f1ff;
color: #0c63e4;
}
.accordion-button-primary:not(.collapsed) {
background-color: #cfe2ff;
color: #0a58ca;
.btn-p-0 {
padding: 0 .375rem;
}
.accordion-button-secondary {
background-color: #f0f1f2;
color: #616971;
.z-1 {
z-index: 1;
}
.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;
}
+1
View File
@@ -1,3 +1,4 @@
@import './Fhc.css';
@import './components/searchbar.css';
@import './components/verticalsplit.css';
@import './components/FilterComponent.css';
+16 -4
View File
@@ -14,7 +14,8 @@ export default {
bsFeedback: Boolean,
noAutoClass: Boolean,
type: String,
name: String
name: String,
containerClass: [String, Array, Object]
},
data() {
return {
@@ -23,6 +24,13 @@ export default {
}
},
computed: {
hasContainer() {
if (!this.bsFeedback)
return true;
if (this.containerClass)
return true;
return false;
},
lcType() {
if (!this.type)
return 'text';
@@ -37,6 +45,8 @@ export default {
return 'VueDatePicker';
case 'autocomplete':
return 'PvAutocomplete';
case 'uploadimage':
return 'UploadImage';
default:
return 'input';
}
@@ -133,7 +143,9 @@ export default {
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"));;
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"));
}
}
},
@@ -149,7 +161,7 @@ export default {
// TODO(chris): wrap check in div?
},
template: `
<component :is="bsFeedback ? 'FhcFragment' : 'div'" class="position-relative">
<component :is="!hasContainer ? 'FhcFragment' : 'div'" class="position-relative" :class="containerClass">
<label v-if="$attrs.label && lcType != 'radio' && lcType != 'checkbox'" :for="idCmp">{{$attrs.label}}</label>
<input v-if="tag == 'input'" :type="lcType" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidation(); $emit('input', $event)">
<textarea v-else-if="tag == 'textarea'" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidation(); $emit('input', $event)"></textarea>
@@ -197,7 +209,7 @@ export default {
>
<slot></slot>
</component>
<div v-if="valid !== undefined" :class="feedbackClass">
<div v-if="valid !== undefined && feedback.length" :class="feedbackClass">
<template v-for="(msg, i) in feedback" :key="i">
<hr v-if="i" class="m-0">
{{msg}}
+85
View File
@@ -0,0 +1,85 @@
export default {
emits: [
'update:modelValue'
],
props: {
modelValue: {
type: [ FileList, Array ],
required: true
},
multiple: Boolean,
id: String,
name: String
},
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" :id="id" :name="name" :multiple="multiple" type="file" @change="addFiles">
<ul v-if="modelValue.length && multiple" class="list-unstyled m-0">
<li v-for="(file, index) in modelValue" :key="index" class="d-flex mx-1 mt-1">
<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>`
}
+9 -9
View File
@@ -46,17 +46,17 @@ export default {
</div>
</template>
<template v-else>
<slot name="placeholder">
<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>
<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>
</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 class="d-none" type="file" ref="fileInput" @input="pickFile" accept="image/*">
<input :id="$attrs.id" class="d-none" type="file" ref="fileInput" @input="pickFile" accept="image/*">
</div>`
}
@@ -1,16 +1,14 @@
import VueDatePicker from '../../../vueDatepicker.js.php';
import FormUploadImage from '../../../Form/Upload/Image.js';
import {CoreRESTClient} from '../../../../RESTClient.js';
import PvToast from "../../../../../../index.ci.php/public/js/components/primevue/toast/toast.esm.min.js";
import FormForm from '../../../Form/Form.js';
import FormInput from '../../../Form/Input.js';
import FormUploadImage from '../../../Form/Upload/Image.js';
export default {
components: {
VueDatePicker,
FormUploadImage,
PvToast
FormForm,
FormInput,
FormUploadImage
},
inject: {
showBpk: {
@@ -62,6 +60,9 @@ export default {
},
changedLength() {
return Object.keys(this.changed).length;
},
noImageSrc() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + 'skin/images/profilbild_dummy.jpg';
}
},
watch: {
@@ -69,6 +70,7 @@ export default {
this.updateStudent(n);
},
data: {
// TODO(chris): use @input instead?
handler(n) {
let res = {};
for (var k in this.original) {
@@ -82,7 +84,6 @@ export default {
}
}
this.changed = res;
this.resetErrors();
},
deep: true
}
@@ -98,213 +99,264 @@ export default {
this.data.familienstand = '';
this.original = {...this.data};
})
.catch(err => {
console.error(err.response.data || err.message);
});
.catch(this.$fhcAlert.handleSystemError);
},
save() {
CoreRESTClient
.post('components/stv/Student/save/' + this.modelValue.prestudent_id, this.changed)
.then(result => result.data)
this.$refs.form
.send(CoreRESTClient.post('components/stv/Student/save/' + this.modelValue.prestudent_id, this.changed))
.then(result => {
this.resetErrors();
if (CoreRESTClient.isError(result)) {
let errors = CoreRESTClient.getError(result);
if (errors === "Generic error")
console.error(errors, result);
else {
for (var k in errors)
this.addError(k, errors[k]);
}
} else {
for (var node of document.querySelectorAll(Object.keys(this.changed).map(el => '#stv-details-' + el).join(',')))
node.classList.add('is-valid');
if (this.changed.gebdatum !== undefined)
this.gebDatumIsValid = true;
// TODO(chris): phrase
this.$fhcAlert.alertSuccess('Gespeichert');
//this.addToast('Gespeichert', '', 'success');
this.original = {...this.data};
this.changed = {};
}
this.original = {...this.data};
this.changed = {};
})
.catch(this.$fhcAlert.handleSystemError/*err => {
// TODO(chris): phrase
this.addToast('Error', err?.response?.data || err?.message, 'error');
}*/);
},
resetErrors() {
Array.from(this.$refs.form.getElementsByClassName('is-valid')).forEach(el => el.classList.remove('is-valid'));
Array.from(this.$refs.form.getElementsByClassName('is-invalid')).forEach(el => el.classList.remove('is-invalid'));
Array.from(this.$refs.form.getElementsByClassName('invalid-feedback')).forEach(el => el.remove());
this.gebDatumIsValid = false;
this.gebDatumIsInvalid = false;
},
addError(field, msg) {
let id = 'stv-details-' + field;
let input = document.getElementById(id);
if (field === 'gebdatum') {
this.gebDatumIsInvalid = true;
input = document.getElementById('dp-input-' + id).parentNode;
}
input.classList.add('is-invalid');
let feedback = document.createElement('div');
feedback.classList.add('invalid-feedback');
feedback.innerHTML = msg;
input.after(feedback);
/*},
addToast(header, msg, severity) {
this.$refs.responseToast.add({
severity: severity,
summary: header,
detail: msg,
life: 3000
})*/
.catch(this.$fhcAlert.handleSystemError);
}
},
created() {
this.updateStudent(this.modelValue);
},
//TODO(chris): Phrasen
//TODO(chris): Geburtszeit? Anzahl der Kinder?
template: `
<form ref="form" class="stv-details-details" @submit.prevent="save">
<div class="position-sticky top-0">
<form-form ref="form" class="stv-details-details" @submit.prevent="save">
<div class="position-sticky top-0 z-1">
<button type="submit" class="btn btn-primary position-absolute top-0 end-0" :disabled="!changedLength">Speichern</button>
</div>
<fieldset class="overflow-hidden">
<legend>Person</legend>
<template v-if="data">
<div class="row mb-3">
<label for="stv-details-person_id" class="col-sm-1 col-form-label">Person ID</label>
<div class="col-sm-3">
<input id="stv-details-person_id" type="text" class="form-control" v-model="data.person_id" disabled>
</div>
<label v-if="showZugangscode" for="stv-details-zugangscode" class="col-sm-1 col-form-label">Zugangscode</label>
<div v-if="showZugangscode" class="col-sm-3 align-self-center">
<span class="form-text">
<a :href="cisRoot + 'addons/bewerbung/cis/registration.php?code=' + data.zugangscode + '&emailAdresse=' + data.email_privat" target="_blank">{{data.zugangscode}}</a>
</span>
</div>
<label v-if="showBpk" for="stv-details-bpk" class="col-sm-1 col-form-label">BPK</label>
<div v-if="showBpk" class="col-sm-3">
<input id="stv-details-bpk" type="text" class="form-control" v-model="data.bpk" maxlength="28">
<form-input
container-class="col-4"
label="Person ID"
type="text"
v-model="data.person_id"
name="person_id"
readonly
>
</form-input>
<div v-if="showZugangscode" class="col-4">
<label>Zugangscode</label>
<div class="align-self-center">
<span class="form-text">
<a :href="cisRoot + 'addons/bewerbung/cis/registration.php?code=' + data.zugangscode + '&emailAdresse=' + data.email_privat" target="_blank">{{data.zugangscode}}</a>
</span>
</div>
</div>
<form-input
v-if="showBpk"
container-class="col-4"
label="BPK"
type="text"
v-model="data.bpk"
name="bpk"
maxlength="28"
>
</form-input>
</div>
<div class="row mb-3">
<label for="stv-details-anrede" class="col-sm-1 col-form-label">Anrede</label>
<div class="col-sm-3">
<input id="stv-details-anrede" type="text" class="form-control" v-model="data.anrede" maxlength="16">
</div>
<label for="stv-details-titelpre" class="col-sm-1 col-form-label">Titel Pre</label>
<div class="col-sm-3">
<input id="stv-details-titelpre" type="text" class="form-control" v-model="data.titelpre" maxlength="64">
</div>
<label for="stv-details-titelpost" class="col-sm-1 col-form-label">Titel Post</label>
<div class="col-sm-3">
<input id="stv-details-titelpost" type="text" class="form-control" v-model="data.titelpost" maxlength="32">
</div>
<form-input
container-class="col-4"
label="Anrede"
type="text"
v-model="data.anrede"
name="anrede"
maxlength="16"
>
</form-input>
<form-input
container-class="col-4"
label="Titel Pre"
type="text"
v-model="data.titelpre"
name="titelpre"
maxlength="64"
>
</form-input>
<form-input
container-class="col-4"
label="Titel Post"
type="text"
v-model="data.titelpost"
name="titelpost"
maxlength="32"
>
</form-input>
</div>
<div class="row mb-3">
<label for="stv-details-nachname" class="col-sm-1 col-form-label">Nachname</label>
<div class="col-sm-3">
<input id="stv-details-nachname" type="text" class="form-control" v-model="data.nachname" maxlength="64">
</div>
<label for="stv-details-vorname" class="col-sm-1 col-form-label">Vorname</label>
<div class="col-sm-3">
<input id="stv-details-vorname" type="text" class="form-control" v-model="data.vorname" maxlength="32">
</div>
<label for="stv-details-vornamen" class="col-sm-1 col-form-label">Vornamen</label>
<div class="col-sm-3">
<input id="stv-details-vornamen" type="text" class="form-control" v-model="data.vornamen" maxlength="128">
</div>
<form-input
container-class="col-4"
label="Nachname"
type="text"
v-model="data.nachname"
name="nachname"
maxlength="64"
>
</form-input>
<form-input
container-class="col-4"
label="Vorname"
type="text"
v-model="data.vorname"
name="vorname"
maxlength="32"
>
</form-input>
<form-input
container-class="col-4"
label="Vornamen"
type="text"
v-model="data.vornamen"
name="vornamen"
maxlength="128"
>
</form-input>
</div>
<div class="row mb-3">
<label for="stv-details-wahlname" class="col-sm-1 col-form-label">Wahlname</label>
<div class="col-sm-3">
<input id="stv-details-wahlname" type="text" class="form-control" v-model="data.wahlname" maxlength="128">
</div>
<form-input
container-class="col-4"
label="Wahlname"
type="text"
v-model="data.wahlname"
name="wahlname"
maxlength="128"
>
</form-input>
</div>
<div class="row mb-3">
<label for="dp-input-stv-details-gebdatum" class="col-sm-1 col-form-label">Geburtsdatum</label>
<div class="col-sm-3">
<vue-date-picker id="stv-details-gebdatum" :input-class-name="gebDatumIsInvalid ? 'form-control is-invalid' : (gebDatumIsValid ? 'form-control is-valid' : 'form-control')" uid="stv-details-gebdatum" v-model="data.gebdatum" :clearable="false" no-today auto-apply :enable-time-picker="false" format="dd.MM.yyyy" preview-format="dd.MM.yyyy"></vue-date-picker>
</div>
<label for="stv-details-gebort" class="col-sm-1 col-form-label">Geburtsort</label>
<div class="col-sm-3">
<input id="stv-details-gebort" type="text" class="form-control" v-model="data.gebort" maxlength="128">
</div>
<label for="stv-details-geburtsnation" class="col-sm-1 col-form-label">Geburtsnation</label>
<div class="col-sm-3">
<select id="stv-details-geburtsnation" class="form-control" v-model="data.geburtsnation">
<option value="">-- keine Auswahl --</option>
<!-- TODO(chris): gesperrte nationen können nicht ausgewählt werden! Um das zu realisieren müsste man ein pseudo select machen -->
<option v-for="nation in lists.nations" :key="nation.nation_code" :value="nation.nation_code" :disabled="nation.sperre">{{nation.kurztext}}</option>
</select>
</div>
<form-input
container-class="col-4"
label="Geburtsdatum"
type="DatePicker"
v-model="data.gebdatum"
name="gebdatum"
:clearable="false"
no-today
auto-apply
:enable-time-picker="false"
format="dd.MM.yyyy"
preview-format="dd.MM.yyyy"
>
</form-input>
<form-input
container-class="col-4"
label="Geburtsort"
type="text"
v-model="data.gebort"
name="gebort"
maxlength="128"
>
</form-input>
<form-input
container-class="col-4"
label="Geburtsnation"
type="select"
v-model="data.geburtsnation"
name="geburtsnation"
>
<option value="">-- keine Auswahl --</option>
<!-- TODO(chris): gesperrte nationen können nicht ausgewählt werden! Um das zu realisieren müsste man ein pseudo select machen -->
<option v-for="nation in lists.nations" :key="nation.nation_code" :value="nation.nation_code" :disabled="nation.sperre">{{nation.kurztext}}</option>
</form-input>
</div>
<div class="row mb-3">
<label for="stv-details-svnr" class="col-sm-1 col-form-label">SVNR</label>
<div class="col-sm-3">
<input id="stv-details-svnr" type="text" class="form-control" v-model="data.svnr" maxlength="16">
</div>
<label for="stv-details-ersatzkennzeichen" class="col-sm-1 col-form-label">Ersatzkennzeichen</label>
<div class="col-sm-3">
<input id="stv-details-ersatzkennzeichen" type="text" class="form-control" v-model="data.ersatzkennzeichen" maxlength="10">
</div>
<form-input
container-class="col-4"
label="SVNR"
type="text"
v-model="data.svnr"
name="svnr"
maxlength="16"
>
</form-input>
<form-input
container-class="col-4"
label="Ersatzkennzeichen"
type="text"
v-model="data.ersatzkennzeichen"
name="ersatzkennzeichen"
maxlength="10"
>
</form-input>
</div>
<div class="row mb-3">
<label for="stv-details-staatsbuergerschaft" class="col-sm-1 col-form-label">Staatsbürgerschaft</label>
<div class="col-sm-3">
<select id="stv-details-staatsbuergerschaft" class="form-control" v-model="data.staatsbuergerschaft">
<option value="">-- keine Auswahl --</option>
<!-- TODO(chris): gesperrte nationen können nicht ausgewählt werden! Um das zu realisieren müsste man ein pseudo select machen -->
<option v-for="nation in lists.nations" :key="nation.nation_code" :value="nation.nation_code" :disabled="nation.sperre">{{nation.kurztext}}</option>
</select>
</div>
<label for="stv-details-matr_nr" class="col-sm-1 col-form-label">Matrikelnummer</label>
<div class="col-sm-3">
<input id="stv-details-matr_nr" type="text" class="form-control" v-model="data.matr_nr" maxlength="32">
</div>
<label for="stv-details-sprache" class="col-sm-1 col-form-label">Sprache</label>
<div class="col-sm-3">
<select id="stv-details-sprache" class="form-control" v-model="data.sprache">
<option v-for="sprache in lists.sprachen" :key="sprache.sprache" :value="sprache.sprache">{{sprache.sprache}}</option>
</select>
</div>
<form-input
container-class="col-4"
label="Staatsbürgerschaft"
type="select"
v-model="data.staatsbuergerschaft"
name="staatsbuergerschaft"
>
<option value="">-- keine Auswahl --</option>
<!-- TODO(chris): gesperrte nationen können nicht ausgewählt werden! Um das zu realisieren müsste man ein pseudo select machen -->
<option v-for="nation in lists.nations" :key="nation.nation_code" :value="nation.nation_code" :disabled="nation.sperre">{{nation.kurztext}}</option>
</form-input>
<form-input
container-class="col-4"
label="Matrikelnummer"
type="text"
v-model="data.matr_nr"
name="matr_nr"
maxlength="32"
>
</form-input>
<form-input
container-class="col-4"
label="Sprache"
type="select"
v-model="data.sprache"
name="sprache"
>
<option v-for="sprache in lists.sprachen" :key="sprache.sprache" :value="sprache.sprache">{{sprache.sprache}}</option>
</form-input>
</div>
<div class="row mb-3">
<label for="stv-details-geschlecht" class="col-sm-1 col-form-label">Geschlecht</label>
<div class="col-sm-3">
<select id="stv-details-geschlecht" class="form-control" v-model="data.geschlecht">
<option v-for="geschlecht in lists.geschlechter" :key="geschlecht.geschlecht" :value="geschlecht.geschlecht">{{geschlecht.bezeichnung}}</option>
</select>
</div>
<label for="stv-details-familienstand" class="col-sm-1 col-form-label">Familienstand</label>
<div class="col-sm-3">
<select id="stv-details-familienstand" class="form-control" v-model="data.familienstand">
<option v-for="(bezeichnung, key) in familienstaende" :key="key" :value="key">{{bezeichnung}}</option>
</select>
</div>
<form-input
container-class="col-4"
label="Geschlecht"
type="select"
v-model="data.geschlecht"
name="geschlecht"
>
<option v-for="geschlecht in lists.geschlechter" :key="geschlecht.geschlecht" :value="geschlecht.geschlecht">{{geschlecht.bezeichnung}}</option>
</form-input>
<form-input
container-class="col-4"
label="Familienstand"
type="select"
v-model="data.familienstand"
name="familienstand"
>
<option v-for="(bezeichnung, key) in familienstaende" :key="key" :value="key">{{bezeichnung}}</option>
</form-input>
</div>
<div class="row mb-3">
<label for="stv-details-foto" class="col-sm-1 col-form-label">Foto</label>
<div class="col-sm-3">
<form-upload-image id="stv-details-foto" v-model="data.foto"></form-upload-image>
</div>
<label for="stv-details-anmerkung" class="col-sm-1 col-form-label">Anmerkung</label>
<div class="col-sm-3">
<textarea id="stv-details-anmerkung" class="form-control" v-model="data.anmerkung"></textarea>
</div>
<label for="stv-details-homepage" class="col-sm-1 col-form-label">Homepage</label>
<div class="col-sm-3">
<input id="stv-details-homepage" type="text" class="form-control" v-model="data.homepage" maxlength="256">
</div>
<form-input
container-class="col-4"
label="Foto"
type="UploadImage"
v-model="data.foto"
name="foto"
>
<img alt="No Image" :src="noImageSrc" class="w-100">
</form-input>
<form-input
container-class="col-4"
label="Anmerkung"
type="textarea"
v-model="data.anmerkung"
name="anmerkung"
rows="8"
>
</form-input>
<form-input
container-class="col-4"
label="Homepage"
type="text"
v-model="data.homepage"
name="homepage"
maxlength="256"
>
</form-input>
</div>
</template>
<div v-else>
@@ -315,47 +367,79 @@ export default {
<legend>StudentIn</legend>
<template v-if="data">
<div class="row mb-3">
<label for="stv-details-student_uid" class="col-sm-1 col-form-label">UID</label>
<div class="col-sm-3">
<input id="stv-details-student_uid" type="text" class="form-control" v-model="data.student_uid" disabled>
</div>
<label for="stv-details-personenkennzeichen" class="col-sm-1 col-form-label">Personenkennzeichen</label>
<div class="col-sm-3">
<input id="stv-details-personenkennzeichen" type="text" class="form-control" v-model="data.matrikelnr" disabled>
</div>
<label for="stv-details-aktiv" class="col-sm-1 col-form-label">Aktiv</label>
<div class="col-sm-3 align-self-center">
<div class="form-check">
<input id="stv-details-aktiv" type="checkbox" class="form-check-input" v-model="data.aktiv">
</div>
<form-input
container-class="col-4"
label="UID"
type="text"
v-model="data.student_uid"
name="student_uid"
readonly
>
</form-input>
<form-input
container-class="col-4"
label="Personenkennzeichen"
type="text"
v-model="data.matrikelnr"
name="matrikelnr"
readonly
>
</form-input>
<div class="col-4 pt-4 d-flex align-items-center">
<form-input
container-class="form-check"
label="Aktiv"
type="checkbox"
v-model="data.aktiv"
name="aktiv"
>
</form-input>
</div>
</div>
<div class="row mb-3">
<label for="stv-details-semester" class="col-sm-1 col-form-label">Semester</label>
<div class="col-sm-3">
<input id="stv-details-semester" type="text" class="form-control" v-model="data.semester" maxlength="2">
</div>
<label for="stv-details-verband" class="col-sm-1 col-form-label">Verband</label>
<div class="col-sm-3">
<input id="stv-details-verband" type="text" class="form-control" v-model="data.verband" maxlength="1">
</div>
<label for="stv-details-gruppe" class="col-sm-1 col-form-label">Gruppe</label>
<div class="col-sm-3">
<input id="stv-details-gruppe" type="text" class="form-control" v-model="data.gruppe" maxlength="1">
</div>
<form-input
container-class="col-4"
label="Semester"
type="text"
v-model="data.semester"
name="semester"
maxlength="2"
>
</form-input>
<form-input
container-class="col-4"
label="Verband"
type="text"
v-model="data.verband"
name="verband"
maxlength="1"
>
</form-input>
<form-input
container-class="col-4"
label="Gruppe"
type="text"
v-model="data.gruppe"
name="gruppe"
maxlength="1"
>
</form-input>
</div>
<div class="row mb-3">
<label for="stv-details-alias" class="col-sm-1 col-form-label">Alias</label>
<div class="col-sm-3">
<input id="stv-details-alias" type="text" class="form-control" v-model="data.alias" :disabled="aliasNotAllowed">
</div>
<form-input
container-class="col-4"
label="Alias"
type="text"
v-model="data.alias"
name="alias"
:disabled="aliasNotAllowed"
>
</form-input>
</div>
</template>
<div v-else>
Loading...
</div>
</fieldset>
<pv-toast ref="responseToast" style="z-index:9999"></pv-toast>
</form>`
</form-form>`
};
@@ -2,6 +2,8 @@ import {CoreFilterCmpt} from "../../../../filter/Filter.js";
import {CoreRESTClient} from '../../../../../RESTClient.js';
import ZeugnisActions from './Zeugnis/Actions.js';
const LOCAL_STORAGE_ID = 'stv_details_noten_zeugnis_2024-01-11_stdsem_all';
export default {
components: {
CoreFilterCmpt,
@@ -13,12 +15,13 @@ export default {
data() {
return {
validStudent: true,
tabulatorEvents: []
tabulatorEvents: [],
stdsem: ''
};
},
computed: {
ajaxURL() {
return CoreRESTClient._generateRouterURI('components/stv/Noten/getZeugnis/' + this.student.prestudent_id);
return CoreRESTClient._generateRouterURI('components/stv/Noten/getZeugnis/' + this.student.prestudent_id + this.stdsem);
},
tabulatorOptions() {
return {
@@ -59,29 +62,50 @@ export default {
};
}
},
watch: {
ajaxURL(n) {
if (this.$refs.table)
this.$refs.table.tabulator.setData(n);
}
},
methods: {
setGrades(selected) {
CoreRESTClient
.post('components/stv/Noten/update', selected)
.then(this.$refs.table.reloadTable)
.catch(this.$fhcAlert.handleFormValidation);
},
saveStdsem(event) {
window.localStorage.setItem(LOCAL_STORAGE_ID, event.target.value ? 'true' : '');
}
},
created() {
const savedPath = window.localStorage.getItem(LOCAL_STORAGE_ID);
this.stdsem = savedPath ? '/all' : '';
},
// TODO(chris): phrasen
template: `
<div class="stv-details-noten-zeugnis h-100 d-flex flex-column">
<div v-if="!validStudent">Kein Student</div>
<core-filter-cmpt
v-else
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
reload
>
<template #actions="{selected}">
<zeugnis-actions :selected="selected" @set-grades="setGrades"></zeugnis-actions>
</template>
</core-filter-cmpt>
<template v-else>
<div class="mb-3">
<select class="form-select" v-model="stdsem" @input="saveStdsem">
<option value="">Aktuelles Semester</option>
<option value="/all">Alle Semester</option>
</select>
</div>
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
reload
>
<template #actions="{selected}">
<zeugnis-actions :selected="selected" @set-grades="setGrades"></zeugnis-actions>
</template>
</core-filter-cmpt>
</template>
</div>`
};
+57 -42
View File
@@ -11,9 +11,14 @@ export default {
'changed'
],
props: {
configUrl: String,
// TODO(chris): rename to config?
configUrl: {
type: [String, Object],
required: true
},
default: String,
modelValue: [String, Number, Boolean, Array, Object, Date, Function, Symbol]
modelValue: [String, Number, Boolean, Array, Object, Date, Function, Symbol],
vertical: Boolean
},
data() {
return {
@@ -37,60 +42,70 @@ export default {
}
}
},
watch: {
configUrl(n) {
this.initConfig(n);
}
},
methods: {
change(key) {
this.$emit("change", key)
this.current = key;
this.$nextTick(() => this.$emit("changed", key));
}
},
created() {
CoreRESTClient
.get(this.configUrl)
.then(result => CoreRESTClient.getData(result.data))
.then(result => {
if (!result)
return;
},
initConfig(config) {
if (!config)
return;
if (typeof config === 'string' || config instanceof String)
return CoreRESTClient.get(config)
.then(result => CoreRESTClient.getData(result.data))
.then(this.initConfig)
.catch(this.$fhcAlert.handleSystemError);
const tabs = {};
console.log(config);
const tabs = {};
if (Array.isArray(result)) {
result.forEach((config, key) => {
if (!config.component)
return console.error('Component missing for ' + key);
if (Array.isArray(config)) {
config.forEach((item, key) => {
if (!item.component)
return console.error('Component missing for ' + key);
tabs[key] = {
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(config.component))),
title: config.title || key,
config: config.config,
key
}
});
tabs[key] = {
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(item.component))),
title: item.title || key,
config: item.config,
key
}
});
} else {
Object.entries(config).forEach(([key, item]) => {
if (!item.component)
return console.error('Component missing for ' + key);
} else {
Object.entries(result).forEach(([key, config]) => {
if (!config.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
}
});
}
tabs[key] = {
component: Vue.markRaw(Vue.defineAsyncComponent(() => import(config.component))),
title: config.title || key,
config: config.config,
key
}
});
}
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;
})
.catch(this.$fhcAlert.handleSystemError);
}
this.tabs = tabs;
}
},
created() {
this.initConfig(this.configUrl);
},
template: `
<div class="fhc-tabs d-flex flex-column" v-if="Object.keys(tabs).length">
<div class="nav nav-tabs">
<div class="fhc-tabs d-flex" :class="vertical ? 'align-items-stretch' : 'flex-column'" 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"
@@ -98,12 +113,12 @@ export default {
:class="{active: tab.key == current}"
@click="change(tab.key)"
:aria-current="tab.key == current ? 'page' : ''"
v-accessibility:tab
v-accessibility:tab.[vertical]
>
{{tab.title}}
</div>
</div>
<div style="flex: 1 1 0%; height: 0%" class="border-bottom border-start border-end overflow-auto p-3">
<div :style="vertical ? '' : 'flex: 1 1 0%; height: 0%'" class="overflow-auto p-3" :class="vertical ? '' : 'border-bottom border-start border-end'">
<keep-alive>
<component ref="current" :is="currentTab.component" v-model="value" :config="currentTab.config"></component>
</keep-alive>