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-09 12:57:28 +01:00
17 changed files with 276 additions and 72 deletions
@@ -23,6 +23,10 @@ class Config extends FHC_Controller
'title' => 'Kontakt',
'component' => './Stv/Studentenverwaltung/Details/Kontakt.js'
];
$result['noten'] = [
'title' => 'Noten',
'component' => './Stv/Studentenverwaltung/Details/Noten.js'
];
$result['notizen'] = [
'title' => 'Notizen',
'component' => './Stv/Studentenverwaltung/Details/Notizen.js'
@@ -0,0 +1,43 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Noten extends Auth_Controller
{
public function __construct()
{
parent::__construct([
'getZeugnis' => 'student/noten:r'
]);
// Load Libraries
$this->load->library('VariableLib', ['uid' => getAuthUID()]);
}
public function getZeugnis($prestudent_id)
{
$this->load->model('crm/Student_model', 'StudentModel');
$this->load->model('education/Zeugnisnote_model', 'ZeugnisnoteModel');
$result = $this->StudentModel->loadWhere([
'prestudent_id' => $prestudent_id
]);
if (isError($result)) {
$this->output->set_status_header(REST_Controller::HTTP_INTERNAL_SERVER_ERROR);
return $this->outputJson($result);
}
if (!hasData($result))
return $this->outputJsonSuccess(null);
$student_uid = current(getData($result))->student_uid;
$studiensemester_kurzbz = $this->variablelib->getVar('semester_aktuell');
$result = $this->ZeugnisnoteModel->getZeugnisnoten($student_uid, $studiensemester_kurzbz);
if (isError($result)) {
$this->output->set_status_header(REST_Controller::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->outputJson($result);
}
}
+2
View File
@@ -1,5 +1,7 @@
@import './components/searchbar.css';
@import './components/verticalsplit.css';
@import './components/FilterComponent.css';
@import './components/Tabs.css';
@import './components/Notiz.css';
html {
+6
View File
@@ -41,3 +41,9 @@
.tabulator {
font-size: 1rem;
}
.tabulator-row.tabulator-selectable:focus {
box-shadow: 0 0 0 .24rem rgba(13,110,253,.25);
z-index: 1;
outline: 0;
}
+16
View File
@@ -0,0 +1,16 @@
.nav-item.nav-link:focus {
box-shadow: 0 0 0 .24rem rgba(13,110,253,.25);
z-index: 1;
outline: 0;
position: relative;
}
.nav-item.nav-link:focus::after {
content: "";
display: block;
position: absolute;
left: -.5rem;
right: -.5rem;
top: calc(100% + 1px);
background: white;
height: .25rem;
}
+1 -1
View File
@@ -46,7 +46,7 @@ export default {
</div>
</template>
<template v-else>
<slot #placeholder>
<slot name="placeholder">
<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">
@@ -102,6 +102,7 @@ export default {
studiensemesterChanged(v) {
this.studiensemesterKurzbz = v;
this.$refs.stvList.updateUrl();
this.$refs.details.reload();
}
},
created() {
@@ -177,7 +178,7 @@ export default {
<stv-list ref="stvList" v-model:selected="selected" :studiengang-kz="studiengangKz" :studiensemester-kurzbz="studiensemesterKurzbz"></stv-list>
</template>
<template #bottom>
<stv-details :students="selected"></stv-details>
<stv-details ref="details" :students="selected"></stv-details>
</template>
</vertical-split>
</main>
@@ -15,6 +15,12 @@ export default {
return FHC_JS_DATA_STORAGE_OBJECT.app_root;
}
},
methods: {
reload() {
if (this.$refs.tabs?.$refs?.current?.reload)
this.$refs.tabs.$refs.current.reload();
}
},
template: `
<div class="stv-details h-100 pb-3 d-flex flex-column">
<div v-if="!students?.length" class="justify-content-center d-flex h-100 align-items-center">
@@ -24,11 +30,11 @@ export default {
<div class="d-flex justify-content-start align-items-center w-100 pb-3 gap-3" style="max-height:8rem">
<img v-for="student in students" :key="student.person_id" class="d-block h-100 rounded" alt="profilbild" :src="appRoot + '/cis/public/bild.php?src=person&person_id=' + student.person_id">
<div v-if="students.length == 1">
<h2 class="h4">{{students[0].titlepre}} {{students[0].vorname}} {{students[0].nachname}} {{students[0].titlepost}}
<h2 class="h4">{{students[0].titlepre}} {{students[0].vorname}} {{students[0].nachname}} {{students[0].titlepost}}</h2>
</div>
</div>
<fhc-tabs v-if="students.length == 1" :modelValue="students[0]" config-url="/components/stv/config/student" :default="$route.params.tab" style="flex: 1 1 0%; height: 0%"></fhc-tabs>
<fhc-tabs v-else :modelValue="students" config-url="/components/stv/config/students" :default="$route.params.tab" style="flex: 1 1 0%; height: 0%"></fhc-tabs>
<fhc-tabs v-if="students.length == 1" ref="tabs" :modelValue="students[0]" config-url="/components/stv/config/student" :default="$route.params.tab" style="flex: 1 1 0%; height: 0%" @changed="reload"></fhc-tabs>
<fhc-tabs v-else ref="tabs" :modelValue="students" config-url="/components/stv/config/students" :default="$route.params.tab" style="flex: 1 1 0%; height: 0%" @changed="reload"></fhc-tabs>
</div>
</div>`
};
@@ -299,7 +299,7 @@ export default {
</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-text="anmerkung"></textarea>
<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">
@@ -0,0 +1,19 @@
import NotenZeugnis from './Noten/Zeugnis.js';
export default {
components: {
NotenZeugnis
},
props: {
modelValue: Object
},
methods: {
reload() {
this.$refs.zeugnis.$refs.table.reloadTable();
}
},
template: `
<div class="stv-details-noten h-100 d-flex flex-column">
<noten-zeugnis ref="zeugnis" :student="modelValue"></noten-zeugnis>
</div>`
};
@@ -0,0 +1,72 @@
import {CoreFilterCmpt} from "../../../../filter/Filter.js";
import {CoreRESTClient} from '../../../../../RESTClient.js';
export default {
components: {
CoreFilterCmpt
},
props: {
student: Object
},
data() {
return {
validStudent: true,
tabulatorEvents: []
};
},
computed: {
ajaxURL() {
return CoreRESTClient._generateRouterURI('components/stv/Noten/getZeugnis/' + this.student.prestudent_id);
},
tabulatorOptions() {
return {
ajaxURL: this.ajaxURL,
ajaxResponse: (url, params, response) => {
if (!response.retval)
this.validStudent = false;
else
this.validStudent = true;
return response.retval || [];
},
columns: [
{ field: 'zeugnis', title: 'Zeugnis', formatter: 'tickCross' },
{ field: 'lehrveranstaltung_bezeichnung', title: 'Lehrveranstaltung' },
{ field: 'note_bezeichnung', title: 'Note' },
{ field: 'uebernahmedatum', title: 'Übernahmedatum', visible: false },
{ field: 'benotungsdatum', title: 'Benotungsdatum', visible: false },
{ field: 'benotungsdatum-iso', title: 'Benotungsdatum ISO', visible: false },
{ field: 'studiensemester_kurzbz', title: 'Studiensemester', visible: false },
{ field: 'note', title: 'Note Numerisch', visible: false },
{ field: 'lehrveranstaltung_id', title: 'Lehrveranstaltung ID', visible: false },
{ field: 'studiengang', title: 'Studiengang', visible: false },
{ field: 'studiengang_kz', title: 'Studiengang Kennzahl', visible: false },
{ field: 'studiengang_lv', title: 'StudiengangLV', visible: false },
{ field: 'studiengang_kz_lv', title: 'Studiengang_kzLV', visible: false },
{ field: 'semester_lv', title: 'SemesterLV', visible: false },
{ field: 'ects_lv', title: 'ECTS', visible: false },
{ field: 'lehrform', title: 'Lehrform', visible: false },
{ field: 'kurzbz', title: 'Kurzbz', visible: false },
{ field: 'punkte', title: 'Punkte', visible: false },
{ field: 'lehrveranstaltung_bezeichnung_english', title: 'Englisch', visible: false }
],
layout: 'fitDataStretch',
height: '100%',
persistence: true
};
}
},
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
>
</core-filter-cmpt>
</div>`
};
@@ -60,11 +60,11 @@ export default {
{title:"GeburtsdatumISO", field:"geburtsdatum_iso", visible:false},
],
layout: 'fitDataFill',
layout: 'fitDataStretch',
layoutColumnsOnNewData: false,
height: 'auto',
height: '100%',
selectable: true,
// TODO(chris): select only one? selectMultiple with click?
selectableRangeMode: 'click',
index: 'prestudent_id',
persistence: true
},
@@ -126,7 +126,12 @@ export default {
case 'Enter':
case 'Space':
e.preventDefault();
this.$refs.table.tabulator.rowManager.findRow(this.focusObj).component.toggleSelect();
const e2 = new Event('click', e);
e2.altKey = e.altKey;
e2.ctrlKey = e.ctrlKey;
e2.shiftKey = e.shiftKey;
this.focusObj.dispatchEvent(e2);
//row.component.toggleSelect();
break;
case 'ArrowUp':
e.preventDefault();
@@ -159,6 +164,9 @@ export default {
}
},
onFocus(e) { // TODO(chris): this should be in the filter component
if (e.target.classList.contains('tablulator-container')) {
this.focusObj = this.changeFocus(e.target, e.target.querySelector('.tabulator-row'));
}
if (e.target.classList.contains('tabulator-tableholder')) {
this.focusObj = this.changeFocus(e.target, e.target.querySelector('.tabulator-row'));
}
@@ -167,20 +175,21 @@ export default {
// TODO(chris): focusin, focusout, keydown and tabindex should be in the filter component
// TODO(chris): filter component column chooser has no accessibilty features
template: `
<div class="stv-list h-100 pt-3" @focusin="onFocus" @focusout="onBlur" @keydown="onKeydown">
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
reload
new-btn-show
new-btn-label="InteressentIn"
@click:new="actionNewPrestudent"
tabindex="0"
>
</core-filter-cmpt>
<div class="stv-list h-100 pt-3">
<div class="tablulator-container d-flex flex-column h-100" tabindex="0" @focusin="onFocus" @focusout="onBlur" @keydown="onKeydown">
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
reload
new-btn-show
new-btn-label="InteressentIn"
@click:new="actionNewPrestudent"
>
</core-filter-cmpt>
</div>
<list-new ref="new" :studiengang-kz="studiengangKz" :studiensemester-kurzbz="studiensemesterKurzbz"></list-new>
</div>`
};
@@ -150,27 +150,28 @@ export default {
});
},
loadStudienplaene() {
CoreRESTClient
.post('components/stv/studienplan/get', {
studiengang_kz: this.formDataStg,
studiensemester_kurzbz: this.formDataSem,
ausbildungssemester: this.formData.ausbildungssemester,
orgform_kurzbz: this.formData.orgform_kurzbz
})
.then(result => CoreRESTClient.getData(result.data) || [])
.then(result => {
this.studienplaene = result;
if (this.formData.studienplan_id !== '' && !this.studienplaene.filter(plan => plan.studienplan_id == this.formData.studienplan_id).length)
this.formData.studienplan_id = '';
})
.catch(error => {
if (error.code == 'ERR_BAD_REQUEST') {
return this.studienplaene = [];
}
// NOTE(chris): repeat request
if (error.code != "ERR_CANCELED")
window.setTimeout(this.loadStudienplaene, 100);
})
if (this.formDataStg)
CoreRESTClient
.post('components/stv/studienplan/get', {
studiengang_kz: this.formDataStg,
studiensemester_kurzbz: this.formDataSem,
ausbildungssemester: this.formData.ausbildungssemester,
orgform_kurzbz: this.formData.orgform_kurzbz
})
.then(result => CoreRESTClient.getData(result.data) || [])
.then(result => {
this.studienplaene = result;
if (this.formData.studienplan_id !== '' && !this.studienplaene.filter(plan => plan.studienplan_id == this.formData.studienplan_id).length)
this.formData.studienplan_id = '';
})
.catch(error => {
if (error.code == 'ERR_BAD_REQUEST') {
return this.studienplaene = [];
}
// NOTE(chris): repeat request
if (error.code != "ERR_CANCELED")
window.setTimeout(this.loadStudienplaene, 100);
})
},
changeAddressNation(e) {
if (this.formData['geburtsnation'] == this.formData['address']['nation'])
@@ -280,7 +281,7 @@ export default {
</tbody>
</table>
</template>
<tempalte v-else>
<template v-else>
<div class="row">
<div class="col-sm-4 mb-3">
<form-input
@@ -670,7 +671,6 @@ export default {
</div>
</div>
</div>
</template>
</template>
<template #footer>
+43 -16
View File
@@ -6,7 +6,9 @@ export default {
accessibility
},
emits: [
'update:modelValue'
'update:modelValue',
'change',
'changed'
],
props: {
configUrl: String,
@@ -35,24 +37,49 @@ export default {
}
}
},
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 => {
const tabs = {};
// TODO(chris): check if result is array
Object.entries(result).forEach(([key, config]) => {
if (!config.component)
return console.error('Component missing for ' + key);
if (!result)
return;
tabs[key] = {
component: Vue.defineAsyncComponent(() => import(config.component)),
title: config.title || key,
config: config.config,
key
}
});
const tabs = {};
if (Array.isArray(result)) {
result.forEach((config, key) => {
if (!config.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
}
});
} 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(config.component))),
title: config.title || key,
config: config.config,
key
}
});
}
if (tabs[this.default])
this.current = this.default;
else
@@ -62,14 +89,14 @@ export default {
.catch(this.$fhcAlert.handleSystemError);
},
template: `
<div class="fhc-tabs d-flex flex-column">
<div class="fhc-tabs d-flex flex-column" v-if="Object.keys(tabs).length">
<div class="nav nav-tabs">
<div
v-for="tab in tabs"
:key="tab.key"
class="nav-item nav-link"
:class="{active: tab.key == current}"
@click="current=tab.key"
@click="change(tab.key)"
:aria-current="tab.key == current ? 'page' : ''"
v-accessibility:tab
>
@@ -78,7 +105,7 @@ export default {
</div>
<div style="flex: 1 1 0%; height: 0%" class="border-bottom border-start border-end overflow-auto p-3">
<keep-alive>
<component :is="currentTab.component" v-model="value" :config="currentTab.config"></component>
<component ref="current" :is="currentTab.component" v-model="value" :config="currentTab.config"></component>
</keep-alive>
</div>
</div>`
+4 -5
View File
@@ -50,8 +50,7 @@ export const CoreFilterCmpt = {
default: true
},
filterType: {
type: String,
required: true
type: String
},
tabulatorOptions: Object,
tabulatorEvents: Array,
@@ -199,7 +198,7 @@ export const CoreFilterCmpt = {
tabulatorOptions.columns = this.filteredColumns;
}
if (tabulatorOptions.columns && tabulatorOptions.columns.filter(el => el.formatter == 'rowSelection').length)
if (tabulatorOptions.selectable || (tabulatorOptions.columns && tabulatorOptions.columns.filter(el => el.formatter == 'rowSelection').length))
this.tabulatorHasSelector = true;
// Start the tabulator with the build options
@@ -235,7 +234,7 @@ export const CoreFilterCmpt = {
}
},
_updateTabulator() {
this.tabulatorHasSelector = this.filteredColumns.filter(el => el.formatter == 'rowSelection').length;
this.tabulatorHasSelector = this.tabulatorOptions.selectable || this.filteredColumns.filter(el => el.formatter == 'rowSelection').length;
this.tabulator.setColumns(this.filteredColumns);
this.tabulator.setData(this.filteredData);
},
@@ -529,7 +528,7 @@ export const CoreFilterCmpt = {
<span class="fa-solid fa-rotate-right" aria-hidden="true"></span>
</button>
<span v-if="$slots.actions && tabulatorHasSelector">Mit {{selectedData.length}} ausgewählten: </span>
<slot name="actions" v-bind="tabulatorHasSelector ? selectedData : []"></slot>
<slot name="actions" v-bind="{selected: tabulatorHasSelector ? selectedData : []}"></slot>
</div>
<div class="d-flex gap-1 align-items-baseline flex-grow-1 justify-content-end">
<span v-if="!tableOnly">[ {{ filterName }} ]</span>
@@ -26,8 +26,8 @@ export default {
default: []
},
names: {
type: Array,
default: []
type: Object,
default: {}
}
},
emits: {
+2 -2
View File
@@ -133,8 +133,8 @@ const helperApp = Vue.createApp({
helperAppContainer.parentElement.removeChild(helperAppContainer);
},
template: `
<pv-toast ref="toast" base-z-index="99999"></pv-toast>
<pv-toast ref="alert" base-z-index="99999" position="center">
<pv-toast ref="toast" :base-z-index="99999"></pv-toast>
<pv-toast ref="alert" :base-z-index="99999" position="center">
<template #message="slotProps">
<i class="fa fa-circle-exclamation fa-2xl mt-3"></i>
<div class="p-toast-message-text">