Merge branch 'master' into feature-61672/svnr_aus_infocenter_tool_entfernen

This commit is contained in:
Andreas Österreicher
2025-08-26 08:43:22 +02:00
452 changed files with 40366 additions and 7408 deletions
+2 -4
View File
@@ -95,10 +95,9 @@ var FHC_DialogLib = {
*/
_displayDialog: function(heading, message, icon, colorClass)
{
var html = "<p class='dialogmessage'><i class='glyphicon glyphicon-"+icon+"'></i>&nbsp;&nbsp;"+message+"</p>";
var html = "<p class='dialogmessage'>"+message+"</p>";
FHC_DialogLib.alertDefault(heading, html);
$(".ui-dialog-titlebar").addClass(colorClass+" text-center");
$(".glyphicon-"+icon).addClass(colorClass);
$(".ui-dialog-titlebar").addClass(colorClass+" alert text-center");
FHC_DialogLib._formatShortDialog();
},
@@ -113,7 +112,6 @@ var FHC_DialogLib = {
$(".ui-dialog-buttonpane.ui-widget-content").css("padding", ".3em .4em .5em .4em");
$(".ui-dialog .ui-dialog-content").css("padding", "0");
$(".ui-dialog-buttonset button").css("margin", "0");
$(".dialogmessage i.glyphicon").css("background-color", "transparent");
}
};
+6 -1
View File
@@ -6,5 +6,10 @@ export default {
{ }
);
},
getAuthInfo() {
return this.$fhcApi.get(
'/api/frontend/v1/AuthInfo/getAuthInfo',
{}
);
},
};
+6
View File
@@ -21,5 +21,11 @@ export default {
method: 'get',
url: '/api/frontend/v1/AuthInfo/getAuthUID'
};
},
getAuthInfo() {
return {
method: 'get',
url: '/api/frontend/v1/AuthInfo/getAuthInfo'
};
}
};
+10 -4
View File
@@ -16,10 +16,13 @@
*/
export default {
getAllBetriebsmittel(type, id) {
getAllBetriebsmittel(type, id, betriebsmitteltypes) {
return {
method: 'get',
url: 'api/frontend/v1/betriebsmittel/betriebsmittelP/getAllBetriebsmittel/' + type + '/' + id
url: 'api/frontend/v1/betriebsmittel/betriebsmittelP/getAllBetriebsmittel/' + type + '/' + id,
params: {
betriebsmitteltypes
}
};
},
addNewBetriebsmittel(person_id, params) {
@@ -48,10 +51,13 @@ export default {
url: 'api/frontend/v1/betriebsmittel/betriebsmittelP/deleteBetriebsmittel/' + betriebsmittelperson_id
};
},
getTypenBetriebsmittel() {
getTypenBetriebsmittel(betriebsmitteltypes) {
return {
method: 'get',
url: 'api/frontend/v1/betriebsmittel/betriebsmittelP/getTypenBetriebsmittel/'
url: 'api/frontend/v1/betriebsmittel/betriebsmittelP/getTypenBetriebsmittel/',
params: {
betriebsmitteltypes
}
};
},
loadInventarliste(query) {
+37
View File
@@ -0,0 +1,37 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getHeader(person_id){
return {
method: 'get',
url: 'api/frontend/v1/vertraege/vertraege/getHeader/' + person_id,
};
},
getPersonAbteilung(mitarbeiter_uid){
return {
method: 'get',
url: 'api/frontend/v1/vertraege/vertraege/getPersonAbteilung/' + mitarbeiter_uid,
};
},
getLeitungOrg(oekurzbz){
return {
method: 'get',
url: 'api/frontend/v1/vertraege/vertraege/getLeitungOrg/' + oekurzbz,
};
},
}
+89
View File
@@ -0,0 +1,89 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getOrgHeads() {
var url = 'api/frontend/v1/funktionen/Funktionen/getOrgHeads';
return {
method: 'get',
url,
};
},
getOrgetsForCompany(unternehmen) {
var url = 'api/frontend/v1/funktionen/Funktionen/getOrgetsForCompany'
+ '/' + unternehmen;
return {
method: 'get',
url,
};
},
getAllOrgUnits(filterStudent) {
var url = 'api/frontend/v1/funktionen/Funktionen/getAllOrgUnits';
return {
method: 'get',
url,
};
},
getAllUserFunctions(mitarbeiter_uid) {
var url = 'api/frontend/v1/funktionen/Funktionen/getAllUserFunctions'
+ '/' + mitarbeiter_uid;
return {
method: 'get',
url,
};
},
getAllFunctions() {
var url = 'api/frontend/v1/funktionen/Funktionen/getAllFunctions';
return {
method: 'get',
url,
};
},
addFunction(params) {
return {
method: 'post',
url: 'api/frontend/v1/funktionen/Funktionen/insertFunction/',
params
};
},
loadFunction(benutzerfunktion_id) {
var url = 'api/frontend/v1/funktionen/Funktionen/loadFunction'
+ '/' + benutzerfunktion_id;
return {
method: 'get',
url,
};
},
updateFunction(params) {
return {
method: 'post',
url: 'api/frontend/v1/funktionen/Funktionen/updateFunction/',
params
};
},
deleteFunction(benutzerfunktion_id) {
return {
method: 'post',
url: 'api/frontend/v1/funktionen/Funktionen/deleteFunction/' + benutzerfunktion_id
};
},
getOes(head, searchString) {
return {
method: 'get',
url: 'api/frontend/v1/funktionen/Funktionen/searchOes/' + head + '/' + searchString
};
},
};
+25
View File
@@ -0,0 +1,25 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getAll() {
return {
method: 'get',
url: '/api/frontend/v1/language/get'
};
}
};
@@ -18,49 +18,79 @@
export default {
getRoomInfo(ort_kurzbz, start_date, end_date) {
return {
method: 'get',
url: '/api/frontend/v1/Stundenplan/getRoomplan',
method: 'post',
url: '/api/frontend/v1/LvPlan/getRoomplan',
params: { ort_kurzbz, start_date, end_date }
};
},
getStundenplan(start_date, end_date, lv_id) {
getLvPlan(start_date, end_date, lv_id) {
return {
method: 'get',
url: '/api/frontend/v1/Stundenplan/getStundenplan',
url: '/api/frontend/v1/LvPlan/getLvPlan',
params: { start_date, end_date, lv_id }
};
},
eventsPersonal(start_date, end_date) {
return {
method: 'post',
url: '/api/frontend/v1/lvPlan/eventsPersonal',
params: { start_date, end_date }
};
},
eventsLv(lv_id, start_date, end_date) {
return {
method: 'post',
url: '/api/frontend/v1/lvPlan/eventsLv',
params: { lv_id, start_date, end_date }
};
},
getStunden() {
// TODO(chris): seems to be called from nowhere?
return {
method: 'get',
url: '/api/frontend/v1/Stundenplan/Stunden'
url: '/api/frontend/v1/LvPlan/Stunden'
};
},
getOrtReservierungen(ort_kurzbz, start_date, end_date) {
return {
method: 'get',
url: `/api/frontend/v1/Stundenplan/Reservierungen/${ort_kurzbz}`,
method: 'post',
url: `/api/frontend/v1/LvPlan/getReservierungen/${ort_kurzbz}`,
params: { start_date, end_date }
};
},
getStundenplanReservierungen(start_date, end_date) {
getLvPlanReservierungen(start_date, end_date) {
return {
method: 'get',
url: '/api/frontend/v1/Stundenplan/Reservierungen',
method: 'post',
url: '/api/frontend/v1/LvPlan/getReservierungen',
params: { start_date, end_date }
};
},
getLehreinheitStudiensemester(lehreinheit_id) {
return {
method: 'get',
url: `/api/frontend/v1/Stundenplan/getLehreinheitStudiensemester/${lehreinheit_id}`
url: `/api/frontend/v1/LvPlan/getLehreinheitStudiensemester/${lehreinheit_id}`
};
},
studiensemesterDateInterval(date) {
return {
method: 'get',
url: `/api/frontend/v1/Stundenplan/studiensemesterDateInterval/${date}`
url: `/api/frontend/v1/LvPlan/studiensemesterDateInterval/${date}`
};
},
LvPlanEvents(start_date, end_date, lv_id) {
return {
method: 'post',
url: '/api/frontend/v1/LvPlan/LvPlanEvents',
params: {
start_date: start_date,
end_date: end_date,
lv_id: lv_id
}
};
},
getLv(lehrveranstaltung_id) {
return {
method: 'get',
url: '/api/frontend/v1/LvPlan/getLv/' + lehrveranstaltung_id
};
}
};
+110
View File
@@ -0,0 +1,110 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getMessages(params) {
return {
method: 'get',
url: 'api/frontend/v1/messages/messages/getMessages/'
+ params.id + '/'
+ params.type + '/'
+ params.size + '/'
+ params.page
};
},
getVorlagen(){
return {
method: 'get',
url: 'api/frontend/v1/messages/messages/getVorlagen/'
};
},
getMsgVarsLoggedInUser(){
return {
method: 'get',
url: 'api/frontend/v1/messages/messages/getMsgVarsLoggedInUser/'
};
},
getMessageVarsPerson(userParams){
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/getMessageVarsPerson/' + userParams.id + '/' + userParams.type_id
};
},
getMsgVarsPrestudent(userParams){
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/getMsgVarsPrestudent/' + userParams.id + '/' + userParams.type_id
};
},
getPersonId(params){
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/getPersonId/' + params.id + '/' + params.type_id
};
},
getUid(userParams){
return {
method: 'get',
url: 'api/frontend/v1/messages/messages/getUid/' + userParams.id + '/' + userParams.type_id
};
},
getVorlagentext(vorlage_kurzbz){
return {
method: 'get',
url: 'api/frontend/v1/messages/messages/getVorlagentext/' + vorlage_kurzbz
};
},
getNameOfDefaultRecipient(params){
return {
method: 'get',
url: 'api/frontend/v1/messages/messages/getNameOfDefaultRecipient/' + params.id + '/' + params.type_id
};
},
getPreviewText(userParams, params){
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/getPreviewText/' + userParams.id + '/' + userParams.type_id,
params
};
},
getReplyData(messageId){
return {
method: 'get',
url: 'api/frontend/v1/messages/messages/getReplyData/' + messageId
};
},
sendMessageFromModalContext(id, params) {
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/sendMessage/' + id,
params
};
},
sendMessage(id, params) {
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/sendMessage/' + id,
params
};
},
deleteMessage(messageId){
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/deleteMessage/' + messageId
};
}
}
+6
View File
@@ -83,5 +83,11 @@ export default {
method: 'get',
url: 'api/frontend/v1/notiz/notizPerson/isBerechtigt/'
};
},
getCountNotes(person_id){
return {
method: 'get',
url: 'api/frontend/v1/notiz/notizPerson/getCountNotes/' + person_id
};
}
};
+8 -3
View File
@@ -16,10 +16,15 @@
*/
export default {
getView(uid) {
profilViewData(uid) {
let url = "/api/frontend/v1/Profil/profilViewData";
if(uid){
url += `/${uid}`;
}
return {
method: 'get',
url: `/api/frontend/v1/Profil/getView/${uid}`
url: url
};
},
fotoSperre(value) {
@@ -69,5 +74,5 @@ export default {
method: 'get',
url: '/api/frontend/v1/Profil/getAllNationen'
};
}
},
};
+7
View File
@@ -61,6 +61,13 @@ export default {
params: dms
};
},
updateProfilbild(dms) {
return {
method: 'post',
url: `/api/frontend/v1/ProfilUpdate/updateProfilbild`,
params: dms
};
},
getProfilRequestFiles(requestID) {
return {
method: 'get',
+12
View File
@@ -0,0 +1,12 @@
export default {
loadRenderers() {
return {
method: 'get',
url: '/api/frontend/v1/RendererLoader/GetRenderers',
params: {
}
};
},
}
+14
View File
@@ -22,5 +22,19 @@ export default {
url: '/api/frontend/v1/searchbar/search',
params
};
},
searchCis(params) {
return {
method: 'post',
url: '/api/frontend/v1/searchbar/searchCis',
params
};
},
searchStv(params) {
return {
method: 'post',
url: '/api/frontend/v1/searchbar/searchStv',
params
};
}
};
+3 -1
View File
@@ -29,6 +29,7 @@ import exam from './stv/exam.js';
import abschlusspruefung from './stv/abschlusspruefung.js';
import grades from './stv/grades.js';
import mobility from './stv/mobility.js';
import admissionDates from './stv/admissionDates.js';
export default {
app,
@@ -44,5 +45,6 @@ export default {
exam,
abschlusspruefung,
grades,
mobility
mobility,
admissionDates
};
+13 -1
View File
@@ -105,7 +105,19 @@ export default {
return {
method: 'post',
url: 'api/frontend/v1/stv/abschlusspruefung/checkForExistingExams/',
params: { uid }
params: { uids }
};
},
getAllMitarbeiter(){
return {
method: 'get',
url: 'api/frontend/v1/stv/abschlusspruefung/getAllMitarbeiter/'
};
},
getAllPersons(){
return {
method: 'get',
url: 'api/frontend/v1/stv/abschlusspruefung/getAllPersons/'
};
}
};
@@ -0,0 +1,99 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getAufnahmetermine(person_id) {
return {
method: 'get',
url: 'api/frontend/v1/stv/aufnahmetermine/getAufnahmetermine/' + person_id,
};
},
getListPlacementTests(prestudent_id){
return {
method: 'get',
url: 'api/frontend/v1/stv/aufnahmetermine/getListPlacementTests/' + prestudent_id,
};
},
getListStudyPlans(person_id){
return {
method: 'get',
url: 'api/frontend/v1/stv/aufnahmetermine/getListStudyPlans/' + person_id,
};
},
addNewPlacementTest(params){
return {
method: 'post',
url: 'api/frontend/v1/stv/aufnahmetermine/insertAufnahmetermin/',
params
};
},
loadPlacementTest(rt_person_id){
return {
method: 'get',
url: 'api/frontend/v1/stv/aufnahmetermine/loadAufnahmetermin/' + rt_person_id,
};
},
updatePlacementTest(params){
return {
method: 'post',
url: 'api/frontend/v1/stv/aufnahmetermine/updateAufnahmetermin/',
params
};
},
deletePlacementTest(rt_person_id){
return {
method: 'post',
url: 'api/frontend/v1/stv/aufnahmetermine/deleteAufnahmetermin/' + rt_person_id
};
},
loadDataRtPrestudent(prestudent_id){
return {
method: 'get',
url: 'api/frontend/v1/stv/aufnahmetermine/loadDataRtPrestudent/' + prestudent_id,
};
},
saveDataRtPrestudent(params){
return {
method: 'post',
url: 'api/frontend/v1/stv/aufnahmetermine/insertOrUpdateDataRtPrestudent/',
params
};
},
loadAufnahmegruppen(params){
return {
method: 'get',
url: 'api/frontend/v1/stv/aufnahmetermine/loadAufnahmegruppen/',
params
};
},
getResultReihungstest(params){
return {
method: 'get',
url: 'api/frontend/v1/stv/aufnahmetermine/getResultReihungstest/',
params
};
},
loadFutureReihungstests(params){
return {
method: 'get',
url: 'api/frontend/v1/stv/aufnahmetermine/getZukuenftigeReihungstestStg/',
params
};
},
}
+43
View File
@@ -0,0 +1,43 @@
export default {
getArchiv(person_id) {
return {
method: 'get',
url: 'api/frontend/v1/stv/archiv/getArchiv',
params: { person_id }
};
},
getArchivVorlagen() {
return {
method: 'get',
url: 'api/frontend/v1/stv/archiv/getArchivVorlagen'
};
},
archive(data) {
return {
method: 'post',
url: 'api/frontend/v1/documents/archive',
params: data
};
},
archiveSigned(data) {
return {
method: 'post',
url: 'api/frontend/v1/documents/archiveSigned',
params: data
};
},
update(data) {
return {
method: 'post',
url: 'api/frontend/v1/stv/archiv/update',
params: data
};
},
delete(akte_id, studiengang_kz) {
return {
method: 'post',
url: 'api/frontend/v1/stv/archiv/delete',
params: {akte_id, studiengang_kz}
};
}
};
+35
View File
@@ -0,0 +1,35 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getCourselist(params) {
return {
method: 'get',
url: 'api/frontend/v1/stv/LvTermine/getStundenplan/' + params.student_uid + '/'
+ params.start_date + '/'
+ params.end_date + '/'
+ params.group_consecutiveHours + '/'
+ params.dbStundenplanTable
};
},
getStudiensemester(){
return {
method: 'get',
url: 'api/frontend/v1/stv/LvTermine/getStudiensemester/'
};
},
}
+10 -4
View File
@@ -16,16 +16,22 @@
*/
export default {
get(prestudent_id) {
get(prestudent_id, studiensemester_kurzbz) {
return {
method: 'post',
url: 'api/frontend/v1/stv/student/get/' + prestudent_id
url: 'api/frontend/v1/stv/student/get/'
+ encodeURIComponent(prestudent_id)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz)
};
},
save(prestudent_id, params) {
save(prestudent_id, studiensemester_kurzbz, params) {
return {
method: 'post',
url: 'api/frontend/v1/stv/student/save/' + prestudent_id,
url: 'api/frontend/v1/stv/student/save/'
+ encodeURIComponent(prestudent_id)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz),
params
};
},
+76
View File
@@ -0,0 +1,76 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getDocumentsUnaccepted(params) {
return {
method: 'get',
url: 'api/frontend/v1/stv/dokumente/getDocumentsUnaccepted/' + params.id + '/' + params.studiengang_kz
};
},
getDocumentsAccepted(params) {
return {
method: 'get',
url: 'api/frontend/v1/stv/dokumente/getDocumentsAccepted/' + params.id + '/' + params.studiengang_kz
};
},
deleteZuordnung(params){
return {
method: 'post',
url: 'api/frontend/v1/stv/dokumente/deleteZuordnung/' + params.prestudent_id + '/' + params.dokument_kurzbz
};
},
createZuordnung(params){
return {
method: 'post',
url: 'api/frontend/v1/stv/dokumente/createZuordnung/' + params.prestudent_id + '/' + params.dokument_kurzbz
};
},
loadAkte(akte_id){
return {
method: 'get',
url: 'api/frontend/v1/stv/dokumente/loadAkte/' + akte_id
};
},
getDoktypen(){
return {
method: 'get',
url: 'api/frontend/v1/stv/dokumente/getDoktypen/'
};
},
updateFile(akte_id, params){
return {
method: 'post',
url: 'api/frontend/v1/stv/dokumente/updateAkte/' + akte_id,
params
};
},
deleteFile(akte_id){
console.log("in deleteFile " + akte_id);
return {
method: 'post',
url: 'api/frontend/v1/stv/dokumente/deleteAkte/' + akte_id,
};
},
uploadFile(prestudent_id, params){
return {
method: 'post',
url: 'api/frontend/v1/stv/dokumente/uploadDokument/' + prestudent_id,
params
};
},
}
+75
View File
@@ -0,0 +1,75 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getAnrechnungen(id) {
return {
method: 'get',
url: 'api/frontend/v1/stv/anrechnungen/getAnrechnungen/' + id
};
},
getLehrveranstaltungen(prestudent_id){
return {
method: 'get',
url: 'api/frontend/v1/stv/anrechnungen/getLehrveranstaltungen/' + prestudent_id
};
},
getBegruendungen(){
return {
method: 'get',
url: 'api/frontend/v1/stv/anrechnungen/getBegruendungen/'
};
},
getLvsKompatibel(lv_id){
return {
method: 'get',
url: 'api/frontend/v1/stv/anrechnungen/getLvsKompatibel/' + lv_id
};
},
getLektoren(studiengang_kz){
return {
method: 'get',
url: 'api/frontend/v1/stv/anrechnungen/getLektoren/' + studiengang_kz
};
},
addNewAnrechnung(params){
return {
method: 'post',
url: 'api/frontend/v1/stv/anrechnungen/insertAnrechnung/',
params
};
},
loadAnrechnung(anrechnung_id){
return {
method: 'get',
url: 'api/frontend/v1/stv/anrechnungen/loadAnrechnung/' + anrechnung_id
};
},
editAnrechnung(params){
return {
method: 'post',
url: 'api/frontend/v1/stv/anrechnungen/updateAnrechnung/',
params
};
},
deleteAnrechnung(anrechnung_id){
return {
method: 'post',
url: 'api/frontend/v1/stv/anrechnungen/deleteAnrechnung/' + anrechnung_id
};
},
}
+24 -11
View File
@@ -22,25 +22,34 @@ export default {
url: 'api/frontend/v1/stv/grades/list'
};
},
getCertificate(prestudent_id, all) {
all = all ? '/all' : '';
getCertificate(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/grades/getCertificate/' + encodeURIComponent(prestudent_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return {
method: 'get',
url: 'api/frontend/v1/stv/grades/getCertificate/' + prestudent_id + all
url: url
};
},
getTeacherProposal(prestudent_id, all) {
all = all ? '/all' : '';
getTeacherProposal(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/grades/getTeacherProposal/' + encodeURIComponent(prestudent_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return {
method: 'get',
url: 'api/frontend/v1/stv/grades/getTeacherProposal/' + prestudent_id + all
url: url
};
},
getRepeaterGrades(prestudent_id, all) {
all = all ? '/all' : '';
getRepeaterGrades(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/grades/getRepeaterGrades/' + encodeURIComponent(prestudent_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return {
method: 'get',
url: 'api/frontend/v1/stv/grades/getRepeaterGrades/' + prestudent_id + all
url: url
};
},
updateCertificate({lehrveranstaltung_id, student_uid, studiensemester_kurzbz, note, lehrveranstaltung_bezeichnung}) {
@@ -86,11 +95,15 @@ export default {
}
};
},
getGradeFromPoints(points, lehrveranstaltung_id) {
getGradeFromPoints(points, lehrveranstaltung_id, studiensemester_kurzbz) {
return {
method: 'post',
url: 'api/frontend/v1/stv/grades/getGradeFromPoints',
params: { points, lehrveranstaltung_id }
params: {
"points": points,
"lehrveranstaltung_id": lehrveranstaltung_id,
"studiensemester_kurzbz": studiensemester_kurzbz
}
};
}
};
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getGruppen(id) {
return {
method: 'get',
url: 'api/frontend/v1/stv/Gruppen/getGruppen/' + id
};
},
deleteGroup(params) {
return {
method: 'post',
url: 'api/frontend/v1/stv/Gruppen/deleteGruppe/',
params
};
}
}
+81
View File
@@ -0,0 +1,81 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getStudies(uid) {
return {
method: 'get',
url: 'api/frontend/v1/stv/GemeinsameStudien/getStudien/' + uid
};
},
getTypenMobility(){
return {
method: 'get',
url: 'api/frontend/v1/stv/GemeinsameStudien/getTypenMobility/'
};
},
getStudiensemester(){
return {
method: 'get',
url: 'api/frontend/v1/stv/GemeinsameStudien/getStudiensemester/'
};
},
getStudyprograms(){
return {
method: 'get',
url: 'api/frontend/v1/stv/GemeinsameStudien/getStudienprogramme/'
};
},
getListPartner(){
return {
method: 'get',
url: 'api/frontend/v1/stv/GemeinsameStudien/getPartnerfirmen/'
};
},
getStatiPrestudent(){
return {
method: 'get',
url: 'api/frontend/v1/stv/GemeinsameStudien/getStatiPrestudent/'
};
},
loadStudy(id){
return {
method: 'get',
url: 'api/frontend/v1/stv/GemeinsameStudien/loadStudie/' + id
};
},
insertStudy(params){
return {
method: 'post',
url: 'api/frontend/v1/stv/GemeinsameStudien/insertStudie/',
params
};
},
updateStudy(params){
return {
method: 'post',
url: 'api/frontend/v1/stv/GemeinsameStudien/updateStudie/',
params
};
},
deleteStudy(id){
return {
method: 'post',
url: 'api/frontend/v1/stv/GemeinsameStudien/deleteStudie/' + id
};
},
}
@@ -67,5 +67,11 @@ export default {
method: 'get',
url: 'api/frontend/v1/stv/address/getNations/'
};
},
getAllFirmen(){
return {
method: 'get',
url: 'api/frontend/v1/stv/kontakt/getAllFirmen/'
}
}
};
+4 -3
View File
@@ -51,7 +51,8 @@ export default {
deleteMobility(bisio_id) {
return {
method: 'post',
url: 'api/frontend/v1/stv/mobility/deleteMobility/' + bisio_id
url: 'api/frontend/v1/stv/mobility/deleteMobility/',
params: { bisio_id }
};
},
getLVList(studiengang_kz) {
@@ -100,7 +101,7 @@ export default {
deleteMobilityPurpose(params) {
return {
method: 'post',
url: 'api/frontend/v1/stv/mobility/deleteMobilityPurpose/' + params.bisio_id,
url: 'api/frontend/v1/stv/mobility/deleteMobilityPurpose/',
params
};
},
@@ -108,7 +109,7 @@ export default {
return {
method: 'post',
url: 'api/frontend/v1/stv/mobility/addMobilityPurpose/' + params.bisio_id,
params
params: params
};
},
deleteMobilitySupport(params) {
+5 -2
View File
@@ -17,10 +17,13 @@
export default {
//------------- Prestudent.js------------------------------------------------------
get(prestudent_id) {
get(prestudent_id, studiensemester_kurzbz) {
return {
method: 'post',
url: 'api/frontend/v1/stv/prestudent/get/' + prestudent_id
url: 'api/frontend/v1/stv/prestudent/get/'
+ encodeURIComponent(prestudent_id)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz)
};
},
updatePrestudent(prestudent_id, params) {
+18 -6
View File
@@ -16,22 +16,34 @@
*/
export default {
uid(uid) {
uid(uid, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/students/'
+ encodeURIComponent(studiensemester_kurzbz)
+ '/uid/'
+ encodeURIComponent(uid);
return {
method: 'get',
url: 'api/frontend/v1/stv/students/uid/' + uid
url: url
};
},
prestudent(prestudent_id) {
prestudent(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/students/'
+ encodeURIComponent(studiensemester_kurzbz)
+ '/prestudent/'
+ encodeURIComponent(prestudent_id);
return {
method: 'get',
url: 'api/frontend/v1/stv/students/prestudent/' + prestudent_id
url: url
};
},
person(person_id) {
person(person_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/students/'
+ encodeURIComponent(studiensemester_kurzbz)
+ '/person/'
+ encodeURIComponent(person_id);
return {
method: 'get',
url: 'api/frontend/v1/stv/students/person/' + person_id
url: url
};
},
verband(relative_path) {
+32
View File
@@ -0,0 +1,32 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getVorlagen() {
return {
method: 'get',
url: 'api/frontend/v1/vorlagen/vorlagen/getVorlagen/'
};
},
getVorlagenByLoggedInUser() {
return {
method: 'get',
url: 'api/frontend/v1/vorlagen/vorlagen/getVorlagenByLoggedInUser/'
};
},
};
+11 -2
View File
@@ -22,7 +22,7 @@ import filter from "./filter.js";
import studstatus from "./studstatus.js";
import profil from "./profil.js";
import profilUpdate from "./profilUpdate.js";
import stundenplan from "./stundenplan.js";
import lvPlan from "./lvPlan.js";
import bookmark from "./bookmark.js";
import stv from "./stv.js";
import notiz from "./notiz.js";
@@ -33,10 +33,14 @@ import ort from "./ort.js";
import cms from "./cms.js";
import lehre from "./lehre.js";
import addons from "./addons.js";
import messages from "./messages.js";
import vorlagen from "./vorlagen.js";
import studiengang from "./studiengang.js";
import menu from "./menu.js";
import dashboard from "./dashboard.js";
import authinfo from "./authinfo.js";
import studium from "./studium.js";
import language from "./language.js";
export default {
search,
@@ -47,7 +51,7 @@ export default {
studstatus,
profil,
profilUpdate,
stundenplan,
lvPlan,
bookmark,
stv,
notiz,
@@ -57,8 +61,13 @@ export default {
ort,
cms,
lehre,
addons,
messages,
vorlagen,
addons,
studiengang,
menu,
authinfo,
studium,
language
};
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright (C) 2024 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getAll() {
return this.$fhcApi.get('/api/frontend/v1/language/get');
}
};
+75
View File
@@ -18,5 +18,80 @@ export default {
`/api/frontend/v1/Lehre/Pruefungen/${lehrveranstaltung_id}`
, {}
);
},
getStudentProjektarbeiten(uid) {
return this.$fhcApi.get(
`/api/frontend/v1/Lehre/getStudentProjektarbeiten/${uid}`
, {}
);
},
getStudentProjektabgaben(detail) {
return this.$fhcApi.get(
`/api/frontend/v1/Lehre/getStudentProjektabgaben`
, {
projektarbeit_id: detail.projektarbeit_id,
student_uid: detail.student_uid
}
);
},
postStudentProjektarbeitEndupload(formData) {
const url = '/api/frontend/v1/Lehre/postStudentProjektarbeitEndupload';
const headers = {Headers: { "Content-Type": "multipart/form-data" }}
return this.$fhcApi.post(url, formData, headers)
},
postStudentProjektarbeitZwischenabgabe(formData) {
const url = '/api/frontend/v1/Lehre/postStudentProjektarbeitZwischenabgabe';
const headers = {Headers: { "Content-Type": "multipart/form-data" }}
return this.$fhcApi.post(url, formData, headers)
},
getStudentProjektarbeitAbgabeFile(paabgabe_id, student_uid) {
const url = `/Cis/Abgabetool/getStudentProjektarbeitAbgabeFile?paabgabe_id=${paabgabe_id}&student_uid=${student_uid}`;
window.location = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url
},
getMitarbeiterProjektarbeiten(uid, all) {
return this.$fhcApi.get(
`/api/frontend/v1/Lehre/getMitarbeiterProjektarbeiten?showall=${all}`
, {}
);
},
postProjektarbeitAbgabe(termin) {
const payload = {
paabgabe_id: termin.paabgabe_id,
paabgabetyp_kurzbz: termin.bezeichnung.paabgabetyp_kurzbz,
datum: termin.datum,
fixtermin: termin.fixtermin,
insertvon: termin.insertvon,
kurzbz: termin.kurzbz,
projektarbeit_id: termin.projektarbeit_id
}
const url = '/api/frontend/v1/Lehre/postProjektarbeitAbgabe';
return this.$fhcApi.post(url, payload, null)
},
deleteProjektarbeitAbgabe(paabgabe_id) {
const payload = {
paabgabe_id
}
const url = '/api/frontend/v1/Lehre/deleteProjektarbeitAbgabe';
return this.$fhcApi.post(url, payload, null)
},
postSerientermin(datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, projektarbeit_ids) {
const payload = {
datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, projektarbeit_ids
}
const url = '/api/frontend/v1/Lehre/postSerientermin';
return this.$fhcApi.post(url, payload, null)
},
fetchDeadlines(person_id) {
const payload = {
person_id
}
const url = '/api/frontend/v1/Lehre/fetchDeadlines';
return this.$fhcApi.post(url, payload, null)
}
}
+47
View File
@@ -0,0 +1,47 @@
export default {
getRoomInfo(ort_kurzbz, start_date, end_date) {
return this.$fhcApi.post(
'/api/frontend/v1/LvPlan/getRoomplan',
{ ort_kurzbz, start_date, end_date}
);
},
getStunden() {
return this.$fhcApi.get(
'/api/frontend/v1/LvPlan/Stunden',
{}
);
},
getOrtReservierungen(ort_kurzbz, start_date, end_date) {
return this.$fhcApi.post(
`/api/frontend/v1/LvPlan/getReservierungen/${ort_kurzbz}`,
{ start_date, end_date}
);
},
getLvPlanReservierungen(start_date, end_date) {
return this.$fhcApi.post(
'/api/frontend/v1/LvPlan/getReservierungen',
{ start_date, end_date }
);
},
getLehreinheitStudiensemester(lehreinheit_id) {
return this.$fhcApi.get(
`/api/frontend/v1/LvPlan/getLehreinheitStudiensemester/${lehreinheit_id}`,
{}
);
},
studiensemesterDateInterval(date) {
return this.$fhcApi.get(
`/api/frontend/v1/LvPlan/studiensemesterDateInterval/${date}`,
{}
);
},
LvPlanEvents(start_date, end_date, lv_id) {
return this.$fhcApi.get(
'/api/frontend/v1/LvPlan/LvPlanEvents',
{ start_date, end_date, lv_id }
);
},
};
+5
View File
@@ -0,0 +1,5 @@
import person from "./messages/person.js";
export default {
person
}
+47
View File
@@ -0,0 +1,47 @@
export default {
getMessages(url, config, params) {
return this.$fhcApi.get('api/frontend/v1/messages/messages/getMessages/' + params.id + '/' + params.type + '/' + params.size + '/' + params.page);
},
getVorlagen(){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getVorlagen/');
},
getMsgVarsLoggedInUser(){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getMsgVarsLoggedInUser/');
},
getMessageVarsPerson(params){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getMessageVarsPerson/' + params.id + '/' + params.type_id);
},
getMsgVarsPrestudent(params){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getMsgVarsPrestudent/' + params.id + '/' + params.type_id);
},
getPersonId(params){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getPersonId/'+ params.id + '/' + params.type_id);
},
getUid(params){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getUid/'+ params.id + '/' + params.type_id);
},
getVorlagentext(vorlage_kurzbz){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getVorlagentext/' + vorlage_kurzbz);
},
getNameOfDefaultRecipient(params){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getNameOfDefaultRecipient/' + params.id + '/' + params.type_id);
},
getPreviewText(params, data){
return this.$fhcApi.post('api/frontend/v1/messages/messages/getPreviewText/' + params.id + '/' + params.type_id,
data);
},
getReplyData(messageId){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getReplyData/' + messageId);
},
sendMessageFromModalContext(form, id, data) {
return this.$fhcApi.post(form,'api/frontend/v1/messages/messages/sendMessage/' + id,
data);
},
sendMessage(id, data) {
return this.$fhcApi.post('api/frontend/v1/messages/messages/sendMessage/' + id,
data);
},
deleteMessage(messageId){
return this.$fhcApi.post('api/frontend/v1/messages/messages/deleteMessage/' + messageId);
}
}
+15
View File
@@ -7,4 +7,19 @@ export default {
{ ort_kurzbz: ort_kurbz }
);
},
getRooms(datum, von, bis, typ, personenanzahl = 0) {
return this.$fhcApi.get(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/api/frontend/v1/Ort/getRooms",
{ datum, von, bis, typ, personenanzahl }
);
},
getRoomTypes() {
return this.$fhcApi.get(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/api/frontend/v1/Ort/getTypes"
);
}
}
+9 -1
View File
@@ -16,8 +16,16 @@
*/
export default {
search(searchsettings) {
search(searchsettings, config) {
const url = '/api/frontend/v1/searchbar/search';
return this.$fhcApi.post(url, searchsettings, config);
},
searchAdvanced(searchsettings, config) {
const url = '/api/frontend/v1/searchbar/searchAdvanced';
return this.$fhcApi.post(url, searchsettings, config);
},
searchdummy(searchsettings) {
const url = 'public/js/apps/api/dummyapi.php/Search';
return this.$fhcApi.post(url, searchsettings);
}
};
+47
View File
@@ -0,0 +1,47 @@
export default {
getStudiensemester: function () {
return this.$fhcApi.get(
'/components/Cis/Mylv/Studiensemester',
{}
);
},
getAllStudienSemester: function (studiensemester=undefined, studiengang=undefined, semester=undefined, studienplan=undefined) {
return this.$fhcApi.get(
'/api/frontend/v1/Studium/getStudienAllSemester',
{studiensemester, studiengang, semester, studienplan}
);
},
getStudiengaengeForStudienSemester: function (studiensemester) {
return this.$fhcApi.get(
`/api/frontend/v1/Studium/getStudiengaengeForStudienSemester/${studiensemester}`,
{}
);
},
getStudienplaeneBySemester: function (studiengang, studiensemester) {
return this.$fhcApi.get(
`/api/frontend/v1/Studium/getStudienplaeneBySemester`,
{
studiengang,
studiensemester,
}
);
},
getLvPlanForStudiensemester: function (studiensemester, lvid) {
return this.$fhcApi.get(
`/api/frontend/v1/LvPlan/getLvPlanForStudiensemester/${studiensemester}/${lvid}`,
{
}
);
},
getLvEvaluierungInfo: function (studiensemester_kurzbz, lvid) {
return this.$fhcApi.get(
`/api/frontend/v1/Studium/getLvEvaluierungInfo/${studiensemester_kurzbz}/${lvid}`,
{
}
);
},
}
-45
View File
@@ -1,45 +0,0 @@
export default {
getRoomInfo(ort_kurzbz, start_date, end_date) {
return this.$fhcApi.get(
'/api/frontend/v1/Stundenplan/getRoomplan',
{ ort_kurzbz, start_date, end_date}
);
},
getStundenplan(start_date, end_date, lv_id) {
return this.$fhcApi.get(
'/api/frontend/v1/Stundenplan/getStundenplan',
{ start_date, end_date, lv_id }
);
},
getStunden() {
return this.$fhcApi.get(
'/api/frontend/v1/Stundenplan/Stunden',
{}
);
},
getOrtReservierungen(ort_kurzbz, start_date, end_date) {
return this.$fhcApi.get(
`/api/frontend/v1/Stundenplan/Reservierungen/${ort_kurzbz}`,
{ start_date, end_date}
);
},
getStundenplanReservierungen(start_date, end_date) {
return this.$fhcApi.get(
'/api/frontend/v1/Stundenplan/Reservierungen',
{ start_date, end_date }
);
},
getLehreinheitStudiensemester(lehreinheit_id) {
return this.$fhcApi.get(
`/api/frontend/v1/Stundenplan/getLehreinheitStudiensemester/${lehreinheit_id}`,
{}
);
},
studiensemesterDateInterval(date) {
return this.$fhcApi.get(
`/api/frontend/v1/Stundenplan/studiensemesterDateInterval/${date}`,
{}
);
},
};
+12
View File
@@ -2,6 +2,7 @@ import verband from './stv/verband.js';
import students from './stv/students.js';
import filter from './stv/filter.js';
import konto from './stv/konto.js';
import group from './stv/group.js';
import kontakt from './stv/kontakt.js';
import prestudent from './stv/prestudent.js';
import status from './stv/status.js';
@@ -10,12 +11,18 @@ import exam from './stv/exam.js';
import abschlusspruefung from './stv/abschlusspruefung.js';
import grades from './stv/grades.js';
import mobility from './stv/mobility.js';
import archiv from './stv/archiv.js';
import documents from './stv/documents.js';
import exemptions from './stv/exemptions.js';
import jointstudies from "./stv/jointstudies.js";
import courselist from './stv/courselist.js';
export default {
verband,
students,
filter,
konto,
group,
kontakt,
prestudent,
status,
@@ -24,6 +31,11 @@ export default {
abschlusspruefung,
grades,
mobility,
archiv,
documents,
exemptions,
jointstudies,
courselist,
configStudent() {
return this.$fhcApi.get('api/frontend/v1/stv/config/student');
},
+36
View File
@@ -0,0 +1,36 @@
export default {
tabulatorConfig(config, self) {
config.ajaxURL = 'api/frontend/v1/stv/archiv/get';
config.ajaxParams = () => {
const params = {
person_id: self.modelValue.person_id || self.modelValue.map(e => e.person_id)
};
return params;
};
config.ajaxRequestFunc = (url, config, params) => this.$fhcApi.post(url, params, config);
config.ajaxResponse = (url, params, response) => response.data;
return config;
},
getArchivVorlagen() {
return this.$fhcApi.post('api/frontend/v1/stv/archiv/getArchivVorlagen');
},
archive(data) {
return this.$fhcApi.post(
'api/frontend/v1/documents/archive',
data
);
},
archiveSigned(data) {
return this.$fhcApi.post(
'api/frontend/v1/documents/archiveSigned',
data
);
},
update(data) {
return this.$fhcApi.post('api/frontend/v1/stv/archiv/update', data);
},
delete({akte_id, studiengang_kz}) {
return this.$fhcApi.post('api/frontend/v1/stv/archiv/delete', {akte_id, studiengang_kz});
}
};
+15
View File
@@ -0,0 +1,15 @@
export default {
getCourselist(url, config, params) {
//corresponding logic controller Stundenplan.php
return this.$fhcApi.get('api/frontend/v1/stv/LvTermine/getStundenplan/'
+ params.student_uid + '/'
+ params.start_date + '/'
+ params.end_date + '/'
+ params.group_consecutiveHours + '/'
+ params.dbStundenplanTable
);
},
getStudiensemester(){
return this.$fhcApi.get('api/frontend/v1/stv/LvTermine/getStudiensemester/');
},
}
+12 -6
View File
@@ -1,10 +1,16 @@
export default {
get(prestudent_id) {
return this.$fhcApi.post('api/frontend/v1/stv/student/get/' + prestudent_id);
get(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/student/get/'
+ encodeURIComponent(prestudent_id)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz);
return this.$fhcApi.post(url);
},
save(form, prestudent_id, data) {
return this.$fhcApi.post(form, 'api/frontend/v1/stv/student/save/' + prestudent_id,
data
);
save(form, prestudent_id, studiensemester_kurzbz, data) {
let url = 'api/frontend/v1/stv/student/save/'
+ encodeURIComponent(prestudent_id)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz);
return this.$fhcApi.post(form, url, data);
},
}
+33
View File
@@ -0,0 +1,33 @@
export default {
getDocumentsUnaccepted(url, config, params) {
return this.$fhcApi.get('api/frontend/v1/stv/dokumente/getDocumentsUnaccepted/' + params.id + '/' + params.studiengang_kz);
},
getDocumentsAccepted(url, config, params) {
return this.$fhcApi.get('api/frontend/v1/stv/dokumente/getDocumentsAccepted/' + params.id + '/' + params.studiengang_kz);
},
deleteZuordnung(params){
return this.$fhcApi.post('api/frontend/v1/stv/dokumente/deleteZuordnung/' + params.prestudent_id + '/' + params.dokument_kurzbz);
},
createZuordnung(params){
return this.$fhcApi.post('api/frontend/v1/stv/dokumente/createZuordnung/'
+ params.prestudent_id + '/'
+ params.dokument_kurzbz);
},
loadAkte(akte_id){
return this.$fhcApi.get('api/frontend/v1/stv/dokumente/loadAkte/' + akte_id);
},
getDoktypen(){
return this.$fhcApi.get('api/frontend/v1/stv/dokumente/getDoktypen/');
},
updateFile(akte_id, data){
return this.$fhcApi.post('api/frontend/v1/stv/dokumente/updateAkte/' + akte_id,
data);
},
deleteFile(akte_id){
return this.$fhcApi.post('api/frontend/v1/stv/dokumente/deleteAkte/' + akte_id);
},
uploadFile(prestudent_id, data){
return this.$fhcApi.post('api/frontend/v1/stv/dokumente/uploadDokument/' + prestudent_id,
data);
},
}
+29
View File
@@ -0,0 +1,29 @@
export default {
getAnrechnungen(url, config, params) {
return this.$fhcApi.get('api/frontend/v1/stv/anrechnungen/getAnrechnungen/' + params.id);
},
getLehrveranstaltungen(prestudent_id){
return this.$fhcApi.get('api/frontend/v1/stv/anrechnungen/getLehrveranstaltungen/' + prestudent_id);
},
getBegruendungen(){
return this.$fhcApi.get('api/frontend/v1/stv/anrechnungen/getBegruendungen/');
},
getLvsKompatibel(lv_id){
return this.$fhcApi.get('api/frontend/v1/stv/anrechnungen/getLvsKompatibel/' + lv_id);
},
getLektoren(studiengang_kz){
return this.$fhcApi.get('api/frontend/v1/stv/anrechnungen/getLektoren/' + studiengang_kz);
},
addNewAnrechnung(form, data){
return this.$fhcApi.post(form, 'api/frontend/v1/stv/anrechnungen/insertAnrechnung/', data);
},
loadAnrechnung(anrechnung_id){
return this.$fhcApi.get('api/frontend/v1/stv/anrechnungen/loadAnrechnung/' + anrechnung_id);
},
editAnrechnung(form, data){
return this.$fhcApi.post(form, 'api/frontend/v1/stv/anrechnungen/updateAnrechnung/', data);
},
deleteAnrechnung(anrechnung_id){
return this.$fhcApi.post('api/frontend/v1/stv/anrechnungen/deleteAnrechnung/' + anrechnung_id);
},
}
+27 -11
View File
@@ -2,17 +2,26 @@ export default {
list() {
return this.$fhcApi.get('api/frontend/v1/stv/grades/list');
},
getCertificate(prestudent_id, all) {
all = all ? '/all' : '';
return this.$fhcApi.get('api/frontend/v1/stv/grades/getCertificate/' + prestudent_id + all);
getCertificate(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/grades/getCertificate/' + encodeURIComponent(prestudent_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return this.$fhcApi.get(url);
},
getTeacherProposal(prestudent_id, all) {
all = all ? '/all' : '';
return this.$fhcApi.get('api/frontend/v1/stv/grades/getTeacherProposal/' + prestudent_id + all);
getTeacherProposal(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/grades/getTeacherProposal/' + encodeURIComponent(prestudent_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return this.$fhcApi.get(url);
},
getRepeaterGrades(prestudent_id, all) {
all = all ? '/all' : '';
return this.$fhcApi.get('api/frontend/v1/stv/grades/getRepeaterGrades/' + prestudent_id + all);
getRepeaterGrades(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/grades/getRepeaterGrades/' + encodeURIComponent(prestudent_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return this.$fhcApi.get(url);
},
updateCertificate({lehrveranstaltung_id, student_uid, studiensemester_kurzbz, note, lehrveranstaltung_bezeichnung}) {
return this.$fhcApi.post(
@@ -65,8 +74,15 @@ export default {
}
);
},
getGradeFromPoints(points, lehrveranstaltung_id, manualErrorHandling) {
getGradeFromPoints(points, lehrveranstaltung_id, studiensemester_kurzbz, manualErrorHandling) {
const config = manualErrorHandling ? {errorHandling: false} : {};
return this.$fhcApi.post('api/frontend/v1/stv/grades/getGradeFromPoints', {points, lehrveranstaltung_id}, config);
return this.$fhcApi.post('api/frontend/v1/stv/grades/getGradeFromPoints',
{
"points": points,
"lehrveranstaltung_id": lehrveranstaltung_id,
"studiensemester_kurzbz": studiensemester_kurzbz
},
config
);
}
}
+8
View File
@@ -0,0 +1,8 @@
export default {
getGruppen(url, config, params) {
return this.$fhcApi.get('api/frontend/v1/stv/Gruppen/getGruppen/' + params.id);
},
deleteGroup(params) {
return this.$fhcApi.post('api/frontend/v1/stv/Gruppen/deleteGruppe/', params);
}
}
+33
View File
@@ -0,0 +1,33 @@
export default {
getStudies(url, config, params) {
return this.$fhcApi.get('api/frontend/v1/stv/GemeinsameStudien/getStudien/' + params.id);
},
getTypenMobility(){
return this.$fhcApi.get('api/frontend/v1/stv/GemeinsameStudien/getTypenMobility/');
},
getStudiensemester(){
return this.$fhcApi.get('api/frontend/v1/stv/GemeinsameStudien/getStudiensemester/');
},
getStudyprograms(){
return this.$fhcApi.get('api/frontend/v1/stv/GemeinsameStudien/getStudienprogramme/');
},
getListPartner(){
return this.$fhcApi.get('api/frontend/v1/stv/GemeinsameStudien/getPartnerfirmen/');
},
getStatiPrestudent(){
return this.$fhcApi.get('api/frontend/v1/stv/GemeinsameStudien/getStatiPrestudent/');
},
loadStudy(id){
return this.$fhcApi.get('api/frontend/v1/stv/GemeinsameStudien/loadStudie/' + id);
},
insertStudy(form, data){
return this.$fhcApi.post(form,'api/frontend/v1/stv/GemeinsameStudien/insertStudie/', data);
},
updateStudy(form, data){
return this.$fhcApi.post(form,'api/frontend/v1/stv/GemeinsameStudien/updateStudie/', data);
},
deleteStudy(id){
return this.$fhcApi.post('api/frontend/v1/stv/GemeinsameStudien/deleteStudie/' + id);
},
}
+7 -2
View File
@@ -2,8 +2,13 @@ export default {
//------------- Prestudent.js------------------------------------------------------
get(prestudent_id){
return this.$fhcApi.post('api/frontend/v1/stv/prestudent/get/' + prestudent_id);
get(prestudent_id, studiensemester_kurzbz) {
return this.$fhcApi.post(
'api/frontend/v1/stv/prestudent/get/'
+ encodeURIComponent(prestudent_id)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz)
);
},
updatePrestudent(form, prestudent_id, data){
return this.$fhcApi.post(form, 'api/frontend/v1/stv/prestudent/updatePrestudent/' + prestudent_id,
+18 -6
View File
@@ -1,12 +1,24 @@
export default {
uid(uid) {
return this.$fhcApi.getUri('api/frontend/v1/stv/students/uid/' + uid);
uid(uid, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/students/'
+ encodeURIComponent(studiensemester_kurzbz)
+ '/uid/'
+ encodeURIComponent(uid);
return this.$fhcApi.getUri(url);
},
prestudent(prestudent_id) {
return this.$fhcApi.getUri('api/frontend/v1/stv/students/prestudent/' + prestudent_id);
prestudent(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/students/'
+ encodeURIComponent(studiensemester_kurzbz)
+ '/prestudent/'
+ encodeURIComponent(prestudent_id);
return this.$fhcApi.getUri(url);
},
person(person_id) {
return this.$fhcApi.getUri('api/frontend/v1/stv/students/person/' + person_id);
person(person_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/students/'
+ encodeURIComponent(studiensemester_kurzbz)
+ '/person/'
+ encodeURIComponent(person_id);
return this.$fhcApi.getUri(url);
},
verband(relative_path) {
return this.$fhcApi.getUri('api/frontend/v1/stv/students/' + relative_path);
+8
View File
@@ -0,0 +1,8 @@
export default {
getVorlagen() {
return this.$fhcApi.get('api/frontend/v1/vorlagen/vorlagen/getVorlagen/');
},
getVorlagenByLoggedInUser() {
return this.$fhcApi.get('api/frontend/v1/vorlagen/vorlagen/getVorlagenByLoggedInUser/');
}
}
+37 -9
View File
@@ -2,6 +2,7 @@ import FhcSearchbar from "../components/searchbar/searchbar.js";
import CisMenu from "../components/Cis/Menu.js";
import PluginsPhrasen from '../plugins/Phrasen.js';
import ApiSearchbar from '../api/factory/searchbar.js';
import Theme from "../plugins/Theme.js";
const app = Vue.createApp({
name: 'CisApp',
@@ -12,14 +13,17 @@ const app = Vue.createApp({
data: function() {
return {
searchbaroptions: {
origin: "cis",
cssclass: "",
calcheightonly: true,
types: [
"mitarbeiter",
"student",
"raum",
"organisationunit"
],
types: {
employee: Vue.computed(() => this.$p.t("search/type_employee")),
student: Vue.computed(() => this.$p.t("search/type_student")),
room: Vue.computed(() => this.$p.t("search/type_room")),
organisationunit: Vue.computed(() => this.$p.t("search/type_organisationunit")),
cms: Vue.computed(() => this.$p.t("search/type_cms")),
dms: Vue.computed(() => this.$p.t("search/type_dms"))
},
actions: {
employee: {
defaultaction: {
@@ -42,7 +46,7 @@ const app = Vue.createApp({
},
childactions: []
},
raum: {
room: {
defaultaction: {
type: "link",
renderif: function(data) {
@@ -104,14 +108,37 @@ const app = Vue.createApp({
}
},
childactions: []
}
},
cms: {
defaultaction: {
type: "link",
action: function (data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
return link;
}
},
childactions: []
},
dms: {
defaultaction: {
type: "link",
action: function (data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
'cms/dms.php?id=' + data.dms_id;
return link;
}
},
childactions: []
}
}
}
};
},
methods: {
searchfunction: function(searchsettings) {
return this.$api.call(ApiSearchbar.search(searchsettings));
return this.$api.call(ApiSearchbar.searchCis(searchsettings));
}
}
});
@@ -122,4 +149,5 @@ app.use(primevue.config.default, {
}
})
app.use(PluginsPhrasen);
app.use(Theme);
app.mount('#cis-header');
+114 -50
View File
@@ -1,20 +1,35 @@
import FhcDashboard from '../../components/Dashboard/Dashboard.js';
import PluginsPhrasen from '../../plugins/Phrasen.js';
import Theme from '../../plugins/Theme.js';
import contrast from '../../directives/contrast.js';
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js";
import Stundenplan, {DEFAULT_MODE_STUNDENPLAN} from "../../components/Cis/Stundenplan/Stundenplan.js";
import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js";
import MyLvPlan from "../../components/Cis/LvPlan/Personal.js";
import MylvStudent from "../../components/Cis/Mylv/Student.js";
import Profil from "../../components/Cis/Profil/Profil.js";
import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js";
import CmsNews from "../../components/Cis/Cms/News.js";
import CmsContent from "../../components/Cis/Cms/Content.js";
import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js";
import RoomInformation, {DEFAULT_MODE_RAUMINFO} from "../../components/Cis/Mylv/RoomInformation.js";
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js";
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js";
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js";
import Studium from "../../components/Cis/Studium/Studium.js";
import ApiRenderers from '../../api/factory/renderers.js';
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(`/${ciPath}`),
routes: [
{
path: `/Cis/Studium`,
name: 'Studium',
component: Studium,
props: true
},
{
path: `/Cis/Profil/View/:uid`,
name: 'ProfilView',
@@ -27,7 +42,30 @@ const router = VueRouter.createRouter({
component: Profil,
props: true
},
{
path: `/Cis/Abgabetool/Student/:student_uid_prop?`,
name: 'AbgabetoolStudent',
component: AbgabetoolStudent,
props: true
},
{
path: `/Cis/Abgabetool/Mitarbeiter`,
name: 'AbgabetoolMitarbeiter',
component: AbgabetoolMitarbeiter,
props: true
},
{
path: `/Cis/Abgabetool/Deadlines/:person_uid_prop?`,
name: 'DeadlineOverview',
component: DeadlineOverview,
props: true
},
{
path: `/Cis/Raumsuche`,
name: 'Raumsuche',
component: Raumsuche,
props: true
},
// Redirect old links to new format
{
path: "/CisVue/Cms/getRoomInformation/:ort_kurzbz",
@@ -113,59 +151,41 @@ const router = VueRouter.createRouter({
},
// Redirect old links to new format
{
// only trigger on first param being numeric to avoid paths like "Stundenplan/Month" entering here
path: "/Cis/Stundenplan/:lv_id(\\d+)",
name: "StundenplanOld",
component: Stundenplan,
redirect: (to) => {
return { // redirect to longer Stundenplan url and map params
name: "Stundenplan",
// only trigger on first param being numeric to avoid paths like "LvPlan/Month" entering here
path: "/Cis/LvPlan/:lv_id(\\d+)",
name: "LvPlanOld",
component: LvPlan,
redirect(to) {
const route = Vue.unref(router.currentRoute);
const { mode, focus_date } = route.params; // keep mode and focus_date if available
return { // redirect to longer LvPlan url and map params
name: "LvPlan",
params: {
mode,
focus_date,
lv_id: to.params.lv_id
},
};
},
},
{
path: `/Cis/Stundenplan/:mode?/:focus_date?/:lv_id?`,
name: 'Stundenplan',
component: Stundenplan,
props: (route) => { // validate and set mode/focus date if for some reason missing
const validModes = ["Month", "Week", "Day"];
// check mode string
const mode = route.params.mode &&
validModes.includes(route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase())
? route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()
: DEFAULT_MODE_STUNDENPLAN;
// default to today date if not provided or string forms invalid date
const d = new Date(route.params.focus_date)
const focus_date = !isNaN(d) ? route.params.focus_date : new Date().toISOString().split("T")[0];
// for consistency reasons format the props into one object but actually use a new name to we dont collide with
// existing viewData declaration written from codeigniter 3 into routerview tag
path: `/Cis/LvPlan/:mode?/:focus_date?/:lv_id?`,
name: 'LvPlan',
component: LvPlan,
props(route) {
return {
propsViewData: {
mode,
focus_date,
lv_id: route.params.lv_id
}
propsViewData: route.params
};
}
},
{
path: `/Cis/MyLvPlan/:mode?/:focus_date?`,
name: 'MyLvPlan',
component: MyLvPlan,
props(route) {
return {
propsViewData: route.params
};
},
beforeEnter: (to, from, next) => {
// missing mode or focus_date -> set defaults
if (!to.params.mode || !to.params.focus_date) {
next({
name: "Stundenplan",
params: {
mode: to.params.mode || DEFAULT_MODE_STUNDENPLAN,
focus_date: to.params.focus_date || new Date().toISOString().split("T")[0],
lv_id: to.params.lv_id
}
});
} else {
next();
}
}
},
{
@@ -200,7 +220,8 @@ const router = VueRouter.createRouter({
const app = Vue.createApp({
name: 'FhcApp',
data: () => ({
appSideMenuEntries: {}
appSideMenuEntries: {},
renderers: null,
}),
components: {},
computed: {
@@ -210,7 +231,8 @@ const app = Vue.createApp({
},
provide() {
return { // provide injectable & watchable language property
language: Vue.computed(() => this.$p.user_language)
language: Vue.computed(() => this.$p.user_language),
renderers: Vue.computed(() => this.renderers),
}
},
methods: {
@@ -220,7 +242,8 @@ const app = Vue.createApp({
},
handleClick(event) {
const target = event.target.closest('a');
if(target?.id == 'skiplink') return
if (target && this.isInternalRoute(target.href)) {
const url = new URL(target.href)
@@ -247,6 +270,45 @@ const app = Vue.createApp({
}
}
},
async created(){
await this.$api
.call(ApiRenderers.loadRenderers())
.then(res => res.data)
.then(data => {
for (let rendertype of Object.keys(data)) {
let modalTitle = null;
let modalContent = null;
let calendarEvent = null;
if (data[rendertype].modalTitle)
modalTitle = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].modalTitle)));
if (data[rendertype].modalContent)
modalContent = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].modalContent)));
if (data[rendertype].calendarEvent)
calendarEvent = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].calendarEvent)));
if (data[rendertype].calendarEventStyles){
var head = document.head;
if(!head.querySelector(`link[href="${data[rendertype].calendarEventStyles}"]`)){
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = data[rendertype].calendarEventStyles;
head.appendChild(link);
}
}
if(this.renderers === null) {
this.renderers = {};
}
if (!this.renderers[rendertype]) {
this.renderers[rendertype] = {}
}
this.renderers[rendertype].modalTitle = modalTitle;
this.renderers[rendertype].modalContent = modalContent;
this.renderers[rendertype].calendarEvent = calendarEvent;
}
});
},
mounted() {
document.addEventListener('click', this.handleClick);
},
@@ -264,6 +326,8 @@ app.use(primevue.config.default, {
tooltip: 8000
}
})
app.directive('tooltip', primevue.tooltip);
app.use(PluginsPhrasen);
app.use(Theme);
app.directive('contrast', contrast);
app.mount('#fhccontent');
app.mount('#fhccontent');
+2 -1
View File
@@ -1,5 +1,6 @@
import {CoreNavigationCmpt} from '../components/navigation/Navigation.js';
import DashboardAdmin from '../components/Dashboard/Admin.js';
import Phrases from "../plugin/Phrasen.js"
Vue.createApp({
name: 'DashboardAdminApp',
@@ -12,4 +13,4 @@ Vue.createApp({
},
mounted() {
}
}).mount('#main');
}).use(Phrases).mount('#main');
+27
View File
@@ -0,0 +1,27 @@
//TODO Manu
//use this instead of Nachrichten.js
import NewMessage from "../../components/Messages/Details/NewMessage/NewDiv.js";
import Phrasen from "../../plugin/Phrasen.js";
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes: [
{ path: `/${ciPath}/NeueNachricht`, component: NewMessage },
{ path: `/${ciPath}/NeueNachricht/:id`, component: NewMessage },
{ path: `/${ciPath}/NeueNachricht/:id/:typeId`, component: NewMessage },
]
});
const app = Vue.createApp();
app
.use(router)
.use(primevue.config.default, {
zIndex: {
overlay: 1100
}
})
.use(Phrasen)
.mount('#main');
+26
View File
@@ -0,0 +1,26 @@
import NewMessage from "../components/Messages/Details/NewMessage/NewDiv.js";
import Phrasen from "../plugin/Phrasen.js";
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes: [
{ path: `/${ciPath}/NeueNachricht/:id/:typeId`, component: NewMessage },
{ path: `/${ciPath}/NeueNachricht/:id/:typeId/:messageId`, component: NewMessage },
]
});
const app = Vue.createApp();
app
.use(router)
.use(primevue.config.default, {
zIndex: {
overlay: 1100
}
})
.use(Phrasen)
.mount('#main');
+139 -5
View File
@@ -26,14 +26,148 @@ const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes: [
{ path: `/${ciPath}/studentenverwaltung`, component: FhcStudentenverwaltung },
{ path: `/${ciPath}/studentenverwaltung/prestudent/:prestudent_id`, component: FhcStudentenverwaltung },
{ path: `/${ciPath}/studentenverwaltung/prestudent/:prestudent_id/:tab`, component: FhcStudentenverwaltung },
{ path: `/${ciPath}/studentenverwaltung/student/:id`, component: FhcStudentenverwaltung },
{ path: `/${ciPath}/studentenverwaltung/person/:person_id`, component: FhcStudentenverwaltung }
{
name: 'index',
path: `/${ciPath}/studentenverwaltung`,
component: FhcStudentenverwaltung
},
{
name: 'studiensemester',
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz`,
component: FhcStudentenverwaltung,
props: (route) => {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz
};
},
beforeEnter: (to, from, next) => {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
next();
}
},
{
name: 'studiengang',
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/:studiengang`,
component: FhcStudentenverwaltung,
props: (route) => {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
url_studiengang: route.params.studiengang,
};
},
beforeEnter: (to, from, next) => {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
const isStudiengang = /^[A-Z]{3}/.test(to.params.studiengang);
if (!isStudiengang) {
return next({
name: 'studiensemester',
params: {
studiensemester_kurzbz: to.params.studiensemester_kurzbz
}
});
}
next();
}
},
{
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/prestudent/:prestudent_id`,
component: FhcStudentenverwaltung,
props: (route) => {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
url_mode: 'prestudent',
url_prestudent_id: route.params.prestudent_id
};
},
beforeEnter: (to, from, next) => {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
next();
}
},
{
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/prestudent/:prestudent_id/:tab`,
component: FhcStudentenverwaltung,
props: (route) => {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
url_mode: 'prestudent',
url_prestudent_id: route.params.prestudent_id,
url_tab: route.params.tab
};
},
beforeEnter: (to, from, next) => {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
next();
}
},
{
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/student/:id`,
component: FhcStudentenverwaltung,
props: (route) => {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
url_mode: 'student',
url_student_id: route.params.id
};
},
beforeEnter: (to, from, next) => {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
next();
}
},
{
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/person/:person_id`,
component: FhcStudentenverwaltung,
props: (route) => {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
url_mode: 'person',
url_prestudent_id: route.params.person_id
};
},
beforeEnter: (to, from, next) => {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
next();
}
},
{
path: '/:pathMatch(.*)*',
redirect: {
name: 'index'
}
}
]
});
router.afterEach((to, from, failure) => {
let title = 'Studierendenverwaltung FH-Complete';
if (to.params.studiengang) {
title = to.params.studiengang + ' ' + title;
}
if (to.params.studiensemester_kurzbz) {
title = to.params.studiensemester_kurzbz + ' ' + title;
}
document.title = title;
});
const app = Vue.createApp({
name: 'StudentenverwaltungApp'
});
+1 -1
View File
@@ -16,7 +16,7 @@ export default {
:class="this.cModalClass"
:id="this.id"
:aria-labelledby="this.id + '_label'" aria-hidden="true" tabindex="-1">
<div class="modal-dialog" :class="this.cModalDialogClass">
<div class="modal-dialog modal-dialog-scrollable" :class="this.cModalDialogClass">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" :id="this.id + '_label'">
@@ -29,14 +29,24 @@ export default {
uid: {
type: [Number, String],
required: true
}
},
/** List of types to allow for creation */
betriebsmittelTypes: {
type: Array,
default: null
},
/**
* If true: only show the types specified in 'betriebsmittelTypes'.
* If false: show all available types.
*/
filterByProvidedTypes: Boolean
},
data() {
return {
tabulatorOptions: {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(
this.endpoint.getAllBetriebsmittel(this.typeId, this.id)
this.endpoint.getAllBetriebsmittel(this.typeId, this.id, (this.filterByProvidedTypes ? this.betriebsmittelTypes : null))
),
ajaxResponse: (url, params, response) => response.data,
columns: [
@@ -137,8 +147,6 @@ export default {
layout: 'fitColumns',
layoutColumnsOnNewData: false,
height: '550',
selectableRangeMode: 'click',
selectable: true,
persistenceID: 'core-betriebsmittel'
},
tabulatorEvents: [
@@ -296,7 +304,7 @@ export default {
},
created() {
return this.$api
.call(this.endpoint.getTypenBetriebsmittel())
.call(this.endpoint.getTypenBetriebsmittel(this.betriebsmittelTypes))
.then(result => {
this.listBetriebsmitteltyp = result.data;
})
@@ -311,6 +319,7 @@ export default {
table-only
:side-menu="false"
reload
:reload-btn-infotext="this.$p.t('table', 'reload')"
new-btn-show
:new-btn-label="this.$p.t('ui', 'betriebsmittel')"
@click:new="actionNewBetriebsmittel"
@@ -427,6 +436,7 @@ export default {
v-model="formData.ausgegebenam"
auto-apply
:enable-time-picker="false"
text-input
format="dd.MM.yyyy"
preview-format="dd.MM.yyyy"
:teleport="true"
@@ -442,6 +452,7 @@ export default {
v-model="formData.retouram"
auto-apply
:enable-time-picker="false"
text-input
format="dd.MM.yyyy"
preview-format="dd.MM.yyyy"
:teleport="true"
+28 -6
View File
@@ -3,7 +3,8 @@
export default {
name: 'BootstrapModal',
data: () => ({
modal: null
modal: null,
fullscreen: false
}),
props: {
backdrop: {
@@ -23,9 +24,21 @@ export default {
},
noCloseBtn: Boolean,
dialogClass: [String,Array,Object],
headerClass: {
type: [String,Array,Object],
default: ''
},
bodyClass: {
type: [String,Array,Object],
default: 'px-4 py-5'
},
footerClass: {
type: [String,Array,Object],
default: ''
},
allowFullscreenExpand: {
type: Boolean,
default: false
}
},
emits: [
@@ -50,6 +63,9 @@ export default {
},
toggle() {
return this.modal.toggle();
},
toggleFullscreen() {
this.fullscreen = !this.fullscreen
}
},
mounted() {
@@ -115,17 +131,23 @@ export default {
});
},
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')" >
<div class="modal-dialog" :class="dialogClass">
<div class="modal-dialog" :class="fullscreen ? 'modal-fullscreen' : dialogClass">
<div class="modal-content">
<div v-if="$slots.title" class="modal-header">
<div v-if="$slots.title" class="modal-header" :class="headerClass">
<h5 class="modal-title"><slot name="title"/></h5>
<slot name="popoutButton"></slot>
<button v-if="!noCloseBtn" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="d-flex align-items-center ms-auto">
<button type="button" class="btn ms-auto" style="filter: invert(1)" v-if="allowFullscreenExpand" @click="toggleFullscreen">
<i v-if="!fullscreen" class="fa-solid fa-expand"></i>
<i v-else class="fa-solid fa-compress"></i>
</button>
<button v-if="!noCloseBtn" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<slot name="modal-header-content"></slot>
</div>
<div class="modal-body" :class="bodyClass">
<slot></slot>
</div>
<div v-if="$slots.footer" class="modal-footer">
<div v-if="$slots.footer" class="modal-footer" :class="footerClass">
<slot name="footer"/>
</div>
</div>
-17
View File
@@ -1,17 +0,0 @@
import CalendarHeader from './Header.js';
export default {
components: {
CalendarHeader
},
inject: [
'date',
'focusDate',
'size'
],
emits: [
'updateMode',
'change:range',
'input'
]
}
+311
View File
@@ -0,0 +1,311 @@
import BaseDraganddrop from './Base/DragAndDrop.js';
import BaseHeader from './Base/Header.js';
import BaseSlider from './Base/Slider.js';
import BsModal from '../Bootstrap/Modal.js';
import CalClick from '../../directives/Calendar/Click.js';
export default {
name: "CalendarBase",
components: {
BaseDraganddrop,
BaseHeader,
BaseSlider,
BsModal
},
directives: {
CalClick
},
provide() {
return {
locale: Vue.computed(() => this.locale),
timezone: Vue.computed(() => this.timezone),
timeGrid: Vue.computed(() => this.timeGrid),
draggableEvents: Vue.computed(() => {
if (!this.draggableEvents)
return () => false;
if (Array.isArray(this.draggableEvents))
return event => this.draggableEvents.includes(event.type);
if (this.draggableEvents instanceof Function)
return this.draggableEvents;
return () => true;
}),
dropableEvents: Vue.computed(() => {
if (!this.onDrop)
return () => false;
if (Array.isArray(this.dropableEvents))
return item => this.dropableEvents.includes(item.type);
if (this.dropableEvents instanceof Function)
return this.dropableEvents;
return () => true;
}),
hasDragoverFunc: Vue.computed(() => this.onDragover),
mode: Vue.computed(() => this.mode)
};
},
props: {
locale: {
type: String,
default: 'de'
},
timezone: {
type: String,
required: true
},
date: {
type: [Date, String, Number, luxon.DateTime],
default: props => luxon.DateTime.now().setZone(props.timezone).startOf('day')
},
modes: {
type: Object,
required: true,
default: {}
// TODO(chris): verfication functions
},
mode: String,
modeOptions: Object,
events: {
type: Array,
default: []
},
backgrounds: {
type: Array,
default: []
},
showBtns: Boolean,
btnMonth: {
type: Boolean,
default: undefined
},
btnWeek: {
type: Boolean,
default: undefined
},
btnDay: {
type: Boolean,
default: undefined
},
btnList: {
type: Boolean,
default: undefined
},
timeGrid: Array,
draggableEvents: [Boolean, Array, Function],
dropableEvents: [Boolean, Array, Function],
onDragover: Function,
onDrop: Function
},
emits: [
"click:next",
"click:prev",
"click:mode",
"click:event",
"click:day",
"click:week",
"update:date",
"update:mode",
"update:range",
"drop"
],
data() {
return {
internalView: null,
internalDate: null,
modalEvent: null
};
},
computed: {
convertedEvents() {
return this.events.map(orig => ({
id: orig.type + orig[orig.type + '_id'],
type: orig.type,
start: luxon.DateTime.fromISO(orig.isostart).setZone(this.timezone),
end: luxon.DateTime.fromISO(orig.isoend).setZone(this.timezone),
orig
}));
},
convertedBackgrounds() {
return this.backgrounds.map(bg => {
const res = { ...bg };
if (res.start) {
if (Number.isInteger(res.start))
res.start = luxon.DateTime.fromMillis(res.start, { zone: this.timezone, locale: this.locale });
else if (res.start instanceof Date)
res.start = luxon.DateTime.fromJSDate(res.start, { zone: this.timezone, locale: this.locale });
else if (typeof res.start ===
'string' || res.start instanceof String)
res.start = luxon.DateTime.fromISO(res.start, { zone: this.timezone, locale: this.locale });
}
if (res.end) {
if (Number.isInteger(res.end))
res.end = luxon.DateTime.fromMillis(res.end, { zone: this.timezone, locale: this.locale });
else if (res.end instanceof Date)
res.end = luxon.DateTime.fromJSDate(res.end, { zone: this.timezone, locale: this.locale });
else if (typeof res.end ===
'string' || res.end instanceof String)
res.end = luxon.DateTime.fromISO(res.end, { zone: this.timezone, locale: this.locale });
}
return res;
});
},
sDate() {
if (this.date instanceof luxon.DateTime)
return this.date;
return luxon.DateTime.fromJSDate(new Date(this.date)).setZone(this.timezone);
},
cDate: {
get() {
const date = this.internalDate ? this.internalDate : this.sDate;
return date.setLocale(this.locale);
},
set(value) {
this.internalDate = value;
this.$emit('update:date', value, this.cMode);
}
},
sMode() {
// choose default mode
let mode = this.mode;
if (mode)
mode = mode.toLowerCase();
if (!mode || !this.modes[mode])
mode = Object.keys(this.modes).find(Boolean); // start with first entry as active mode
return mode || '';
},
cMode: {
get() {
return this.internalView ? this.internalView : this.sMode;
},
set(value) {
this.internalView = value;
this.$emit('update:mode', value, this.cDate);
}
}
},
watch: {
sDate(n, o) {
if (this.sDate.isValid && !this.sDate.hasSame(this.internalDate, 'day'))
this.internalDate = this.sDate;
},
sMode() {
if (this.sMode)
this.internalView = this.sMode;
}
},
methods: {
clickPrev() {
const evt = new Event('click:prev', {cancelable: true});
this.$emit('click:prev', evt);
if (evt.defaultPrevented)
return;
// default: switch page
this.$refs.mode.prevPage();
},
clickNext() {
const evt = new Event('click:next', {cancelable: true});
this.$emit('click:next', evt);
if (evt.defaultPrevented)
return;
// default: switch page
this.$refs.mode.nextPage();
},
handleClickDefaults(evt) {
// TODO(chris): implement
switch (evt.detail.source) {
case 'day':
if (this.cMode != 'day' && this.modes['day']) {
evt.stopPropagation();
this.cDate = evt.detail.value;
this.cMode = 'day';
}
break;
case 'week':
if (this.cMode != 'week' && this.modes['week']) {
evt.stopPropagation();
this.cDate = luxon.DateTime.fromObject({
localWeekNumber: evt.detail.value.number,
localWeekYear: evt.detail.value.year
}, {
zone: this.cDate.zoneName,
locale: this.cDate.locale
});
this.cMode = 'week';
}
break;
}
},
onDropItem(evt, start, end) {
this.$emit('drop', evt, start, end);
},
showEventModal(eventObj) {
this.modalEvent = eventObj;
this.$refs.modal.show();
},
hideEventModal() {
if (this.modalEvent)
this.modalEvent.closeFn = undefined;
this.$refs.modal.hide();
this.modalEvent = null;
},
onModalHidden() {
if (this.modalEvent.closeFn)
this.modalEvent.closeFn();
}
},
beforeUnmount() {
this.hideEventModal();
},
template: /* html */`
<div class="fhc-calendar-base h-100">
<base-draganddrop
class="card h-100"
:events="convertedEvents"
:backgrounds="convertedBackgrounds"
@drop="onDropItem"
v-cal-click:container
@cal-click-default.capture="handleClickDefaults"
>
<base-header
class="card-header"
v-model:date="cDate"
v-model:mode="cMode"
@prev="clickPrev"
@next="clickNext"
@click:mode="$emit('click:mode', $event)"
:btn-day="!!modes['day'] && (btnDay || (showBtns && btnDay !== false))"
:btn-week="!!modes['week'] && (btnWeek || (showBtns && btnWeek !== false))"
:btn-month="!!modes['month'] && (btnMonth || (showBtns && btnMonth !== false))"
:btn-list="!!modes['list'] && (btnList || (showBtns && btnList !== false))"
:mode-options="modeOptions ? modeOptions[cMode] : undefined"
>
<slot name="actions" />
</base-header>
<component
:is="modes ? modes[cMode] : null || 'div'"
ref="mode"
v-model:current-date="cDate"
@update:range="$emit('update:range', $event)"
@request-modal-open="showEventModal"
@request-modal-close="hideEventModal"
v-bind="modeOptions ? modeOptions[cMode] : null || {}"
>
<template v-slot="slot"><slot v-bind="slot" /></template>
</component>
</base-draganddrop>
<bs-modal ref="modal" dialog-class="modal-lg" body-class="" @hidden-bs-modal="onModalHidden">
<template #title>
<slot v-if="modalEvent" v-bind="{mode: 'eventheader', event: modalEvent.event}" />
</template>
<template #default>
<slot v-if="modalEvent" v-bind="{mode: 'event', event: modalEvent.event}" />
</template>
</bs-modal>
</div>
`
}
@@ -0,0 +1,162 @@
import DragAndDrop from '../../../helpers/DragAndDrop.js';
import CalDnd from '../../../directives/Calendar/DragAndDrop.js';
/**
* TODO(chris): this needs serious rework!
*/
export default {
name: "CalendarDragAndDrop",
directives: {
CalDnd
},
provide() {
return {
events: Vue.computed(() => this.correctedEvents),
backgrounds: Vue.computed(() => this.backgrounds),
dropAllowed: Vue.computed(() => this.dragging && this.dropAllowed)
};
},
inject: {
mode: "mode",
dropableEvents: "dropableEvents"
},
props: {
events: Array,
backgrounds: Array
},
emits: [
"drop"
],
data() {
return {
dragging: false,
allowed: false,
draggedInternalEvent: null,
draggedExternalEvent: null,
targetTimestamp: 0,
targetGridEnds: null,
dropAllowed: false,
shadowPreview: false // TODO(chris): IMPLEMENT! (use background instead of event as preview)
};
},
computed: {
correctedEvents() {
if (this.dragging) {
if (this.draggedInternalEvent) {
const index = this.events.findIndex(e => e.id == this.draggedInternalEvent.id);
if (this.previewEvent && !this.shadowPreview)
return this.events.toSpliced(index, 1, this.previewEvent);
else
return this.events.toSpliced(index, 1);
}
if (this.previewEvent && !this.shadowPreview)
return [...this.events, this.previewEvent];
}
return this.events;
},
correctedBackgrounds() {
if (this.dragging) {
if (this.shadowPreview) {
// TODO(chris): how to get the length
return [...this.backgrounds, {
start: new Date(this.targetTimestamp),
class: 'shadow-preview'
}];
}
}
return this.backgrounds;
},
previewEvent() {
if (!this.dragging || !this.dropAllowed)
return null;
if (!this.targetTimestamp)
return null;
const event = this.draggedInternalEvent || this.draggedExternalEvent;
if (!event)
return null;
// TODO(chris): calculate length correctly from orig
let length = event.end - event.start;
if (this.targetGridEnds)
length = this.targetGridEnds.find(end => end >= this.targetTimestamp + length) - this.targetTimestamp;
return {
orig: event.orig,
start: this.targetTimestamp,
end: this.targetTimestamp + length
};
}
},
methods: {
onDragstart(evt) {
DragAndDrop.setTransferData(evt.detail.originalEvent, evt.detail.item.orig);
this.draggedInternalEvent = evt.detail.item;
},
onDragend() {
this.draggedInternalEvent = null;
this.dragging = false;
},
onDragenter(evt) {
this.dragging = true;
if (!this.draggedInternalEvent) {
const event = DragAndDrop.getValidTransferData(evt.detail.originalEvent);
if (event) {
this.draggedExternalEvent = {
id: event.id,
type: event.type,
start: event.isostart
? luxon.DateTime.fromISO(event.isostart).setZone(this.timezone)
: luxon.DateTime.local().setZone(this.timezone),
end: event.isoend
? luxon.DateTime.fromISO(event.isoend).setZone(this.timezone)
: luxon.DateTime.local().setZone(this.timezone),
orig: event
};
} else {
this.draggedExternalEvent = null;
}
this.dropAllowed = this.dropableEvents(event, this.mode);
} else {
this.dropAllowed = this.dropableEvents(this.draggedInternalEvent, this.mode);
}
},
onDragleave() {
this.dragging = false;
},
onDragchange(evt) {
this.targetTimestamp = evt.detail.timestamp;
this.targetGridEnds = evt.detail.ends || null;
},
onDrop(evt) {
if (!this.dragging || !this.dropAllowed)
return;
this.$emit('drop', evt, this.previewEvent.start, this.previewEvent.end);
this.dropAllowed = false;
this.dragging = false;
}
},
template: `
<div
class="fhc-calendar-base-draganddrop"
@calendar-dragstart="onDragstart"
@calendar-dragend="onDragend"
v-cal-dnd:dropcage
@calendar-dragenter="onDragenter"
@calendar-dragleave="onDragleave"
@calendar-dragchange="onDragchange"
@drop="onDrop"
>
<slot />
</div>
`
}
+435
View File
@@ -0,0 +1,435 @@
import GridLine from './Grid/Line.js';
import GridLineEvent from './Grid/Line/Event.js';
import CalDnd from '../../../directives/Calendar/DragAndDrop.js';
export default {
name: "CalendarGrid",
components: {
GridLine,
GridLineEvent
},
directives: {
CalDnd
},
inject: {
originalEvents: "events",
originalBackgrounds: "backgrounds",
dropAllowed: "dropAllowed"
},
provide() {
return {
flipAxis: Vue.computed(() => this.flipAxis),
axisRow: Vue.computed(() => this.axisRow)
};
},
props: {
axisMain: {
type: Array,
required: true,
validator(value) {
return value.every(item => item instanceof luxon.DateTime);
}
},
axisParts: {
type: Array,
required: true,
validator(value) {
return value.every(item =>
item instanceof luxon.Duration
|| Number.isInteger(item)
|| (
(
item.start instanceof luxon.Duration
|| Number.isInteger(item.start)
) && (
item.end instanceof luxon.Duration
|| Number.isInteger(item.end)
)
)
);
}
},
flipAxis: Boolean,
allDayEvents: Boolean,
axisMainCollapsible: Boolean,
snapToGrid: Boolean
},
data() {
return {
dragging: false,
resizeObserver: null,
mutationObserver: null,
userScroll: true
};
},
computed: {
axisRow() {
return this.flipAxis ? 'column' : 'row';
},
axisCol() {
return this.flipAxis ? 'row' : 'column';
},
axisPartsWithBreaks() {
return this.axisParts.reduce((res, tu, index) => {
const start = tu.start || tu;
const end = tu.end;
if (res.length) {
const lastTuEnd = res.pop();
if (Array.isArray(lastTuEnd)) {
res.push({
start: lastTuEnd[0],
end: start,
index: lastTuEnd[1]
});
} else if (lastTuEnd != start) {
// add pause
res.push({
start: lastTuEnd,
end: start
});
}
}
if (!end) {
res.push([start, index]);
} else {
res.push({
start,
end,
index
});
res.push(end);
}
return res;
}, []).slice(0, -1);
},
axisPartsSave() {
if (!this.axisParts[this.axisParts.length - 1].end)
return this.axisParts.slice(0, -1);
return this.axisParts;
},
start() {
return this.axisPartsWithBreaks[0].start;
},
end() {
return this.axisPartsWithBreaks[this.axisPartsWithBreaks.length - 1].end;
},
ends() {
const ends = [];
const partsEnds = this.axisPartsWithBreaks
.filter(p => p.index !== undefined)
.map(p => p.end);
for (var date of this.axisMain)
for (var part of partsEnds)
ends.push(date.plus(part));
return ends;
},
axisMainBorders() {
return this.axisMain.reduce(
(res, curr) => res.concat([curr.plus(this.start), curr.plus(this.end)]),
[]
);
},
eventsAllDay() {
if (!this.allDayEvents)
return [];
return this.mapIntoMainAxis(this.originalEvents.filter(event => event.orig.allDayEvent));
},
eventsNormal() {
if (!this.allDayEvents)
return this.events;
return this.mapIntoMainAxis(this.originalEvents.filter(event => !event.orig.allDayEvent));
},
events() {
return this.mapIntoMainAxis(this.originalEvents);
},
backgrounds() {
return this.mapIntoMainAxis(this.originalBackgrounds);
},
hasValidEvents() {
return this.events.find(e => e.length);
},
styleGridCols() {
let cols = 'repeat(' + this.axisMain.length + ', 1fr)';
if (this.axisMainCollapsible) {
if (this.hasValidEvents)
cols = this.events
.map(e => e.length
? '1fr'
: 'var(--fhc-calendar-axis-collapsible, .5fr)')
.join(' ');
}
return cols;
},
styleGridRows() {
const gridlines = {};
this.axisPartsWithBreaks.forEach(part => {
let ts = part.start.toMillis();
if (!gridlines[ts])
gridlines[ts] = ['t_' + ts];
if (part.index !== undefined)
gridlines[ts].push('ps_' + part.index);
ts = part.end.toMillis();
if (!gridlines[ts])
gridlines[ts] = ['t_' + ts];
if (part.index !== undefined)
gridlines[ts].push('pe_' + part.index);
});
this.eventsNormal.forEach((events, mainIndex) => {
let day = this.axisMain[mainIndex];
events.forEach(event => {
if (!event.startsHere && !event.endsHere)
return;
if (event.startsHere) {
let ts = event.start.diff(day).toMillis();
if (!gridlines[ts])
gridlines[ts] = ['t_' + ts, 'e_' + ts];
}
if (event.endsHere) {
let ts = event.end.diff(day).toMillis();
if (!gridlines[ts])
gridlines[ts] = ['t_' + ts, 'e_' + ts];
}
});
});
return Object.keys(gridlines).sort((a,b) => parseInt(a)-parseInt(b)).map((start, i, keys) => {
let end = keys[i + 1];
if (!end) {
gridlines[start].push('end');
return '[' + gridlines[start].join(' ') + ']';
}
return '[' + gridlines[start].join(' ') + '] ' + (end - start) + 'fr';
}).join(' ');
}
},
methods: {
mapIntoMainAxis(target) {
const result = Array.from({length: this.axisMain.length}, () => Array());
target.forEach(event => {
const start = event.start || this.axisMainBorders[0].plus(-1);
const end = event.end || this.axisMainBorders[this.axisMainBorders.length - 1].plus(1);
for (var i = 0; i < this.axisMain.length; i++) {
let laneStart = this.axisMainBorders[i * 2];
let laneEnd = this.axisMainBorders[i * 2 + 1];
if (event.orig?.allDayEvent) {
laneStart = laneStart.startOf('day');
laneEnd = laneEnd.endOf('day');
}
if (start < laneEnd && end > laneStart) {
const startsHere = start >= laneStart;
const endsHere = end <= laneEnd;
result[i].push({
...event,
startsHere,
endsHere
});
}
}
});
return result;
},
/* DRAG AND DROP */
getPageTop(el) {
let pageTop = el.offsetTop;
if (el.offsetParent)
pageTop += this.getPageTop(el.offsetParent);
return pageTop;
},
getPageLeft(el) {
let pageLeft = el.offsetLeft;
if (el.offsetParent)
pageLeft += this.getPageLeft(el.offsetParent);
return pageLeft;
},
getTimestampFromMouse(evt, dayTimestamp) {
let mouse, mouseFrac;
if (this.flipAxis) {
mouse = evt.pageX - this.getPageLeft(this.$refs.body) + this.$refs.main.scrollLeft;
mouseFrac = mouse / this.$refs.body.offsetWidth;
} else {
mouse = evt.pageY - this.getPageTop(this.$refs.body) + this.$refs.main.scrollTop;
mouseFrac = mouse / this.$refs.body.offsetHeight;
}
return dayTimestamp + this.start + Math.floor((this.end - this.start) * mouseFrac);
},
/* SCROLLING */
enableAutoScroll() {
if (!this.resizeObserver)
this.resizeObserver = new ResizeObserver(this.scrollToEarliestEvent);
this.resizeObserver.observe(this.$refs.body);
if (!this.mutationObserver)
this.mutationObserver = new MutationObserver(mutations => {
if (mutations.some(m => m.addedNodes.length && [].some.call(m.addedNodes, el => el.matches && el.matches('.fhc-calendar-base-grid-line-event'))))
this.scrollToEarliestEvent();
});
this.mutationObserver.observe(this.$refs.body, {
subtree: true,
childList: true
});
this.scrollToEarliestEvent();
},
disableAutoScroll() {
if (this.resizeObserver)
this.resizeObserver.disconnect();
this.resizeObserver = null;
if (this.mutationObserver)
this.mutationObserver.disconnect();
this.mutationObserver = null;
},
scrollToEarliestEvent() {
const eventElements = this.$refs.scroller.querySelectorAll('.fhc-calendar-base-grid-line-event');
let earliestEventOffset = [0, null];
for (var el of eventElements.values()) {
const top = el.offsetTop;
if (!earliestEventOffset[1] || top < earliestEventOffset[0])
earliestEventOffset = [top, el];
}
this.userScroll = false;
if (earliestEventOffset[1]) {
earliestEventOffset[1].scrollIntoView({ behavior: "smooth" });
} else {
this.$refs.scroller.scrollTo(0, 0);
}
}
},
beforeUnmount() {
this.disableAutoScroll();
},
template: /* html */`
<div
class="fhc-calendar-base-grid"
style="display:grid;width:100%;height:100%"
:style="'grid-template-' + axisRow + 's:auto' + (allDayEvents ? ' auto ' : ' ') + '1fr;grid-template-' + axisCol + 's:auto ' + styleGridCols"
>
<div
class="grid-header"
style="display:grid"
:style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'"
>
<div
v-for="(date, index) in axisMain"
:key="index"
class="main-header"
:class="{'collapsed-header': axisMainCollapsible && hasValidEvents && !events[index].length}"
:style="'grid-' + axisCol + ':' + (2+index)"
>
<slot name="main-header" v-bind="{ index, date }" />
</div>
</div>
<div
v-if="allDayEvents"
class="grid-allday"
style="display:grid"
:style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'"
>
<div
v-for="(events, index) in eventsAllDay"
:key="index"
class="all-day-events"
:style="'grid-' + axisCol + ':' + (2+index)"
>
<grid-line-event
v-for="(event, i) in events"
:key="i"
:event="event"
>
<template v-slot="slot">
<slot name="event" v-bind="slot" />
</template>
</grid-line-event>
</div>
</div>
<div
ref="scroller"
@scrollend="userScroll ? disableAutoScroll() : userScroll = true"
style="display:grid;overflow:auto"
:style="'grid-' + axisCol + ':1/-1;grid-template-' + axisCol + 's:subgrid'"
>
<div
ref="main"
class="grid-main"
style="position:relative;grid-column:1/-1;grid-row:1/-1;display:grid"
:style="'grid-template-' + axisCol + 's:subgrid;grid-template-' + axisRow + 's:' + styleGridRows"
>
<div
v-for="(part, index) in axisPartsSave"
:key="index"
class="part-header"
:style="'grid-' + axisCol + ':1;grid-' + axisRow + ': ps_' + index + '/pe_' + index"
>
<slot name="part-header" v-bind="{ index, part }" />
</div>
<div
ref="body"
class="grid-body"
style="display:grid;grid-template-rows:subgrid;grid-template-columns:subgrid"
:style="'grid-' + axisCol + ':2/-1;grid-' + axisRow + ':1/-1'"
v-cal-dnd:dropcage
@calendar-dragenter="dragging = true"
@calendar-dragleave="dragging = false"
@dragover="dropAllowed ? $event.preventDefault() : null"
>
<template
v-for="(date, index) in axisMain"
:key="index"
>
<div
v-for="(part, i) in axisPartsSave"
:key="i"
class="part-body"
style="position:relative"
:style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i"
>
<slot name="part-body" v-bind="{ index, part }" />
<div
v-if="snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1"
v-cal-dnd:dropzone.once="{date: date.plus(part.start || part), ends: ends.slice(ends.findIndex(end => end > date))}"
></div>
</div>
<grid-line
:start="date.plus(start)"
:end="date.plus(end)"
:date="date"
:events="eventsNormal[index]"
:backgrounds="backgrounds[index]"
style="position:relative"
:style="'grid-' + axisRow + ':1/-1;grid-' + axisCol + ':' + (1+index)"
>
<template #event="slot">
<slot name="event" v-bind="slot" />
</template>
<template #dropzone>
<div
v-if="!snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1"
v-cal-dnd:dropzone="evt => getTimestampFromMouse(evt, date)"
></div>
</template>
</grid-line>
</template>
</div>
</div>
</div>
</div>
`
}
@@ -0,0 +1,85 @@
import LineEvent from './Line/Event.js';
import LineBackground from './Line/Background.js';
/**
* TODO(chris):
* Event overflow for Month mode (more-button)
*/
export default {
name: "GridLine",
components: {
LineEvent,
LineBackground
},
inject: {
axisRow: "axisRow"
},
props: {
date: {
type: luxon.DateTime,
required: true
},
start: {
type: luxon.DateTime,
required: true
},
end: {
type: luxon.DateTime,
required: true
},
events: {
type: Array,
default: []
},
backgrounds: {
type: Array,
default: []
}
},
computed: {
eventsWithRowInfo() {
const events = [];
this.events.forEach(event => {
const rows = [1, -1];
if (event.startsHere) {
rows[0] = 't_' + event.start.diff(this.date).toMillis();
}
if (event.endsHere) {
rows[1] = 't_' + event.end.diff(this.date).toMillis();
}
events.push({
...event,
rows
});
});
return events;
}
},
template: /* html */`
<div
class="fhc-calendar-base-grid-line"
style="position:relative;display:grid;grid-auto-flow:dense"
:style="'grid-template-' + axisRow + 's:subgrid'"
>
<line-background
v-for="bg in backgrounds"
:start="start"
:end="end"
:background="bg"
></line-background>
<line-event
v-for="(event, i) in eventsWithRowInfo"
:key="i"
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
:event="event"
>
<template v-slot="slot">
<slot name="event" v-bind="slot" />
</template>
</line-event>
<slot name="dropzone" />
</div>
`
}
@@ -0,0 +1,72 @@
export default {
name: "GridLineBackground",
inject: {
flipAxis: "flipAxis"
},
props: {
start: {
type: luxon.DateTime,
required: true
},
end: {
type: luxon.DateTime,
required: true
},
background: {
type: Object,
required: true,
validator(value) {
if (!value.start && !value.end)
return false;
if (value.start && !(value.start instanceof luxon.DateTime))
return false;
if (value.end && !(value.end instanceof luxon.DateTime))
return false;
return true;
}
}
},
computed: {
styles() {
if (!this.background.endsHere && !this.background.startsHere)
return this.background.style;
const perc = (this.end.ts - this.start.ts) / 100;
let border = {};
if (this.background.startsHere)
border[this.flipAxis ? 'left' : 'top'] = (this.background.start.diff(this.start)) / perc + '%';
if (this.background.endsHere)
border[this.flipAxis ? 'right' : 'bottom'] = (this.end.diff(this.background.end)) / perc + '%';
if (!this.background.style)
return border;
return [this.background.style, border];
},
classes() {
if (!this.background.endsHere && !this.background.startsHere)
return this.background.class;
const result = [];
if (this.background.class)
result.push(this.background.class);
if (this.background.startsHere)
result.push('bg-begin');
if (this.background.endsHere)
result.push('bg-end');
return result;
}
},
template: /* html */`
<div
class="fhc-calendar-base-grid-line-background"
:class="classes"
style="position:absolute;inset:0;z-index:0"
:style="styles"
:title="background.title"
>
<span v-if="background.label">{{ background.label }}</span>
</div>
`
}
@@ -0,0 +1,57 @@
import CalDnd from '../../../../../directives/Calendar/DragAndDrop.js';
import CalClick from '../../../../../directives/Calendar/Click.js';
export default {
name: "GridLineEvent",
directives: {
CalDnd,
CalClick
},
inject: {
draggableEvents: "draggableEvents",
mode: "mode"
},
props: {
event: {
type: Object,
required: true,
validator(value) {
return (value.start && value.end && value.orig);
}
}
},
computed: {
isHeaderOrFooter() {
return ['header', 'footer'].includes(this.event.orig);
},
draggable() {
return !this.isHeaderOrFooter && this.draggableEvents(this.event.orig, this.mode);
},
classes() {
const classes = [];
if (this.isHeaderOrFooter) {
classes.push('event-' + this.event.orig);
} else {
if (this.event.startsHere)
classes.push('event-begin');
if (this.event.endsHere)
classes.push('event-end');
}
return classes
}
},
template: /* html */`
<div
class="fhc-calendar-base-grid-line-event event"
:class="classes"
style="z-index: 1"
:draggable="draggable"
v-cal-dnd:draggable="event"
v-cal-click:event="isHeaderOrFooter ? event : event.orig"
>
<slot :event="isHeaderOrFooter ? event : event.orig">
{{ event.orig }}
</slot>
</div>
`
}
@@ -0,0 +1,123 @@
/**
* TODO(chris): use click-directive
*/
import DatePicker from './Header/Datepicker.js';
export default {
name: "CalendarHeader",
components: {
DatePicker
},
props: {
date: {
type: luxon.DateTime,
required: true
},
mode: {
type: String,
required: true
},
modeOptions: {
type: Object,
default: {}
},
btnMonth: Boolean,
btnWeek: Boolean,
btnDay: Boolean,
btnList: Boolean
},
emits: [
"next",
"prev",
"click:mode",
"update:date",
"update:mode"
],
data() {
return {
open: false
};
},
methods: {
clickMode(evt, mode) {
this.$emit('click:mode', evt);
if (!evt.defaultPrevented)
this.$emit('update:mode', mode);
}
},
template: /* html */`
<div class="fhc-calendar-base-header">
<div class="header-actions">
<div class="header-userdefined">
<slot />
</div>
<div class="header-modes">
<div class="d-flex gap-1 justify-content-end" role="group">
<button
v-if="btnMonth"
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'month'}"
@click="clickMode($event, 'month')"
>
<i class="fa fa-calendar-days"></i>
</button>
<button
v-if="btnWeek"
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'week'}"
@click="clickMode($event, 'week')"
>
<i class="fa fa-calendar-week"></i>
</button>
<button
v-if="btnDay"
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'day'}"
@click="clickMode($event, 'day')"
>
<i class="fa fa-calendar-day"></i>
</button>
<button
v-if="btnList"
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'list'}"
@click="clickMode($event, 'list')"
>
<i class="fa fa-table-list"></i>
</button>
</div>
</div>
</div>
<div class="header-picker">
<div class="btn-group" role="group">
<button
class="btn btn-outline-secondary border-0"
@click="$emit('prev')"
:disabled="open"
>
<i class="fa fa-chevron-left"></i>
</button>
<date-picker
:mode="mode"
:date="date"
@update:date="$emit('update:date', $event)"
@open="open = true"
@closed="open = false"
:list-length="modeOptions.length"
/>
<button
class="btn btn-outline-secondary border-0"
@click="$emit('next')"
:disabled="open"
>
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
</div>
`
}
@@ -0,0 +1,116 @@
// TODO(chris): translate aria-labels
export default {
name: "CalendarHeaderDatepicker",
components: {
VueDatePicker
},
inject: [
"locale",
"timezone"
],
props: {
date: {
type: luxon.DateTime,
required: true
},
mode: {
type: String,
required: true
},
listLength: {
type: Number,
default: 7
}
},
emits: [
"update:date"
],
computed: {
convertedDate() {
// convert to target TZ then strip TZ Information
// so the datepicker can work with local times
return this.date.setZone(this.timezone).setZone('local', { keepLocalTime: true });
},
current() {
switch (this.mode) {
case "month":
return {month: this.convertedDate.month-1, year: this.convertedDate.year};
case "list":
return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.listLength }).ts - 1];
case "week":
return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts];
case "day":
return this.convertedDate;
default:
return null;
}
},
title() {
switch (this.mode) {
case "month":
return this.date.toLocaleString({ month: 'long', year: 'numeric' });
case "week":
var year = this.date.localWeekYear;
var week = this.date.toFormat('nn');
return this.$p.t('calendar/year_kw', { year, week });
case "list":
return this.date.toLocaleString(luxon.DateTime.DATE_FULL) + '-' + this.date.plus({ days: this.listLength - 1 }).toLocaleString(luxon.DateTime.DATE_FULL);
case "day":
return this.date.toLocaleString(luxon.DateTime.DATE_FULL);
default:
return 'View not Supported';
}
},
weekStart() {
return luxon.Info.getStartOfWeek(this.date)%7;
}
},
methods: {
update(value) {
let date;
switch (this.mode) {
case "month":
value.month++;
date = luxon.DateTime.fromObject(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break;
case "list":
case "week":
date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break;
case "day":
date = luxon.DateTime.fromJSDate(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break;
default:
return; // Don't update if the value is invalid!
}
this.$emit("update:date", date);
},
weekNumbers(date) {
return luxon.DateTime.fromJSDate(date, { locale: this.locale }).localWeekNumber;
}
},
template: /* html */`
<vue-date-picker
:model-value="current"
@update:model-value="update"
:format="() => title"
:month-picker="mode == 'month'"
:week-picker="mode == 'week'"
:range="mode == 'list' ? { autoRange: listLength - 1 } : false"
:text-input="mode == 'day'"
:week-start="weekStart"
:week-numbers="{ type: weekNumbers }"
:clearable="false"
:enable-time-picker="false"
:config="{ keepActionRow: mode != 'month' }"
:action-row="{ showSelect: false, showCancel: false, showNow: mode != 'month', showPreview: false }"
auto-apply
six-weeks
teleport
:locale="locale"
:now-button-label="$p.t('calendar/today')"
:week-num-name="$p.t('calendar/kw')"
/>
`
}
@@ -0,0 +1,39 @@
import CalClick from '../../../../directives/Calendar/Click.js';
export default {
name: "LabelDay",
directives: {
CalClick
},
props: {
date: {
type: luxon.DateTime,
required: true
}
},
computed: {
titleFull() {
return this.date.toLocaleString({day: 'numeric', month: 'long', year: 'numeric'});
},
titleLong() {
return this.date.toLocaleString({day: '2-digit', month: '2-digit', year: 'numeric'});
},
titleShort() {
return this.date.toLocaleString({day: 'numeric', month: 'numeric'});
},
titleNarrow() {
return this.date.toLocaleString({day: 'numeric'});
}
},
template: /* html */`
<div
class="fhc-calendar-base-label-day"
v-cal-click:day="date"
>
<span class="full">{{ titleFull }}</span>
<span class="long">{{ titleLong }}</span>
<span class="short">{{ titleShort }}</span>
<span class="narrow">{{ titleNarrow }}</span>
</div>
`
}
@@ -0,0 +1,35 @@
import CalClick from '../../../../directives/Calendar/Click.js';
export default {
name: "LabelDow",
directives: {
CalClick
},
props: {
date: {
type: luxon.DateTime,
required: true
}
},
computed: {
titleLong() {
return this.date.toLocaleString({weekday: 'long'});
},
titleShort() {
return this.date.toLocaleString({weekday: 'short'});
},
titleNarrow() {
return this.date.toLocaleString({weekday: 'narrow'});
}
},
template: /* html */`
<div
class="fhc-calendar-base-label-dow"
v-cal-click:dow="date"
>
<b class="long">{{ titleLong }}</b>
<b class="short">{{ titleShort }}</b>
<b class="narrow">{{ titleNarrow }}</b>
</div>
`
}
@@ -0,0 +1,58 @@
export default {
name: "LabelTime",
props: {
part: {
type: [luxon.Duration, Number, Object],
required: true,
validator(value) {
if (value instanceof Object) {
if (value instanceof luxon.Duration)
return true;
let start_ok = true;
let end_ok = true;
if (value.start) {
start_ok = (
value.start instanceof luxon.Duration
|| Number.isInteger(value.start)
);
}
if (value.end) {
end_ok = (
value.end instanceof luxon.Duration
|| Number.isInteger(value.end)
);
}
return start_ok && end_ok;
}
return true;
}
}
},
computed: {
sanitizedTimestamps() {
return this.part.start || this.part.end ? this.part : { start: this.part };
},
start() {
if (!this.sanitizedTimestamps.start)
return null;
return this.formatTime(this.sanitizedTimestamps.start);
},
end() {
if (!this.sanitizedTimestamps.end)
return null;
return this.formatTime(this.sanitizedTimestamps.end);
}
},
methods: {
formatTime(date) {
return date.toISOTime({ suppressSeconds: true });
}
},
template: `
<div class="fhc-calendar-base-label-time">
<span v-if="start">{{ start }}</span>
<span v-if="end">-</span>
<span v-if="end">{{ end }}</span>
</div>
`
}
@@ -0,0 +1,38 @@
import CalClick from '../../../../directives/Calendar/Click.js';
export default {
name: "LabelWeek",
directives: {
CalClick
},
props: {
date: {
type: luxon.DateTime,
required: true
}
},
computed: {
weeks() {
const firstDay = this.date.startOf('week', { useLocaleWeeks: true });
const lastDay = this.date.endOf('week', { useLocaleWeeks: true });
const weeks = [
{ number: firstDay.localWeekNumber, year: firstDay.localWeekYear },
{ number: lastDay.localWeekNumber, year: lastDay.localWeekYear }
];
if (weeks[0].number == weeks[1].number)
weeks.pop();
return weeks;
}
},
template: `
<div class="fhc-calendar-base-label-week">
<span
v-for="week in weeks"
v-cal-click:week="week"
>
{{ week.number }}
</span>
</div>
`
}
@@ -0,0 +1,129 @@
export default {
name: 'CalendarSlider',
inject: {
time: {
from: "sliderTime",
default: ".3s"
}
},
emits: [
'slid'
],
data() {
return {
target: 0,
extrasAfter: 0,
extrasBefore: 0,
running: false,
promiseResolve: null
}
},
computed: {
itemsAfter() {
return [...Array(this.extrasAfter)].map((i, k) => 1+k);
},
itemsBefore() {
return [...Array(this.extrasBefore)].map((i, k) => k-this.extrasBefore);
},
styleSlider() {
const style = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%'
};
if (this.running) {
style.left = (-this.target * 100) + '%';
style.transition = 'left ' + this.time + ' ease-in-out';
}
return style;
},
styleBefore() {
return {
position: 'absolute',
top: 0,
height: '100%',
display: 'flex',
right: '100%',
width: (this.extrasBefore * 100) + '%'
};
},
styleAfter() {
return {
position: 'absolute',
top: 0,
height: '100%',
display: 'flex',
left: '100%',
width: (this.extrasAfter * 100) + '%'
};
}
},
methods: {
prevPage() {
return this.slidePages(-1);
},
nextPage() {
return this.slidePages(1);
},
slidePages(dir) {
return new Promise(resolve => {
this.promiseResolve = resolve;
this.running = true;
const newTarget = this.target + dir;
if (newTarget > 0) {
if (this.extrasAfter < newTarget)
this.extrasAfter = newTarget;
} else if (newTarget < 0) {
if (-this.extrasBefore > newTarget)
this.extrasBefore = -newTarget;
}
this.target = newTarget;
});
},
endSlide() {
if (this.promiseResolve) {
this.promiseResolve(this.target);
this.promiseResolve = null;
}
this.$emit('slid', this.target);
this.running = false;
this.target = 0;
this.extrasAfter = this.extrasBefore = 0;
}
},
template: /* html */`
<div
class="fhc-calendar-base-slider h-100"
style="position:relative;overflow:hidden"
>
<div
:style="styleSlider"
@transitionend="endSlide"
>
<div :style="styleBefore">
<div
v-for="i in itemsBefore"
:key="i"
style="height:100%;width:100%"
>
<slot :offset="i" />
</div>
</div>
<div :style="styleAfter">
<div
v-for="i in itemsAfter"
:key="i"
style="height:100%;width:100%"
>
<slot :offset="i" />
</div>
</div>
<div style="height:100%;width:100%">
<slot :offset="0" />
</div>
</div>
</div>
`
}
-269
View File
@@ -1,269 +0,0 @@
import CalendarMonth from './Month.js';
import CalendarMonths from './Months.js';
import CalendarYears from './Years.js';
import CalendarWeek from './Week.js';
import CalendarWeeks from './Weeks.js';
import CalendarDay from './Day.js';
import CalendarMinimized from './Minimized.js';
import CalendarDate from '../../composables/CalendarDate.js';
import CalendarDates from '../../composables/CalendarDates.js';
const todayDate = new Date(new Date().setHours(0, 0, 0, 0));
const today = todayDate.getTime()
export default {
components: {
CalendarMonth,
CalendarMonths,
CalendarYears,
CalendarWeek,
CalendarWeeks,
CalendarDay,
CalendarMinimized,
},
provide() {
return {
today,
todayDate,
date: this.date,
focusDate: this.focusDate,
size: Vue.computed({ get: () => this.size }),
containerHeight: Vue.computed({ get: () => this.containerHeight }),
containerWidth: Vue.computed({ get: () => this.containerWidth }),
events: Vue.computed(() => this.eventsPerDay),
filteredEvents: Vue.computed(() => this.filteredEvents),
minimized: Vue.computed({ get: () => this.minimized, set: v => this.$emit('update:minimized', v) }),
showWeeks: this.showWeeks,
noMonthView: this.noMonthView,
noWeekView: this.noWeekView,
eventsAreNull: Vue.computed(() => this.events === null),
mode: Vue.computed(()=>this.mode),
selectedEvent: Vue.computed(() => this.selectedEvent),
setSelectedEvent: (event)=>{this.selectedEvent = event;},
};
},
props: {
events: Array,
initialDate: {
type: [Date, String],
default: new Date()
},
showWeeks: {
type: Boolean,
default: true
},
initialMode: {
type: String,
default: 'month'
},
minimized: Boolean,
noWeekView: Boolean,
noMonthView: Boolean
},
watch:{
mode(newVal) {
this.$emit('change:mode', newVal)
},
selectedEvent:{
handler(newSelectedEvent) {
this.$emit('selectedEvent', newSelectedEvent);
},
immediate: true,
},
// scroll to the first event if the html element was found
scrollTime({focusDate,scrollTime}){
// return early if the scrollTime is not set
if(!scrollTime) return;
// scroll the Stundenplan to the closest event
Vue.nextTick(()=>{
let previousScrollAnchor = document.getElementById('scroll' + (scrollTime - 1) + this.focusDate.d + this.focusDate.w)
let scrollAnchor = document.getElementById('scroll' + scrollTime + this.focusDate.d + this.focusDate.w);
if (previousScrollAnchor) {
previousScrollAnchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
else {
if (scrollAnchor) {
scrollAnchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
});
},
},
emits: [
'select:day',
'select:event',
'change:range',
'update:minimized',
'selectedEvent',
'change:offset'
],
data() {
return {
header: '',
prevMode: null,
currMode: null,
date: new CalendarDate(),
focusDate: new CalendarDate(),
size: 0,
containerWidth: 0,
containerHeight: 0,
selectedEvent:null,
}
},
computed: {
sizeClass() {
// mainly determines calendar font-size
return 'fhc-calendar-' + ['xs', 'sm', 'md', 'lg'][this.size];
},
mode: {
get() { return this.minimized ? 'minimized' : this.currMode; },
set(v) {
if (!v && this.prevMode) {
this.currMode = this.prevMode;
this.prevMode = null;
} else {
this.prevMode = this.currMode;
this.currMode = v;
}
}
},
eventsPerDay() {
if (!this.events)
return {};
return this.events.reduce((result, event) => {
let days = Math.ceil((event.end - event.start) / 86400000) || 1;
while (days-- > 0) {
let day = (new Date(event.start.getFullYear(), event.start.getMonth(), event.start.getDate() + days)).toDateString();
if (!result[day])
result[day] = [];
result[day].push(event);
}
return result;
}, {});
},
// returns the hour of the earliest event, used to scroll to the events in the calendar (week / day view)
scrollTime() {
// return the first beginning time of the filtered events
if(this.filteredEvents && Array.isArray(this.filteredEvents) && this.filteredEvents.length > 0)
{
let scrollTime = parseInt(this.filteredEvents.sort((a, b) => parseInt(a.beginn) - parseInt(b.beginn))[0].beginn);
// to ensure that the scrollTime watcher triggers even if the scrollTime doesn't change, it returns both the scrollTime and the focusDate
return { focusDate: this.focusDate, scrollTime };
}
// there is no event that matches the current view mode constraints
else
{
return { focusDate: this.focusDate, scrollTime: null };
}
},
// filters the events based on the current calendar view mode
// week view - filter events based on their week
// day view - filter events based on their day and week
// month view - does not filter the events
filteredEvents: function(){
if (this.events && Array.isArray(this.events) && this.events.length > 0) {
let filteredEvents = this.events.filter(event => {
let eventDate = new CalendarDate(new Date(event.datum));
if (this.mode == 'week' || this.mode == 'Week')
{
// week view filters the elements only for the same week
return this.focusDate.w == eventDate.w;
}
else if (this.mode == 'day' || this.mode == 'Day')
{
// day view filters the elements for the same day and the same week
return this.focusDate.d == eventDate.d && this.focusDate.w == eventDate.w;
}
else
{
// returns all the events, does not filter the events
return true;
}
})
return filteredEvents;
}
else
{
return null;
}
},
},
methods: {
setMode(mode) {
this.mode = mode
},
handleInput(day) {
this.$emit(day[0], day[1]);
},
},
created() {
const initMode = this.initialMode.toLowerCase()
const allowedInitialModes = ['day'];
if (!this.noWeekView)
allowedInitialModes.push('week');
if (!this.noMonthView)
allowedInitialModes.push('month');
this.mode = allowedInitialModes[allowedInitialModes.indexOf(initMode)] || allowedInitialModes.pop();
this.date.set(new Date(this.initialDate));
this.focusDate.set(this.date);
},
mounted() {
if (this.$refs.container) {
new ResizeObserver(entries => {
for (const entry of entries) {
const w = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width;
const h = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
// https://getbootstrap.com/docs/5.0/layout/breakpoints/
// bootstrap breakpoints watch window size and this function monitors container size of calendar itself.
// calendar is using bootstrap breakpoints which influence layout, which retriggers this function
// -> some width constellations will loop so we dont use values around bs5 breakpoints
// ['xs', 'sm', 'md', 'lg'][this.size]
if (w >= 600)
this.size = 3;
else if (w >= 350)
this.size = 2;
else if (w >= 250)
this.size = 1;
else
this.size = 0;
this.containerWidth = w
this.containerHeight = h
}
}).observe(this.$refs.container);
}
},
unmounted(){
CalendarDates.cleanup();
},
template: /*html*/`
<div ref="container" class="fhc-calendar card h-100" :class="sizeClass">
<component :is="'calendar-' + mode" @updateMode="mode = $event" @change:range="$emit('change:range',$event)"
@input="handleInput" @change:offset="$emit('change:offset', $event)">
<template #calendarDownloads>
<slot name="calendarDownloads" ></slot>
</template>
<template #monthPage="{event,day}">
<slot name="monthPage" :event="event" :day="day" ></slot>
</template>
<template #weekPage="{event,day}">
<slot name="weekPage" :event="event" :day="day" ></slot>
</template>
<template #dayPage="{event,day,mobile}">
<slot name="dayPage" :event="event" :day="day" :mobile="mobile"></slot>
</template>
<template #pageMobilContent="{lvMenu}">
<slot name="pageMobilContent" :lvMenu="lvMenu"></slot>
</template>
<template #pageMobilContentEmpty>
<slot name="pageMobilContentEmpty" ></slot>
</template>
<template #minimizedPage="{event,day}">
<slot name="minimizedPage" :event="event" :day="day"></slot>
</template>
</component>
</div>`
}
-64
View File
@@ -1,64 +0,0 @@
import CalendarAbstract from './Abstract.js';
import CalendarPane from './Pane.js';
import CalendarDayPage from './Day/Page.js';
import CalendarDate from '../../composables/CalendarDate.js';
export default {
mixins: [
CalendarAbstract
],
components: {
CalendarDayPage,
CalendarPane
},
computed: {
title() {
return this.focusDate.wYear + ' KW ' + this.focusDate.w;
}
},
methods: {
paneChanged(dir) {
let previousDate = new CalendarDate(this.focusDate);
this.focusDate.d += dir;
this.emitRangeChanged(previousDate);
},
emitRangeChanged(previousDate) {
this.$emit('change:range', { start: previousDate, end:this.focusDate });
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: 0, m: 0, d: -1 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 0, m: 0, d: 1 });
},
selectEvent(event) {
this.$emit('input', ['select:event', event]);
}
},
created() {
this.emitRangeChanged();
},
template: /*html*/`
<div class="fhc-calendar-day">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'week')">
<template #calendarDownloads>
<slot name="calendarDownloads"></slot>
</template>
</calendar-header>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-day-page :active="slot.active" :year="focusDate.y" :week="focusDate.w+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" >
<template #dayPage="{event,day,mobile}">
<slot name="dayPage" :event="event" :day="day" :mobile="mobile" ></slot>
</template>
<template #pageMobilContent="{lvMenu}">
<slot name="pageMobilContent" :lvMenu="lvMenu" ></slot>
</template>
<template #pageMobilContentEmpty>
<slot name="pageMobilContentEmpty" ></slot>
</template>
</calendar-day-page>
</calendar-pane>
</div>`
}
-434
View File
@@ -1,434 +0,0 @@
import CalendarDate from '../../../composables/CalendarDate.js';
import LvModal from "../../../components/Cis/Mylv/LvModal.js";
import ApiStundenplan from '../../../api/factory/stundenplan.js';
import ApiAddons from '../../../api/factory/addons.js';
function ggt(m, n) {
return n == 0 ? m : ggt(n, m % n);
}
function kgv(m, n) {
return (m * n) / ggt(m, n);
}
export default {
name: 'DayPage',
components: {
LvModal,
},
data() {
return {
hourPosition: null,
curHourPosition: null,
hourPositionTime: null,
lvMenu: null,
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
'events',
'noMonthView',
'filteredEvents',
'isSliding',
'calendarScrollTop',
'calendarClientHeight',
'setSelectedEvent',
'selectedEvent',
'rowMinHeight'
],
props: {
year: Number,
week: Number,
active: Boolean,
},
emits: [
'updateMode',
'page:back',
'page:forward',
'input'
],
watch: {
//TODO: on first render non of the day-page components are active and the watcher on selectedEvent does not fetch the lvMenu
//TODO: workaround is to watch the active state and refetch in case the lvMenu is empty
active: {
handler(value) {
if (value) {
if (!this.lvMenu) {
this.fetchLvMenu(this.selectedEvent);
}
}
},
immediate: true,
},
eventsPerDayAndHour: {
handler(newEvents) {
// if no event is selected, select the first event of the day
if (this.selectedEvent == null && newEvents[this.day.toDateString()]?.events.length > 0) {
let events = newEvents[this.day.toDateString()]?.events;
if (Array.isArray(events) && events.length > 0) {
this.setSelectedEvent(events[0].orig);
}
}
},
immediate: true
},
selectedEvent: {
handler(event) {
// return early if the day-page component is not the active carousel item
if (!this.active) {
return;
}
this.lvMenu = null;
this.fetchLvMenu(event);
},
immediate: true,
},
isSliding: {
handler(value) {
if (value) {
this.setSelectedEvent(null);
}
}
}
},
computed: {
overlayStyle() {
return {
'background-color': '#F5E9D7',
'position': 'absolute',
'pointer-events': 'none',
'z-index': 2,
height: this.getDayTimePercent + '%',
opacity: 0.5,
overflow: 'hidden'
}
},
pageHeaderStyle() {
return {
'z-index': 4,
'grid-template-columns': 'repeat(' + this.day.length + ', 1fr)',
'grid-template-rows': 1,
position: 'sticky',
top: 0,
}
},
dayText(){
if(!this.day)return {};
return {
heading: this.day.toLocaleString(this.$p.user_locale.value, { dateStyle: 'short' }),
tag: this.day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 2 ? 'narrow' : (this.size < 3 ? 'short' : 'long') }),
datum: this.day.toLocaleString(this.$p.user_locale.value, [{ day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { dateStyle: 'short' }][this.size]),
}
},
noLvStyle() {
return {
top: (this.calendarScrollTop + 100) + 'px',
position: 'absolute',
left: 0,
'text-align': 'center',
width: '100%',
'z-index': 1,
}
},
indicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '3.5rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.hourPosition + 'px',
left: 0,
right: 0,
}
},
curTime() {
const now = new Date();
return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
},
curIndicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '7rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.getDayTimePercent + '%',
left: 0,
right: 0,
}
},
noEventsCondition() {
return !this.isSliding && (this.filteredEvents?.length === 0 || !this.filteredEvents);
},
hours() {
// returns an array with elements starting at 7 and ending at 24
return [...Array(24).keys()].filter(hour => hour >= 7 && hour <= 24);
},
day() {
return new Date(this.focusDate.y, this.focusDate.m, this.focusDate.d);
},
eventsPerDayAndHour() {
// return early if the calendar pane is sliding
if (this.isSliding) return {};
const res = {};
let key = this.day.toDateString();
let nextDay = new Date(this.day);
nextDay.setDate(nextDay.getDate() + 1);
nextDay.setMilliseconds(nextDay.getMilliseconds() - 1);
let d = {events: [], lanes: 1};
if (this.events[key]) {
this.events[key].forEach(evt => {
let event = {
orig: evt,
lane: 1,
maxLane: 1,
start: evt.start < this.day ? this.day : evt.start,
end: evt.end > nextDay ? nextDay : evt.end,
shared: [],
setSharedMaxRecursive(doneItems) {
this.maxLane = Math.max(doneItems[0].maxLane, this.maxLane);
doneItems.push(this);
this.shared.filter(other => !doneItems.includes(other)).forEach(i => i.setSharedMaxRecursive(doneItems));
}
};
event.shared = d.events.filter(other => other.start < event.end && other.end > event.start);
event.shared.forEach(other => other.shared.push(event));
let occupiedLanes = event.shared.map(other => other.lane);
while (occupiedLanes.includes(event.lane))
event.lane++;
event.maxLane = Math.max(...[event.lane], ...occupiedLanes);
if (event.maxLane > 1) {
event.setSharedMaxRecursive([event]);
}
d.events.push(event);
});
d.lanes = d.events.map(e => e.maxLane).reduce((res, i) => kgv(res, i), 1);
}
res[key] = d;
return res;
},
smallestTimeFrame() {
return [30, 15, 10, 5][this.size];
},
lookingAtToday() {
return this.date.compare(this.todayDate)
},
getDayTimePercent() {
const now = new Date(Date.now())
const currentMinutes = now.getMinutes() + now.getHours() * 60
let timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
return timePercentage
}
},
methods: {
dayGridStyle(day) {
const styleObj = {
'grid-template-columns': '1 1fr',
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
}
if(this.date.compare(this.todayDate)) {
styleObj['backgroundImage'] = 'linear-gradient(to bottom, #F5E9D7 '+this.getDayTimePercent+'%, #FFFFFF '+this.getDayTimePercent+'%)'
styleObj['border-color'] = '#E8E8E8';
// styleObj.opacity = 0.5; // would opaque the whole column
}
return styleObj
},
fetchLvMenu(event) {
if (event && event.type == 'lehreinheit') {
this.$api
.call(ApiStundenplan.getLehreinheitStudiensemester(event.lehreinheit_id[0]))
.then(res => res.data)
.then(studiensemester_kurzbz => this.$api.call(
ApiAddons.getLvMenu(
event.lehreinheit_id,
studiensemester_kurzbz
)
))
.then(res => {
if (res.data) {
this.lvMenu = res.data;
}
});
}
},
hourGridIdentifier(hour) {
// this is the id attribute that is responsible to scroll the calender to the first event
return 'scroll' + hour + this.focusDate.d + this.week;
},
hourGridStyle(hour) {
return {
'pointer-events': 'none',
top: this.getAbsolutePositionForHour(hour),
left: 0,
right: 0,
'z-index': 0,
}
},
eventGridStyle(day, event) {
return {
'z-index': 1,
'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane,
'grid-column-end': 1 + event.lane * day.lanes / event.maxLane,
'grid-row-start': this.dateToMinutesOfDay(event.start),
'grid-row-end': this.dateToMinutesOfDay(event.end),
'background-color': event.orig.color,
'--test': this.dateToMinutesOfDay(event.end),
}
},
eventClick(evt) {
let event = evt.orig;
this.setSelectedEvent(event);
this.$emit('input', event);
},
calcHourPosition(event) {
let height = this.$refs.eventcontainer.getBoundingClientRect().height;
let top = this.$refs.eventcontainer.getBoundingClientRect().top;
let position = event.clientY - top;
// position percentage of total height
let timePercentage = (position / height) * 100;
// minute percentage of total minutes
let result = (this.hours.length * 60) * (timePercentage / 100);
// calculate time in float
let currentMinutes = ((result + (this.hours[0] * 60)) / 60);
// get hour part of time
let currentHour = Math.floor(currentMinutes);
// get float part of time
let minutePercentage = currentMinutes % currentHour;
// calculate minutes from float part of time
let minute = Math.round(60 * minutePercentage);
// convert minutes to 5 minute interval
if (minute % 5 != 0) {
minute = Math.round(minute / 5) * 5;
}
// in case the rounding made the minutes 60, increase the hour and reset the minutes
if (minute == 60) {
currentHour++;
minute = 0;
}
// ## after rounding the time to the nearest 5 Minute interval, we have to convert the time back to the relative position
// convert current time in minutes
currentMinutes = currentHour * 60 + minute;
// calculate the minutes percentage of the total minutes
timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
// calculate the relative position of the time percentage
position = height * (timePercentage / 100);
this.hourPosition = position;
// add padding to minutes that consist of only one digit
minute.toString().length == 1 ? minute = "0" + minute : minute;
this.hourPositionTime = currentHour + ":" + minute;
},
getAbsolutePositionForHour(hour) {
// used for the absolute positioning of the gutters of hours
return (100 / this.hours.length) * (hour - (24 - this.hours.length)) + '%';
},
changeToMonth(day) {
if (!this.noMonthView) {
this.date.set(day);
this.focusDate.set(day);
this.$emit('updateMode', 'month');
}
},
dateToMinutesOfDay(day) {
return Math.floor(((day.getHours() - 7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
}
},
template: /*html*/`
<div class="fhc-calendar-day-page h-100">
<div class="row m-0 h-100">
<div class="col-12 col-xl-6 p-0 h-100">
<div class="d-flex flex-column h-100">
<div ref="header" class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle">
<div type="button" class="flex-grow-1" :title="dayText.heading" @click.prevent="changeToMonth(day)">
<div class="fw-bold">{{dayText.tag}}</div>
<a href="#" class="small text-secondary text-decoration-none" >{{dayText.datum}}</a>
</div>
</div>
<div id="scroll g-0" style="height: 100%; overflow-y: scroll;">
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="hourPosition = null" >
<div :id="hourGridIdentifier(hour)" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border-top" :style="hourGridStyle(hour)"></div>
<Transition>
<div v-if="hourPosition && !noEventsCondition" class="position-absolute border-top small" :style="indicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
</div>
</Transition>
<Transition>
<div v-if="lookingAtToday && !noEventsCondition" class="position-absolute border-top small" :style="curIndicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{curTime}}</span>
</div>
</Transition>
<div>
<h1 v-if="noEventsCondition" class="m-0 text-secondary" ref="noEventsText" :style="noLvStyle">{{ $p.t('lehre/noLvFound') }}</h1>
<div :class="{'fhc-calendar-no-events-overlay':noEventsCondition, 'events':true}">
<div class="hours">
<div v-for="hour in hours" :style="'min-height:' + rowMinHeight " :key="hour" class="text-muted text-end small" :ref="'hour' + hour">{{hour}}:00</div>
</div>
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
<div v-if="lookingAtToday && !noEventsCondition" :style="overlayStyle"></div>
<div v-for="event in day.events" :key="event" :style="eventGridStyle(day,event)" v-contrast
:selected="event.orig == selectedEvent" class="fhc-entry mx-2 small rounded overflow-hidden" >
<!-- desktop version of the page template, parent receives slotProp mobile = false -->
<div class="d-none d-xl-block h-100 " @click.prevent="eventClick(event)">
<slot name="dayPage" :event="event" :day="day" :mobile="false">
<p>this is a slot placeholder</p>
</slot>
</div>
<!-- mobile version of the page template, parent receives slotProp mobile = true -->
<div class="d-block d-xl-none h-100" @click.prevent="eventClick(event)">
<slot name="dayPage" :event="event" :day="day" :mobile="true">
<p>this is a slot placeholder</p>
</slot>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-xl-block col-xl-6 p-4 d-none" style="max-height: 100%">
<div style="z-index:0; max-height: 100%" class="sticky-top d-flex justify-content-center align-items-center flex-column">
<div style="max-height: 100%; overflow-y:auto;" class="w-100">
<template v-if="selectedEvent && lvMenu">
<slot name="pageMobilContent" :lvMenu="lvMenu" >
<p>this is a slot placeholder</p>
</slot>
</template>
<template v-else-if="noEventsCondition">
<slot name="pageMobilContentEmpty" >
<h3>This is an slot placeholder</h3>
</slot>
</template>
<template v-else>
<div class="p-4 d-flex w-100 justify-content-center align-items-center">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
`
}
-73
View File
@@ -1,73 +0,0 @@
export default {
data(){
return{
selected: this.mode,
modes:{
day: { mode_bezeichnung: "day", icon: "fa-calendar-day" , condition:true},
week: { mode_bezeichnung: "week", icon: "fa-calendar-week", condition: !this.noWeekView },
month: { mode_bezeichnung: "month", icon: "fa-calendar-days", condition: !this.noMonthView },
},
headerPadding:null,
}
},
inject: [
'eventsAreNull',
'size',
'mode',
'noWeekView',
'noMonthView',
'containerWidth'
],
props: {
title: String
},
emits: [
'updateMode',
'prev',
'next',
'click'
],
computed: {
getHeaderClassSide() {
return this.containerWidth > 780 ? 'col-3' : 'col-12'
},
getHeaderClassMiddle() {
return this.containerWidth > 780 ? 'col-6' : 'col-12'
}
},
template: /*html*/`
<div class="calendar-header card-header w-100">
<div class="row align-items-center ">
<div :class="getHeaderClassSide" class="d-flex justify-content-center justify-content-md-start align-items-center">
<slot name="calendarDownloads"></slot>
</div>
<div :class="getHeaderClassMiddle" :style="{'padding-left':headerPadding}">
<div class="row align-items-center justify-content-center">
<div class="col-auto p-2">
<button class="btn btn-outline-secondary border-0" :class="{'btn-sm':!this.size}" @click="$emit('prev')"><i class="fa fa-chevron-left"></i></button>
</div>
<div class="justify-content-center text-center col-auto">
<div class="d-flex justify-content-center align-items-center">
<button class="btn btn-link link-secondary text-decoration-none" :class="{'btn-sm': !this.size}" @click="$emit('click')">
{{ title }}
<i v-if="eventsAreNull" class="fa fa-spinner fa-pulse"></i>
</button>
</div>
</div>
<div class="col-auto p-2">
<button class="btn btn-outline-secondary border-0" :class="{'btn-sm': !this.size}" @click="$emit('next')"><i class="fa fa-chevron-right"></i></button>
</div>
</div>
</div>
<div ref="viewButtons" v-if="!noWeekView && !noMonthView" :class="getHeaderClassSide" class="d-flex justify-content-center justify-content-md-end align-items-center" style="pointer-events: none;">
<div style="pointer-events: all;">
<div role="group" aria-label="Kalender Modus">
<button type="button" :class="{'active':mode_kurzbz.toLowerCase() === mode.toLowerCase()}" style="margin-right: 4px;" @click.prevent="$emit('updateMode',mode_kurzbz)" class="btn btn-outline-secondary" v-for="({mode_bezeichnung,icon,condition},mode_kurzbz) in modes">
<i v-if="condition" class="fa" :class="icon" ></i>
</button>
</div>
</div>
</div>
</div>
</div>`
}
+164
View File
@@ -0,0 +1,164 @@
import FhcCalendar from "./Base.js";
import ApiLvPlan from '../../api/factory/lvPlan.js';
import { useEventLoader } from '../../composables/EventLoader.js';
import ModeDay from './Mode/Day.js';
import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js';
export default {
name: "CalendarLvPlan",
components: {
FhcCalendar
},
inject: [
"renderers"
],
props: {
timezone: {
type: String,
required: true
},
date: {
type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local()
},
mode: {
type: String,
default: 'Week'
},
getPromiseFunc: {
type: Function,
required: true
}
},
emits: [
"update:date",
"update:mode",
"update:range"
],
data() {
return {
modes: {
day: Vue.markRaw(ModeDay),
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth)
},
modeOptions: {
day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound'))
},
week: {
collapseEmptyDays: false
}
},
teachingunits: null
};
},
computed: {
backgrounds() {
let now = luxon.DateTime.now().setZone(this.timezone);
if (this.mode == 'Month')
return [
{
class: 'background-past',
end: now.startOf('day')
}
];
return [
{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}
];
}
},
methods: {
eventStyle(event) {
if (!event.farbe)
return undefined;
return '--event-bg:#' + event.farbe;
},
updateRange(rangeInterval) {
this.rangeInterval = rangeInterval;
this.$emit('update:range', rangeInterval);
}
},
setup(props, context) {
const rangeInterval = Vue.ref(null);
const { events, lv } = useEventLoader(rangeInterval, props.getPromiseFunc);
Vue.watch(lv, newValue => {
context.emit('update:lv', newValue);
});
return {
rangeInterval,
events,
lv
};
},
created() {
this.$api
.call(ApiLvPlan.getStunden())
.then(res => {
return this.teachingunits = res.data.map(el => ({
id: el.stunde,
start: el.beginn,
end: el.ende
}));
});
},
template: /* html */`
<fhc-calendar
ref="calendar"
class="fhc-calendar-lvplan"
:date="date"
:modes="modes"
:mode-options="modeOptions"
:mode="mode"
:timezone="timezone"
:locale="$p.user_locale.value"
:events="events || []"
:backgrounds="backgrounds"
:time-grid="teachingunits"
show-btns
@update:date="(newDate, newMode) => $emit('update:date', newDate, newMode)"
@update:mode="(newMode, newDate) => $emit('update:mode', newMode, newDate)"
@update:range="updateRange"
>
<template v-slot="{ event, mode }">
<div
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
:type="mode == 'day' ? 'button' : undefined"
:style="eventStyle(event)"
>
<component
v-if="mode == 'event'"
:is="renderers[event.type]?.modalContent"
:event="event"
></component>
<component
v-else-if="mode == 'eventheader'"
:is="renderers[event.type]?.modalTitle"
:event="event"
></component>
<component
v-else
:is="renderers[event.type]?.calendarEvent"
:event="event"
></component>
</div>
</template>
<template #actions>
<slot />
</template>
</fhc-calendar>`
}
@@ -1,26 +0,0 @@
import CalendarAbstract from './Abstract.js';
export default {
mixins: [
CalendarAbstract
],
inject: [
'size',
'minimized',
'date',
],
data() {
return {
start: 0
}
},
methods: {
maximize() {
this.minimized = false;
}
},
template: `
<div class="fhc-calendar-minimized h-100 d-flex flex-column">
<slot name="minimizedPage"></slot>
</div>`
}
+106
View File
@@ -0,0 +1,106 @@
import BaseSlider from '../Base/Slider.js';
import DayView from './Day/View.js';
export default {
name: "ModeDay",
components: {
BaseSlider,
DayView
},
props: {
currentDate: {
type: luxon.DateTime,
required: true
}
},
emits: [
"update:currentDate",
"update:range",
"click",
"requestModalOpen",
"requestModalClose"
],
data() {
return {
focusDate: this.currentDate,
rangeOffset: 0
};
},
computed: {
range() {
let first = this.focusDate.startOf('day');
let last = this.focusDate.endOf('day');
if (this.rangeOffset != 0) {
if (this.rangeOffset < 0) {
first = first.plus({ days: this.rangeOffset });
} else {
last = last.plus({ days: this.rangeOffset });
}
}
return luxon.Interval.fromDateTimes(first, last);
}
},
watch: {
currentDate() {
if (this.currentDate.locale != this.focusDate.locale) {
this.focusDate = this.currentDate;
this.$emit('update:range', this.range);
} else {
this.rangeOffset = this.currentDate.startOf('day').diff(this.focusDate.startOf('day'), 'days').days;
if (this.rangeOffset) {
this.$refs.view.$refs.grid.disableAutoScroll();
this.$emit('update:range', this.range);
this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
}
}
}
},
methods: {
prevPage() {
this.rangeOffset = this.$refs.slider.target - 1;
this.$refs.view.$refs.grid.disableAutoScroll();
this.$emit('update:range', this.range);
this.$refs.slider.prevPage().then(this.updatePage);
},
nextPage() {
this.rangeOffset = this.$refs.slider.target + 1;
this.$refs.view.$refs.grid.disableAutoScroll();
this.$emit('update:range', this.range);
this.$refs.slider.nextPage().then(this.updatePage);
},
updatePage(days) {
const newFocusDate = this.focusDate.plus({ days });
this.focusDate = newFocusDate;
this.rangeOffset = 0;
this.$emit('update:currentDate', this.focusDate);
this.$emit('update:range', this.range);
this.$refs.view.$refs.grid.enableAutoScroll();
},
viewAttrs(days) {
const day = this.focusDate.plus({ days });
return { ...this.$attrs, day };
}
},
mounted() {
this.$emit('update:range', this.range);
this.$refs.view.$refs.grid.enableAutoScroll();
},
template: `
<div
class="fhc-calendar-mode-day flex-grow-1 position-relative"
>
<base-slider ref="slider" v-slot="slot">
<day-view
ref="view"
v-bind="viewAttrs(slot.offset)"
@request-modal-open="$emit('requestModalOpen', $event)"
@request-modal-close="$emit('requestModalClose', $event)"
>
<template v-slot="slot"><slot v-bind="slot" /></template>
</day-view>
</base-slider>
</div>
`
}
@@ -0,0 +1,175 @@
import CalendarGrid from '../../Base/Grid.js';
import LabelDay from '../../Base/Label/Day.js';
import LabelDow from '../../Base/Label/Dow.js';
import LabelTime from '../../Base/Label/Time.js';
import { useResizeObserver } from '../../../../composables/Responsive.js';
export default {
name: "DayView",
components: {
CalendarGrid,
LabelDay,
LabelDow,
LabelTime
},
inject: {
timeGrid: "timeGrid",
originalEvents: "events",
timezone: "timezone"
},
props: {
day: {
type: luxon.DateTime,
required: true
},
emptyMessage: String,
emptyMessageDetails: String
},
emits: [
"requestModalOpen",
"requestModalClose"
],
data() {
return {
chosenEvent: null,
gridMainRef: null
};
},
computed: {
axisMain() {
return [this.day.startOf('day')];
},
axisParts() {
if (this.timeGrid) {
// create {start, end} array
return this.timeGrid.map(tu => {
return {
start: luxon.Duration.fromISOTime(tu.start),
end: luxon.Duration.fromISOTime(tu.end)
};
});
} else {
// create 07:00-23:00
return Array.from({ length: 17 }, (e, i) => luxon.Duration.fromObject({ hours: i + 7 }));
}
},
events() {
return this.originalEvents
.filter(event => event.start < this.day.plus({ days: 1 }) && event.end > this.day)
.sort((a, b) => a.start.ts - b.start.ts)
.map(evt => evt.orig);
},
currentEvent() {
if (this.chosenEvent) {
if (this.events.find(e => e == this.chosenEvent))
return this.chosenEvent;
}
let first = null;
if (this.events)
first = this.events.find(Boolean); // undefined => none found
if (first && first.type == 'loading')
return null; // null => loading
return first;
},
isToday() {
return this.day.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
}
},
watch: {
compact() {
if (this.compact) {
if (this.chosenEvent) {
this.$emit('requestModalOpen', {
event: this.chosenEvent,
closeFn: () => { this.chosenEvent = null; }
});
}
} else {
this.$emit('requestModalClose');
}
}
},
methods: {
handleClickDefaults(evt) {
if (evt.detail.source == 'event') {
this.chosenEvent = evt.detail.value;
if (this.compact) {
this.$emit('requestModalOpen', {
event: this.chosenEvent,
closeFn: () => { this.chosenEvent = null; }
});
}
}
}
},
setup() {
const container = Vue.ref(null); // use useTemplateRef when updating to Vue 3.5
const { compact } = useResizeObserver(container, 750);
return {
container, // must be exposed or it won't be set in Vue < 3.5
compact
};
},
mounted() {
this.gridMainRef = this.$refs.grid.$refs.main;
},
template: /* html */`
<div
ref="container"
class="fhc-calendar-mode-day-view d-flex h-100"
@cal-click-default.capture="handleClickDefaults"
>
<calendar-grid
ref="grid"
:axis-main="axisMain"
:axis-parts="axisParts"
:snap-to-grid="!!timeGrid"
all-day-events
>
<template #main-header="{ date }">
<div :class="{ today: isToday }">
<label-dow
@cal-click="evt => evt.detail.source = 'day'"
v-bind="{ date }"
/>
<label-day
v-bind="{ date }"
/>
</div>
</template>
<template #part-header="{ part }">
<label-time v-bind="{ part }" />
</template>
<template #event="slot">
<div v-if="slot.event.type == 'loading'" class="placeholder-glow h-100 opacity-50">
<span class="placeholder w-100 h-100" />
</div>
<slot v-else v-bind="slot" mode="day" />
</template>
</calendar-grid>
<Teleport :disabled="!gridMainRef" :to="gridMainRef">
<div
v-if="emptyMessage && currentEvent !== null && !currentEvent"
class="fhc-calendar-no-events-overlay"
style="position:absolute;inset:0"
>
{{ emptyMessage }}
</div>
</Teleport>
<div class="event-details" v-if="!compact">
<div
v-if="currentEvent === null"
class="p-4 d-flex w-100 justify-content-center align-items-center"
>
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<h3 v-else-if="!currentEvent">{{ emptyMessageDetails }}</h3>
<slot v-else :event="currentEvent" mode="event" />
</div>
</div>
`
}
+108
View File
@@ -0,0 +1,108 @@
import BaseSlider from '../Base/Slider.js';
import ListView from './List/View.js';
export default {
name: "ModeList",
components: {
BaseSlider,
ListView
},
props: {
currentDate: {
type: luxon.DateTime,
required: true
},
length: {
type: Number,
default: 7
}
},
emits: [
"update:currentDate",
"update:range",
"click",
"requestModalOpen"
],
data() {
return {
focusDate: this.currentDate,
rangeOffset: 0
};
},
computed: {
range() {
let first = this.focusDate;
let last = first.plus({ days: this.length });
if (this.rangeOffset != 0) {
if (this.rangeOffset < 0) {
first = first.plus({ days: this.rangeOffset });
} else {
last = first.plus({ days: this.rangeOffset + this.length });
}
}
return luxon.Interval.fromDateTimes(first, last);
}
},
watch: {
currentDate() {
if (this.currentDate.locale != this.focusDate.locale) {
this.focusDate = this.currentDate;
this.$emit('update:range', this.range);
} else {
this.rangeOffset = this.currentDate.startOf('day').diff(this.focusDate.startOf('day'), 'days').days;
if (this.rangeOffset) {
this.$emit('update:range', this.range);
this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
}
}
}
},
methods: {
prevPage() {
this.rangeOffset = this.$refs.slider.target - 1;
this.$emit('update:range', this.range);
this.$refs.slider.prevPage().then(this.updatePage);
},
nextPage() {
this.rangeOffset = this.$refs.slider.target + 1;
this.$emit('update:range', this.range);
this.$refs.slider.nextPage().then(this.updatePage);
},
updatePage(offset) {
const newFocusDate = this.focusDate.plus({ days: offset });
this.focusDate = newFocusDate;
this.rangeOffset = 0;
this.$emit('update:currentDate', this.focusDate);
this.$emit('update:range', this.range);
},
viewAttrs(offset) {
const day = this.focusDate.plus({ days: offset });
return { day, length: this.length };
},
handleClickDefaults(evt) {
switch (evt.detail.source) {
case 'event':
// default: Request Modal
this.$emit('requestModalOpen', { event: evt.detail.value });
break;
}
}
},
mounted() {
this.$emit('update:range', this.range);
},
template: `
<div
class="fhc-calendar-mode-list flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults"
>
<base-slider ref="slider" v-slot="slot">
<list-view v-bind="viewAttrs(slot.offset)">
<template v-slot="slot"><slot v-bind="slot" /></template>
</list-view>
</base-slider>
</div>
`
}
@@ -0,0 +1,98 @@
import LabelDay from '../../Base/Label/Day.js';
import LabelDow from '../../Base/Label/Dow.js';
import CalDnd from '../../../../directives/Calendar/DragAndDrop.js';
import CalClick from '../../../../directives/Calendar/Click.js';
// TODO(chris): drag and drop
export default {
name: "ListView",
components: {
LabelDay,
LabelDow
},
directives: {
CalDnd,
CalClick
},
inject: {
draggableEvents: "draggableEvents",
events: "events",
timezone: "timezone"
},
props: {
day: {
type: luxon.DateTime,
required: true
},
length: {
type: Number,
required: true
}
},
data() {
return {
chosenEvent: null
};
},
computed: {
days() {
return Array.from({ length: this.length }, (e, days) => this.day.plus({ days }));
},
eventsPerDay() {
const eventsPerDay = this.days.map(day => {
return {
day,
events: this.events
.filter(event => event.start < day.plus({ days: 1 }) && event.end > day)
.sort((a, b) => a.start.ts - b.start.ts)
};
});
return eventsPerDay.filter(day => day.events.length);
}
},
methods: {
draggable(event) {
return this.draggableEvents(event.orig, 'list');
},
isToday(date) {
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
}
},
template: /* html */`
<div class="fhc-calendar-mode-list-view h-100 overflow-auto">
<div v-if="!eventsPerDay.length" class="h-100">
<slot :event="undefined" mode="list" />
</div>
<div v-for="{ day, events } in eventsPerDay">
<div
class="text-center"
:class="{ today: isToday(day) }"
>
<label-dow
:date="day"
class="d-inline"
@cal-click="evt => evt.detail.source = 'day'"
/>
,
<label-day :date="day" class="d-inline" />
</div>
<div v-for="event in events">
<div v-if="event.type == 'loading'" class="placeholder-glow opacity-50">
<span class="placeholder w-100" />
</div>
<div
v-else
class="event"
:draggable="draggable(event)"
v-cal-dnd:draggable="event"
v-cal-click:event="event.orig"
>
<slot :event="event.orig" mode="list" />
</div>
</div>
</div>
</div>
`
}
+126
View File
@@ -0,0 +1,126 @@
import BaseSlider from '../Base/Slider.js';
import MonthView from './Month/View.js';
export default {
name: "ModeMonth",
components: {
BaseSlider,
MonthView
},
props: {
currentDate: {
type: luxon.DateTime,
required: true
}
},
emits: [
"update:currentDate",
"update:range",
"click",
"requestModalOpen"
],
data() {
return {
focusDate: this.currentDate,
rangeOffset: 0
};
},
computed: {
range() {
let first = this.focusDate.startOf('month').startOf('week', { useLocaleWeeks: true });
let last = first.plus({ days: 41 }).endOf('day'); // NOTE(chris): 6 weeks minus 1 day
if (this.rangeOffset != 0) {
const nextFocusDate = this.focusDate.plus({ months: this.rangeOffset});
const nextRangeStart = nextFocusDate.startOf('month').startOf('week', { useLocaleWeeks: true });
if (this.rangeOffset < 0) {
first = nextRangeStart;
} else {
last = nextRangeStart.plus({ days: 41 }).endOf('day');
}
}
return luxon.Interval.fromDateTimes(first, last);
}
},
watch: {
currentDate() {
if (this.currentDate.locale != this.focusDate.locale) {
this.focusDate = this.currentDate;
this.$emit('update:range', this.range);
} else {
this.rangeOffset = this.currentDate.startOf('month').diff(this.focusDate.startOf('month'), 'months').months;
if (this.rangeOffset) {
this.$emit('update:range', this.range);
this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
}
}
}
},
methods: {
prevPage() {
this.rangeOffset = this.$refs.slider.target - 1;
this.$emit('update:range', this.range);
this.$refs.slider.prevPage().then(this.updatePage);
},
nextPage() {
this.rangeOffset = this.$refs.slider.target + 1;
this.$emit('update:range', this.range);
this.$refs.slider.nextPage().then(this.updatePage);
},
updatePage(months) {
const newFocusDate = this.focusDate.plus({ months });
this.focusDate = newFocusDate;
this.rangeOffset = 0;
this.$emit('update:currentDate', this.focusDate);
this.$emit('update:range', this.range);
},
viewAttrs(months) {
const day = this.focusDate.plus({ months });
return { day };
},
handleClickDefaults(evt) {
switch (evt.detail.source) {
case 'week':
// default: Move to week if not in month
let dayInWeek = luxon.DateTime.fromObject({
localWeekNumber: evt.detail.value.number,
localWeekYear: evt.detail.value.year
}, {
zone: this.currentDate.zoneName,
locale: this.currentDate.locale
});
if (!this.focusDate.hasSame(dayInWeek.startOf('week', { useLocaleWeeks: true }), 'month')) {
this.$emit('update:currentDate', dayInWeek.startOf('week', { useLocaleWeeks: true }));
} else if (!this.focusDate.hasSame(dayInWeek.endOf('week', { useLocaleWeeks: true }), 'month')) {
this.$emit('update:currentDate', dayInWeek.endOf('week', { useLocaleWeeks: true }));
}
break;
case 'day':
// default: Set current-date
this.$emit('update:currentDate', evt.detail.value);
break;
case 'event':
// default: Request Modal
this.$emit('requestModalOpen', { event: evt.detail.value });
break;
}
}
},
mounted() {
this.$emit('update:range', this.range);
},
template: `
<div
class="fhc-calendar-mode-month flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults"
>
<base-slider ref="slider" v-slot="slot">
<month-view v-bind="viewAttrs(slot.offset)">
<template v-slot="slot"><slot v-bind="slot" mode="month" /></template>
</month-view>
</base-slider>
</div>
`
}
@@ -0,0 +1,95 @@
import CalendarGrid from '../../Base/Grid.js';
import LabelWeek from '../../Base/Label/Week.js';
import LabelDow from '../../Base/Label/Dow.js';
import LabelDay from '../../Base/Label/Day.js';
export default {
name: "MonthView",
components: {
CalendarGrid,
LabelWeek,
LabelDow,
LabelDay
},
provide() {
return {
// NOTE(chris): snap events to day
events: Vue.computed(() => {
//const events = [];
const events = this.events.map(event => {
const start = event.start.startOf('day');
const end = event.end.plus({ days: 1 }).startOf('day');
return {
...event,
start,
end
};
});
for (var w = 5; w > -1; w--) {
for (var d = 6; d > -1; d--) {
const startdate = this.axisMain[w].plus(this.axisParts[d]);
events.unshift({
start: startdate,
end: startdate.plus({ days: 1 }),
orig: 'header'
});
}
}
return events;
})
};
},
inject: {
events: "events",
timezone: "timezone"
},
props: {
day: {
type: luxon.DateTime,
required: true
}
},
computed: {
axisMain() {
const start = this.day.startOf('month').startOf('week', { useLocaleWeeks: true });
return Array.from({ length: 6 }, (e, i) => start.plus({ weeks: i }));
},
axisParts() {
return Array.from({ length: 8 }, (e, i) => luxon.Duration.fromObject({ days: i }));
}
},
methods: {
isToday(date) {
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
}
},
template: /* html */`
<div class="fhc-calendar-mode-month-view h-100">
<calendar-grid
flip-axis
:axis-main="axisMain"
:axis-parts="axisParts"
snap-to-grid
>
<template #main-header="{ date }">
<label-week v-bind="{ date }" />
</template>
<template #part-header="{ part }">
<label-dow :date="axisMain[0].plus(part)" class="text-center" />
</template>
<template #event="slot">
<label-day
v-if="slot.event.orig == 'header'"
:date="slot.event.start"
:class="{ disabled: !day.hasSame(slot.event.start, 'month'), today: isToday(slot.event.start) }"
/>
<div v-else-if="slot.event.type == 'loading'" class="placeholder-glow opacity-50">
<span class="placeholder w-100 fs-1" />
</div>
<slot v-else v-bind="slot" />
</template>
</calendar-grid>
</div>
`
}
+113
View File
@@ -0,0 +1,113 @@
import BaseSlider from '../Base/Slider.js';
import WeekView from './Week/View.js';
export default {
name: "ModeWeek",
components: {
BaseSlider,
WeekView
},
props: {
currentDate: {
type: luxon.DateTime,
required: true
}
},
emits: [
"update:currentDate",
"update:range",
"click",
"requestModalOpen"
],
data() {
return {
focusDate: this.currentDate,
rangeOffset: 0
};
},
computed: {
range() {
let first = this.focusDate.startOf('week', { useLocaleWeeks: true });
let last = this.focusDate.endOf('week', { useLocaleWeeks: true });
if (this.rangeOffset != 0) {
if (this.rangeOffset < 0) {
first = first.plus({ weeks: this.rangeOffset });
} else {
last = last.plus({ weeks: this.rangeOffset });
}
}
return luxon.Interval.fromDateTimes(first, last);
}
},
watch: {
currentDate() {
if (this.currentDate.locale != this.focusDate.locale) {
this.focusDate = this.currentDate;
this.$emit('update:range', this.range);
} else {
this.rangeOffset = this.currentDate.startOf('week', { useLocaleWeeks: true }).diff(this.focusDate.startOf('week', { useLocaleWeeks: true }), 'weeks').weeks;
if (this.rangeOffset) {
this.$refs.view.$refs.grid.disableAutoScroll();
this.$emit('update:range', this.range);
this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
}
}
}
},
methods: {
prevPage() {
this.rangeOffset = this.$refs.slider.target - 1;
this.$refs.view.$refs.grid.disableAutoScroll();
this.$emit('update:range', this.range);
this.$refs.slider.prevPage().then(this.updatePage);
},
nextPage() {
this.rangeOffset = this.$refs.slider.target + 1;
this.$refs.view.$refs.grid.disableAutoScroll();
this.$emit('update:range', this.range);
this.$refs.slider.nextPage().then(this.updatePage);
},
updatePage(weeks) {
const newFocusDate = this.focusDate.plus({ weeks });
this.focusDate = newFocusDate;
this.rangeOffset = 0;
this.$emit('update:currentDate', this.focusDate);
this.$emit('update:range', this.range);
this.$refs.view.$refs.grid.enableAutoScroll();
},
viewAttrs(weeks) {
const day = this.focusDate.plus({ weeks });
return { ...this.$attrs, day };
},
handleClickDefaults(evt) {
switch (evt.detail.source) {
case 'day':
// default: Set current-date
this.$emit('update:currentDate', evt.detail.value);
break;
case 'event':
// default: Request Modal
this.$emit('requestModalOpen', { event: evt.detail.value });
break;
}
}
},
mounted() {
this.$emit('update:range', this.range);
this.$refs.view.$refs.grid.enableAutoScroll();
},
template: `
<div
class="fhc-calendar-mode-week flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults"
>
<base-slider ref="slider" v-slot="slot">
<week-view ref="view" v-bind="viewAttrs(slot.offset)">
<template v-slot="slot"><slot v-bind="slot" mode="week" /></template>
</week-view>
</base-slider>
</div>
`
}
@@ -0,0 +1,85 @@
import CalendarGrid from '../../Base/Grid.js';
import LabelDay from '../../Base/Label/Day.js';
import LabelDow from '../../Base/Label/Dow.js';
import LabelTime from '../../Base/Label/Time.js';
export default {
name: "WeekView",
components: {
CalendarGrid,
LabelDay,
LabelDow,
LabelTime
},
inject: {
timeGrid: "timeGrid",
timezone: "timezone"
},
props: {
day: {
type: luxon.DateTime,
required: true
},
collapseEmptyDays: Boolean
},
computed: {
start() {
return this.day.startOf('week', { useLocaleWeeks: true });
},
axisMain() {
return Array.from({ length: 7 }, (e, i) => this.start.plus({ days: i }));
},
axisParts() {
if (this.timeGrid) {
// create {start, end} array
return this.timeGrid.map(tu => {
return {
start: luxon.Duration.fromISOTime(tu.start),
end: luxon.Duration.fromISOTime(tu.end)
};
});
} else {
// create 07:00-23:00
return Array.from({ length: 17 }, (e, i) => luxon.Duration.fromObject({ hours: i + 7 }));
}
}
},
methods: {
isToday(date) {
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
}
},
template: /* html */`
<div class="fhc-calendar-mode-week-view h-100">
<calendar-grid
ref="grid"
:axis-main="axisMain"
:axis-parts="axisParts"
:axis-main-collapsible="collapseEmptyDays"
:snap-to-grid="!!timeGrid"
all-day-events
>
<template #main-header="{ date }">
<div :class="{ today: isToday(date) }">
<label-dow
v-bind="{ date }"
@cal-click="evt => evt.detail.source = 'day'"
/>
<label-day
v-bind="{ date }"
/>
</div>
</template>
<template #part-header="{ part }">
<label-time v-bind="{ part }" />
</template>
<template #event="slot">
<div v-if="slot.event.type == 'loading'" class="placeholder-glow h-100 opacity-50">
<span class="placeholder w-100 h-100" />
</div>
<slot v-else v-bind="slot" />
</template>
</calendar-grid>
</div>
`
}
-79
View File
@@ -1,79 +0,0 @@
import CalendarAbstract from './Abstract.js';
import CalendarPane from './Pane.js';
import CalendarMonthPage from './Month/Page.js';
export default {
mixins: [
CalendarAbstract
],
components: {
CalendarMonthPage,
CalendarPane
},
data() {
return {
syncOnNextChange: false
}
},
computed: {
title() {
return this.focusDate.format({month: ['short','long','long','long'][this.size], year: 'numeric'}, this.$p.user_locale.value);
}
},
methods: {
paneChanged(dir) {
if (this.syncOnNextChange) {
this.syncOnNextChange = false;
this.focusDate.set(this.date);
} else {
this.focusDate.moveMonthInDirection(dir)
}
this.$emit('change:range', {
start: new Date(this.focusDate.y, this.focusDate.m, 1),
end: new Date(this.focusDate.y, this.focusDate.m+1, 0)
});
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: 0, m: -1, d: 0 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 0, m: 1, d: 0 });
},
selectDay(day) {
let m = day.getMonth();
if (this.focusDate.m != m) {
this.syncOnNextChange = true;
if (this.focusDate.m-1 == m || (m == 11 && !this.focusDate.m))
this.$refs.pane.prev();
else
this.$refs.pane.next();
} else {
this.focusDate.set(this.date);
}
this.$emit('input', ['select:day',day])
}
},
created() {
this.$emit('change:range', {
start: new Date(this.focusDate.y, this.focusDate.m, 1),
end: new Date(this.focusDate.y, this.focusDate.m+1, 0)
});
},
template: `
<div class="fhc-calendar-month">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'months')" >
<template #calendarDownloads>
<slot name="calendarDownloads"></slot>
</template>
</calendar-header>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-month-page :year="focusDate.y" :month="focusDate.m+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectDay" >
<template #monthPage="{event,day}">
<slot name="monthPage" :event="event" :day="day" ></slot>
</template>
</calendar-month-page>
</calendar-pane>
</div>`
}
-166
View File
@@ -1,166 +0,0 @@
import CalendarDate from '../../../composables/CalendarDate.js';
export default {
name: 'MonthPage',
data(){
return{
highlightedWeek: null,
highlightedDay: null,
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
'events',
'showWeeks',
'noWeekView',
'selectedEvent',
'setSelectedEvent'
],
props: {
year: Number,
month: Number
},
emits: [
'updateMode',
'page:back',
'page:forward',
'input'
],
computed: {
dayText(){
if (!this.size || !this.weeks[0]?.days) return {};
let dayTextMap ={};
this.weeks[0].days.forEach((day)=>{
dayTextMap[day] = day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 1 ? 'narrow' : (this.size < 3 ? 'short' : 'long') });
});
return dayTextMap;
},
weeks() {
let firstDayOfMonth = new CalendarDate(this.year, this.month, 1);
let startDay = firstDayOfMonth.firstDayOfCalendarMonth;
let endDay = firstDayOfMonth.lastDayOfCalendarMonth;
let res = [];
let week = {no:0,y:0,days:[]};
while (startDay <= endDay) {
week.days.push(new Date(startDay));
if (week.days.length == 7) {
let d = new CalendarDate(week.days[5]);
week.no = d.w;
week.y = d.y;
res.push(week);
week = {no:0,y:0,days:[]};
}
startDay.setDate(startDay.getDate() + 1);
}
return res;
},
},
methods: {
getDayClass(week, day) {
let classstring = 'fhc-calendar-month-page-day text-decoration-none overflow-hidden'
const isHighlightedWeek = this.isHighlightedWeek(week)
const isHighlightedDay = this.isHighlightedDay(day)
const isThisDate = this.date.compare(day)
const isNotThisMonth = day.getMonth() != this.month
const isInThePast = day.getTime() < this.today // this.date is just the focusDate but not the initial Date
if(isHighlightedWeek) classstring += ' fhc-highlight-week'
if(isHighlightedDay) classstring += ' fhc-highlight-day'
if(isNotThisMonth) classstring += ' opacity-25'
if(isInThePast) classstring += ' fhc-calendar-past'
return classstring
},
selectDay(day) {
this.date.set(day);
this.$emit('input', day);
},
changeToWeek(week) {
if (!this.noWeekView) {
if (!this.focusDate.isInWeek(week.no, week.y))
this.focusDate.set(week.days[0]);
this.$emit('updateMode', 'week');
}
},
highlight(week, day){
this.highlightedWeek = week.no;
this.highlightedDay = day;
},
isHighlightedDay(day) {
return day == this.highlightedDay
},
isHighlightedWeek(week) {
return week.no == this.highlightedWeek
},
clickEvent(day,week) {
if(!this.noWeekView)
{
this.focusDate.set(day);
this.$emit('updateMode', 'day');
}
this.selectDay(day);
},
getNumberStyle(day) {
const styleObj = {}
styleObj.display = 'inline-block';
styleObj.height = '32px';
styleObj['line-height'] = '32px';
styleObj['text-align'] = 'center';
styleObj['font-weight'] = 'bold';
styleObj['font-size'] = '14px';
if(day.getDate() === this.todayDate.getDate()
&& day.getMonth() === this.todayDate.getMonth()
&& day.getFullYear() === this.todayDate.getFullYear()) {
styleObj['background-color'] = '#00649c'; // fh blau
styleObj.color = 'white';
}
return styleObj
}
},
mounted() {
const container = document.getElementById("calendarContainer")
if(container) container.style['overflow-y'] = 'auto'
},
template: /*html*/`
<div class="fhc-calendar-month-page" :class="{'show-weeks': showWeeks}">
<div v-if="showWeeks" class=" bg-light fw-bold border-top border-bottom text-center"></div>
<div v-for="day in weeks[0].days" :key="day" class="bg-light fw-bold border-top border-bottom text-center">
{{dayText[day]}}
</div>
<template v-for="week in weeks"
:key="week.no">
<a href="#" v-if="showWeeks" class="fhc-calendar-month-page-weekday text-decoration-none text-end opacity-25"
@click.prevent="changeToWeek(week)">{{week.no}}</a>
<a href="#"
@click="clickEvent(day,week)"
@mouseover="highlight(week,day)"
@mouseleave="highlightedWeek = null; highlightedDay = null"
v-for="day in week.days"
:key="day"
:class="getDayClass(week, day)"
>
<span @click="clickEvent(day,week)" class="no" :style="getNumberStyle(day)">{{day.getDate()}}</span>
<span v-if="events[day.toDateString()] && events[day.toDateString()].length" class="events">
<div v-for="event in events[day.toDateString()]" :key="event.id"
:style="{'background-color': event.color}" class="fhc-entry" :selected="event == selectedEvent" v-contrast >
<slot name="monthPage" :event="event" :day="day" >
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
</slot>
</div>
</span>
</a>
</template>
</div>
`
}
-34
View File
@@ -1,34 +0,0 @@
import CalendarAbstract from './Abstract.js';
export default {
mixins: [
CalendarAbstract
],
inject: [
'size'
],
data() {
return {
monthIndices: [...Array(12).keys()]
}
},
computed: {
title() {
return this.focusDate.format({year: 'numeric'});
},
months() {
return this.monthIndices.map(i => (new Date(0, i, 1)).toLocaleString(this.$p.user_locale.value, {month: this.size < 2 ? 'short' : 'long'}));
}
},
template: `
<div class="fhc-calendar-months">
<calendar-header :title="title" @prev="focusDate.y--" @next="focusDate.y++" @click="$emit('updateMode', 'years')" @updateMode="$emit('updateMode', $event)" />
<div class="d-flex flex-wrap">
<div v-for="(month, key) in months" :key="key" class="d-grid col-4">
<button @click="focusDate.m = key; $emit('updateMode', 'month')" class="btn btn-outline-secondary" :class="{'border-0': key != focusDate.m}">
{{month}}
</button>
</div>
</div>
</div>`
}
-91
View File
@@ -1,91 +0,0 @@
export default {
name: 'Pane',
emits: [
'slid'
],
data() {
return {
carousel: null,
queue: 0,
offset: 0,
slideAnimation:false,
scrollTop:null,
clientHeight:null,
carouselItems:null,
}
},
provide() {
return {
isSliding: Vue.computed(() => this.slideAnimation),
calendarScrollTop: Vue.computed(() =>this.scrollTop),
calendarClientHeight: Vue.computed(() => this.clientHeight),
}
},
computed: {
offsets() {
return [...Array(3).keys()].map(i => (3+i-this.offset)%3-1);
},
activeCarouselItemIndex() {
if (Array.isArray(this.carouselItems) && this.carouselItems.length > 0) {
for(let index=0; index < this.carouselItems.length; index++){
if (this.carouselItems[index] == true){
return index;
}
}
}
return -1;
}
},
methods: {
scrollCalendar(event){
this.scrollTop = this.$refs.calendarContainer.scrollTop;
this.clientHeight = this.$refs.calendarContainer.clientHeight;
},
prev() {
if (!this.queue--)
this.carousel.prev();
},
next() {
if (!this.queue++)
this.carousel.next();
},
slid(evt) {
let dir = evt.direction == 'left' ? 1 : -1;
this.queue -= dir;
this.$emit('slid', dir);
this.offset = (3+this.offset+dir)%3;
if (this.queue) {
if (this.queue > 0)
this.carousel.next();
else
this.carousel.prev();
}
this.carouselItems = this.$refs.carouselItems.map((item) => { return item.classList.contains('active') });
this.slideAnimation = false;
},
slide(evt) {
this.slideAnimation = true;
}
},
mounted() {
if (this.$refs.carousel) {
this.$refs.carousel.children[0].children[1].classList.add('active');
this.carousel = new window.bootstrap.Carousel(this.$refs.carousel, {
interval: false
});
}
this.carouselItems = this.$refs.carouselItems.map((item)=>{return item.classList.contains('active')});
this.scrollTop = this.$refs.calendarContainer.scrollTop;
this.clientHeight = this.$refs.calendarContainer.clientHeight;
},
template: `
<div ref="carousel" class="calendar-pane carousel slide" @[\`slide.bs.carousel\`]="slide" @[\`slid.bs.carousel\`]="slid" :data-queue="queue">
<div id="calendarContainer" @scroll="scrollCalendar" ref="calendarContainer" class="carousel-inner " style="height:var(--fhc-calendar-pane-height);">
<div ref="carouselItems" v-for="i in [...Array(3).keys()]" :key="i" class="carousel-item" style="height:var(--fhc-calendar-pane-height);">
<slot :active="i == activeCarouselItemIndex" :index="i" :offset="offsets[i]" />
</div>
</div>
</div>`
}
-58
View File
@@ -1,58 +0,0 @@
import CalendarAbstract from './Abstract.js';
import CalendarPane from './Pane.js';
import CalendarWeekPage from './Week/Page.js';
export default {
mixins: [
CalendarAbstract
],
components: {
CalendarWeekPage,
CalendarPane
},
computed: {
title() {
return this.focusDate.wYear + ' KW ' + this.focusDate.w;
}
},
methods: {
paneChanged(dir) {
this.focusDate.d += dir * 7;
this.emitRangeChanged();
},
emitRangeChanged() {
let start = this.focusDate.firstDayOfWeek;
let end = this.focusDate.lastDayOfWeek;
this.$emit('change:range', { start, end });
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: 0, m: 0, d: -7 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 0, m: 0, d: 7 });
},
selectEvent(event) {
this.$emit('input', ['select:event',event]);
}
},
created() {
this.emitRangeChanged();
},
template: /*html*/`
<div class="fhc-calendar-week">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'weeks')">
<template #calendarDownloads>
<slot name="calendarDownloads"></slot>
</template>
</calendar-header>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-week-page :year="focusDate.wYear" :week="focusDate.w+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" >
<template #weekPage="{event,day}">
<slot name="weekPage" :event="event" :day="day" ></slot>
</template>
</calendar-week-page>
</calendar-pane>
</div>`
}
-368
View File
@@ -1,368 +0,0 @@
import CalendarDate from '../../../composables/CalendarDate.js';
function ggt(m,n) { return n==0 ? m : ggt(n, m%n); }
function kgv(m,n) { return (m*n) / ggt(m,n); }
export default {
name: 'WeekPage',
data(){
return{
hourPosition:null,
hourPositionTime:null,
resizeObserver: null,
width: 0
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
'events',
'noMonthView',
'isSliding',
'selectedEvent',
'setSelectedEvent',
'rowMinHeight'
],
props: {
year: Number,
week: Number
},
emits: [
'updateMode',
'page:back',
'page:forward',
'input',
],
computed: {
getGridStyle() {
return {
'min-height': this.rowMinHeight,
// this.size is the magic number anyway which directs font-size,
// which in turn influences a lot of layout
width: '42px'
}
},
laneWidth() {
return (this.width - 42) / this.days.length
},
curTime() {
const now = new Date();
return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
},
pageHeaderStyle(){
return {
'z-index': 4,
'grid-template-columns': 'repeat(' + this.days.length + ', 1fr)',
'grid-template-rows': 1,
position: 'sticky',
top: 0,
}
},
overlayStyle() {
return {
'background-color': '#F5E9D7',
'position': 'absolute',
'pointer-events': 'none',
'z-index': 2,
height: this.getDayTimePercent + '%',
width: this.laneWidth + 'px',
opacity: 0.5
}
},
indicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '3.5rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.hourPosition + 'px',
left: 0,
right: 0,
}
},
hours(){
// returns an array with elements starting at 7 and ending at 24
return [...Array(24).keys()].filter(hour => hour >= 7 && hour <= 24);
},
dayText() {
if (!this.size || !this.days) return {};
let dayTextMap ={};
this.days.forEach((day)=>{
dayTextMap[day] = {
heading: day.toLocaleString(this.$p.user_locale.value, { dateStyle: 'short' }),
tag: day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 2 ? 'narrow' : (this.size < 3 ? 'short' : 'long') }),
datum: day.toLocaleString(this.$p.user_locale.value, [{ day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { dateStyle: 'short' }][this.size]),
};
});
return dayTextMap;
},
days() {
let tmpDate = new CalendarDate(this.year,1,1); // NOTE(chris): somewhere in the middle of the year
tmpDate.w = this.week;
let startDay = tmpDate.firstDayOfWeek;
let result = [];
for (let i = 0; i < 7; i++) {
result.push(new Date(startDay.getFullYear(), startDay.getMonth(), startDay.getDate() + i));
}
return result;
},
eventsPerDayAndHour() {
// return early if the calendar pane is sliding
if (this.isSliding) return {};
const res = {};
this.days.forEach(day => {
let key = day.toDateString();
let nextDay = new Date(day);
nextDay.setDate(nextDay.getDate()+1);
nextDay.setMilliseconds(nextDay.getMilliseconds()-1);
let d = {events:[],lanes:1, isPast: false};
d.isPast = nextDay.getTime() < this.today
d.isToday = nextDay.getFullYear() === this.todayDate.getFullYear() && nextDay.getMonth() === this.todayDate.getMonth() && nextDay.getDate() === this.todayDate.getDate()
if (this.events[key]) {
this.events[key].forEach(evt => {
let event = {orig:evt,lane:1,maxLane:1,start: evt.start < day ? day : evt.start, end: evt.end > nextDay ? nextDay : evt.end,shared:[],setSharedMaxRecursive(doneItems) {
this.maxLane = Math.max(doneItems[0].maxLane, this.maxLane);
doneItems.push(this);
this.shared.filter(other => !doneItems.includes(other)).forEach(i => i.setSharedMaxRecursive(doneItems));
}};
event.shared = d.events.filter(other => other.start < event.end && other.end > event.start);
event.shared.forEach(other => other.shared.push(event));
let occupiedLanes = event.shared.map(other => other.lane);
while (occupiedLanes.includes(event.lane))
event.lane++;
event.maxLane = Math.max(...[event.lane], ...occupiedLanes);
if (event.maxLane > 1) {
event.setSharedMaxRecursive([event]);
}
d.events.push(event);
});
d.lanes = d.events.map(e => e.maxLane).reduce((res, i) => kgv(res, i), 1);
}
res[key] = d;
});
return res;
},
smallestTimeFrame() {
return [30,15,10,5][this.size];
},
lookingAtToday() {
return this.days.some(d =>
d.getFullYear() === this.todayDate.getFullYear() &&
d.getMonth() === this.todayDate.getMonth() &&
d.getDate() === this.todayDate.getDate()
)
},
curIndicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '1rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.getDayTimePercent + '%',
width: this.laneWidth + 'px'
}
},
getDayTimePercent() {
const now = new Date(Date.now())
const currentMinutes = now.getMinutes() + now.getHours() * 60
let timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
return timePercentage
}
},
methods: {
hourGridIdentifier(hour) {
// this is the id attribute that is responsible to scroll the calender to the first event
return 'scroll' + hour + this.focusDate.d + this.week;
},
hourGridStyle(hour) {
return {
'pointer-events': 'none',
top: this.getAbsolutePositionForHour(hour),
left: 0,
right: 0,
'z-index': 0
}
},
dayGridStyle(day) {
const styleObj = {
'grid-template-columns': 'repeat(' + day.lanes + ', 1fr)',
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
}
if(day.isPast) {
styleObj['background-color'] = '#F5E9D7'
styleObj['border-color'] = '#E8E8E8';
styleObj.opacity = 0.5;
} else if (day.isToday) {
// styleObj['backgroundImage'] = 'linear-gradient(to bottom, #F5E9D7 '+this.getDayTimePercent+'%, #FFFFFF '+this.getDayTimePercent+'%)'
// styleObj['border-color'] = '#E8E8E8';
// styleObj.opacity = 0.5;
}
return styleObj
},
eventGridStyle(day, event) {
return {
'z-index': 1,
'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane,
'grid-column-end': 1 + event.lane * day.lanes / event.maxLane,
'grid-row-start': this.dateToMinutesOfDay(event.start),
'grid-row-end': this.dateToMinutesOfDay(event.end),
'background-color': event.orig.color,
'max-height': '75px'
}
},
calcHourPosition(event) {
let height = this.$refs.eventcontainer.getBoundingClientRect().height;
let top = this.$refs.eventcontainer.getBoundingClientRect().top;
let position = event.clientY - top;
// position percentage of total height
let timePercentage = (position / height) * 100;
// minute percentage of total minutes
let result = (this.hours.length * 60) * (timePercentage / 100);
// calculate time in float
let currentMinutes = ((result + (this.hours[0] * 60)) / 60);
// get hour part of time
let currentHour = Math.floor(currentMinutes);
// get float part of time
let minutePercentage = currentMinutes % currentHour;
// calculate minutes from float part of time
let minute = Math.round(60 * minutePercentage);
// convert minutes to 5 minute interval
if (minute % 5 != 0) {
minute = Math.round(minute / 5) * 5;
}
// in case the rounding made the minutes 60, increase the hour and reset the minutes
if (minute == 60) {
currentHour++;
minute = 0;
}
// ## after rounding the time to the nearest 5 Minute interval, we have to convert the time back to the relative position
// convert current time in minutes
currentMinutes = currentHour * 60 + minute;
// calculate the minutes percentage of the total minutes
timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
// calculate the relative position of the time percentage
position = height * (timePercentage / 100);
this.hourPosition = position;
// add padding to minutes that consist of only one digit
minute.toString().length == 1 ? minute = "0" + minute : minute;
this.hourPositionTime = currentHour + ":" + minute;
},
getAbsolutePositionForHour(hour){
// used for the absolute positioning of the gutters of hours
return (100 / this.hours.length) * (hour - (24-this.hours.length)) + '%';
},
changeToMonth(day) {
if (!this.noMonthView) {
this.date.set(day);
this.focusDate.set(day);
this.$emit('updateMode', 'month');
}
},
changeToDay(day) {
this.date.set(day);
this.focusDate.set(day);
this.$emit('updateMode', 'day');
},
dateToMinutesOfDay(day) {
// subtract 7 from the total hours because the hours range from 7 to 24
return Math.floor(((day.getHours()-7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
},
weekPageClick(event, day) {
this.setSelectedEvent(event);
this.focusDate.set(new CalendarDate(new Date(event.datum)));
this.$emit('input', event)
},
initResizeObserver() {
const events = this.$refs['eventsRef'+this.week];
if (!events) return;
this.resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
if(width > 0) this.width = width
}
});
this.resizeObserver.observe(events);
},
destroyResizeObserver() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
},
},
mounted() {
setTimeout(() => this.$refs.eventcontainer.scrollTop = this.$refs.eventcontainer.scrollHeight / 3 + 1, 0);
const container = document.getElementById("calendarContainer")
if(container) {
container.style['overflow-y'] = 'scroll'
container.style['overflow-x'] = 'auto'
}
this.initResizeObserver();
},
beforeUnmount() {
this.destroyResizeObserver();
},
template: /*html*/`
<div ref="page" class="fhc-calendar-week-page" style="min-width: 700px;">
<div class="d-flex flex-column">
<div class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle" >
<div type="button" v-for="day in days" :key="day" class="flex-grow-1" :title="dayText[day]?.heading" @click.prevent="changeToDay(day)">
<div class="fw-bold">{{dayText[day]?.tag}}</div>
<a href="#" class="small text-secondary text-decoration-none" >{{dayText[day]?.datum}}</a>
</div>
</div>
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="hourPosition = null" >
<div :id="hourGridIdentifier(hour)" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border-top" :style="hourGridStyle(hour)"></div>
<Transition>
<div v-if="hourPosition" class="position-absolute border-top small" :style="indicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
</div>
</Transition>
<div class="events" :ref="'eventsRef'+week">
<div class="hours">
<div v-for="hour in hours" :style="getGridStyle" :key="hour" class="text-muted text-end small" :ref="'hour' + hour">{{hour}}:00</div>
</div>
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
<Transition>
<div v-if="day.isToday" class="position-absolute border-top small" :style="curIndicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{curTime}}</span>
</div>
</Transition>
<div v-if="day.isToday" :style="overlayStyle"></div>
<div v-for="event in day.events" :key="event" @click.prevent="weekPageClick(event.orig, day)"
:selected="event.orig == selectedEvent"
:style="eventGridStyle(day,event)"
class="mx-2 small rounded overflow-hidden fhc-entry "
v-contrast >
<slot name="weekPage" :event="event" :day="day">
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
</slot>
</div>
</div>
</div>
</div>
</div>
</div>`
}
-45
View File
@@ -1,45 +0,0 @@
import CalendarAbstract from './Abstract.js';
export default {
mixins: [
CalendarAbstract
],
inject: [
'size',
'focusDate'
],
computed: {
weeks(){
return [...Array(this.focusDate.numWeeks).keys()].map(i => i + 1);
},
title() {
return this.focusDate.format({year: 'numeric'});
}
},
methods: {
setWeek(week) {
// TODO(chris): test is there a week jump on year select? => yes there is if the same month/day are in different weeks ... should we prevent that?
this.focusDate.w = week;
this.$emit('updateMode', 'week');
},
prev(){
this.focusDate.y--;
this.focusDate._clean();
},
next() {
this.focusDate.y++;
this.focusDate._clean();
},
},
template: `
<div class="fhc-calendar-weeks h-100">
<calendar-header :title="title" @prev="prev" @next="next" @click="$emit('updateMode', 'years')" @updateMode="$emit('updateMode', $event)" />
<div class="d-flex flex-wrap">
<div v-for="(week, key) in weeks" :key="key" class="d-grid col-2">
<button @click="setWeek(week)" class="btn btn-outline-secondary" :class="{'border-0': week != focusDate.w}">
{{week}}
</button>
</div>
</div>
</div>`
}
+106
View File
@@ -0,0 +1,106 @@
import FhcCalendar from "./Base.js";
import { useEventLoader } from '../../composables/EventLoader.js';
import ModeList from '../Calendar/Mode/List.js';
export default {
name: "CalendarWidget",
components: {
FhcCalendar
},
inject: [
"renderers"
],
props: {
timezone: {
type: String,
required: true
},
getPromiseFunc: {
type: Function,
required: true
}
},
data() {
return {
now: luxon.DateTime.now().setZone(this.timezone),
modes: {
list: Vue.markRaw(ModeList)
},
modeOptions: {
list: {
length: 7
}
}
};
},
methods: {
eventStyle(event) {
const styles = {};
if (event.farbe)
styles['--event-bg'] = '#' + event.farbe;
else if (event.type == 'reservierung')
styles['--event-bg'] = '#ffffff';
else
styles['--event-bg'] = '#cccccc';
const eventEnd = luxon.DateTime.fromISO(event.isoend, { zone: this.timezone });
if (eventEnd < this.now)
styles['opacity'] = .5;
return styles;
},
updateRange(rangeInterval) {
this.rangeInterval = rangeInterval;
}
},
setup(props) {
const rangeInterval = Vue.ref(null);
const { events } = useEventLoader(rangeInterval, props.getPromiseFunc);
return {
rangeInterval,
events
};
},
template: /* html */`
<fhc-calendar
:modes="modes"
:mode-options="modeOptions"
:timezone="timezone"
:locale="$p.user_locale.value"
:events="events || []"
@update:range="updateRange"
>
<template v-slot="{ event, mode }">
<div
v-if="!event"
class="h-100 d-flex justify-content-center align-items-center"
>
{{ $p.t('lehre/noLvFound') }}
</div>
<component
v-else-if="mode == 'eventheader'"
:is="renderers[event.type]?.modalTitle"
:event="event"
></component>
<component
v-else-if="mode == 'event'"
:is="renderers[event.type]?.modalContent"
:event="event"
></component>
<div
v-else
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
:style="eventStyle(event)"
>
<component
:is="renderers[event.type]?.calendarEvent"
:event="event"
></component>
</div>
</template>
</fhc-calendar>`
}
-61
View File
@@ -1,61 +0,0 @@
import CalendarAbstract from './Abstract.js';
import CalendarPane from './Pane.js';
import CalendarYearsPage from './Years/Page.js';
export default {
mixins: [
CalendarAbstract
],
components: {
CalendarYearsPage,
CalendarPane
},
inject: [
'size'
],
data() {
return {
start: 0
}
},
computed: {
range() {
switch (this.size) {
case 3:
// eslint-disable-next-line
case 2:
return 24;
}
return 12;
},
end() {
return this.start + this.range - 1;
},
title() {
return this.start + ' - ' + this.end;
}
},
methods: {
paneChanged(dir) {
this.start += this.range * dir;
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: -1, m: 0, d: 0 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 1, m: 0, d: 0 });
}
},
created() {
this.start = this.focusDate.y - this.focusDate.y%this.range;
},
template: `
<div class="fhc-calendar-years">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" />
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-years-page :data-test="slot.index" :start="start+range*slot.offset" :end="start+range*slot.offset+range" @updateMode="$emit('updateMode')"/>
</calendar-pane>
</div>`
}
@@ -1,36 +0,0 @@
export default {
inject: [
'focusDate'
],
props: {
start: Number,
end: Number
},
emits: [
'updateMode'
],
data() {
return {
}
},
computed: {
years() {
return [...Array(this.end - this.start).keys()].map(i => i + this.start);
}
},
mounted() {
const container = document.getElementById("calendarContainer")
if(container) {
container.style['overflow-y'] = 'scroll'
container.style['overflow-x'] = 'auto'
}
},
template: `
<div class="fhc-calendar-years-page d-flex flex-wrap">
<div v-for="year in years" :key="year" class="d-grid col-4">
<button class="btn btn-outline-secondary" :class="{'border-0': year != focusDate.y}" @click="focusDate.y = year; $emit('updateMode')">
{{year}}
</button>
</div>
</div>`
}
@@ -0,0 +1,315 @@
import BsModal from '../../Bootstrap/Modal.js';
import VueDatePicker from '../../vueDatepicker.js.php';
const today = new Date()
export const AbgabeMitarbeiterDetail = {
name: "AbgabeMitarbeiterDetail",
components: {
BsModal,
InputNumber: primevue.inputnumber,
Checkbox: primevue.checkbox,
Dropdown: primevue.dropdown,
Textarea: primevue.textarea,
VueDatePicker
},
props: {
projektarbeit: {
type: Object,
default: null
}
},
data() {
return {
oldPaBeurteilungLink: 'https://moodle.technikum-wien.at/mod/page/view.php?id=1005052', // TODO: inject from app & app provide link from config
eidAkzeptiert: false,
enduploadTermin: null,
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
// TODO: fetch types
allAbgabeTypes: [
{
paabgabetyp_kurzbz: 'abstract',
bezeichnung: 'Entwurf'
},
{
paabgabetyp_kurzbz: 'zwischen',
bezeichnung: 'Zwischenabgabe'
},
{
paabgabetyp_kurzbz: 'note',
bezeichnung: 'Benotung'
},
{
paabgabetyp_kurzbz: 'end',
bezeichnung: 'Endupload'
},
{
paabgabetyp_kurzbz: 'enda',
bezeichnung: 'Endabgabe im Sekretariat'
}
]
}
},
methods: {
openZusatzdatenModal(termin) {
},
saveTermin(termin) {
const paabgabe_id = termin.paabgabe_id
this.$fhcApi.factory.lehre.postProjektarbeitAbgabe(termin).then( (res) => {
if(res?.meta?.status == 'success') {
this.$fhcAlert.alertSuccess(this.$p.t('ui/gespeichert'))
if(paabgabe_id === -1) { // new abgabe has been inserted
termin.paabgabe_id = res?.data?.retval
this.projektarbeit.abgabetermine.push({ // new abgatermin row
'paabgabe_id': -1,
'projektarbeit_id': this.projektarbeit.projektarbeit_id,
'fixtermin': false,
'kurzbz': '',
'datum': new Date().toISOString().split('T')[0],
'paabgabetyp_kurzbz': '',
'bezeichnung': '',
'abgabedatum': null,
'insertvon': this.viewData?.uid ?? '',
'allowedToSave': true,
'allowedToDelete': true
})
}
} else if(res?.meta?.status == 'error'){
this.$fhcAlert.alertError()
}
})
},
deleteTermin(termin) {
this.$fhcApi.factory.lehre.deleteProjektarbeitAbgabe(termin.paabgabe_id).then( (res) => {
if(res?.meta?.status == 'success') {
this.$fhcAlert.alertSuccess(this.$p.t('ui/genericDeleted', [this.$p.t('abgabetool/abgabe')]))
// this.$p.t('global/tooltipLektorDeleteKontrolle', [this.$entryParams.permissions.kontrolleDeleteMaxReach ])
const deletedTerminIndex = this.projektarbeit.abgabetermine.findIndex(t => t.paabgabe_id === termin.paabgabe_id)
this.projektarbeit.abgabetermine.splice(deletedTerminIndex, 1)
} else if(res?.meta?.status == 'error'){
this.$fhcAlert.alertError()
}
})
},
validate: function(termin) {
if(!termin.file.length) {
this.$fhcAlert.alertWarning(this.$p.t('global/warningChooseFile'));
return false
}
return true;
},
downloadAbgabe(termin) {
this.$fhcApi.factory.lehre.getStudentProjektarbeitAbgabeFile(termin.paabgabe_id, this.projektarbeit.student_uid)
},
dateDiffInDays(datum, today){
const oneDayMs = 1000 * 60 * 60 * 24
return Math.round((new Date(datum) - new Date(today)) / oneDayMs)
},
getDateStyle(termin) {
const datum = new Date(termin.datum)
const abgabedatum = new Date(termin.abgabedatum)
// todo: rework styling but keep the color pattern logic
// https://wiki.fhcomplete.info/doku.php?id=cis:abgabetool_fuer_studierende
let color = 'white'
let fontColor = 'black'
if (termin.abgabedatum === null) {
if(datum < today) {
color = 'red'
fontColor = 'white'
} else if (datum > today && this.dateDiffInDays(datum, today) <= 12) {
color = 'yellow'
}
} else if(abgabedatum > datum) {
color = 'pink' // aka "hellrot"
fontColor = 'white'
} else {
color = 'green'
}
return `font-color: ${fontColor} ; background-color: ${color}; border-radius: 50%;`
},
openBeurteilungLink(link) {
window.open(link, '_blank')
},
getOptionLabelSprache(option) {
return option.sprache
},
getOptionLabelAbgabetyp(option){
return option.bezeichnung
},
formatDate(dateParam) {
const date = new Date(dateParam)
// handle missing leading 0
const padZero = (num) => String(num).padStart(2, '0');
const month = padZero(date.getMonth() + 1); // Months are zero-based
const day = padZero(date.getDate());
const year = date.getFullYear();
return `${day}.${month}.${year}`;
},
openStudentPage() {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router
+ '/Cis/Abgabetool/Student/' + this.projektarbeit?.student_uid
window.open(link, '_blank')
},
openPlagiatcheck() {
// todo: hardcoded turnitin link?
const link = "https://technikum-wien.turnitin.com/sso/sp/redwood/saml/5IyfmBr2OcSIaWQTKlFCGj/start"
window.open(link, '_blank')
},
openBenotung() {
const path = this.projektarbeit?.betreuerart_kurzbz == 'Zweitbegutachter' ? 'ProjektarbeitsbeurteilungZweitbegutachter' : 'ProjektarbeitsbeurteilungErstbegutachter'
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/' + path
window.open(link, '_blank')
}
},
computed: {
getEid() {
return this.$p.t('abgabetool/c4eidesstattlicheErklaerung')
},
getEnduploadErlaubt() {
return !this.eidAkzeptiert
},
getSemesterBenotbar(){
return this.projektarbeit?.isCurrent ?? false
},
endUploadVorhanden(){
return this.projektarbeit?.abgabetermine.find(abgabe => abgabe.paabgabetyp_kurzbz === 'end' && abgabe.abgabedatum !== null)
}
},
created() {
},
mounted() {
},
template: `
<div v-if="projektarbeit">
<h5>{{$p.t('abgabetool/c4abgabeMitarbeiterbereich')}}</h5>
<div class="row">
<div class="col-8">
<p> {{projektarbeit?.student}}</p>
<p> {{projektarbeit?.titel}}</p>
<p v-if="projektarbeit?.zweitbegutachter"> {{projektarbeit?.zweitbegutachter}}</p>
</div>
<div class="col-4 d-flex">
<div class="col">
<div class="row">
<button :disabled="!getSemesterBenotbar || !endUploadVorhanden" class="btn btn-secondary border-0" @click="openBenotung" style="width: 80%;">
benoten
<i style="margin-left: 8px" class="fa-solid fa-user-check"></i>
</button>
</div>
<div class="row" style="width: 90%;">
<span v-if="!getSemesterBenotbar" v-html="$p.t('abgabetool/c4aeltereParbeitBenoten', oldPaBeurteilungLink)"></span>
<span v-else-if="!endUploadVorhanden">Kein Endupload vorhanden!</span>
</div>
</div>
<div class="col">
<div class="row">
<button v-if="projektarbeit?.betreuerart_kurzbz !== 'Zweitbegutachter'" class="btn btn-secondary border-0" @click="openPlagiatcheck" style="width: 80%;">
zur Plagiatsprüfung
<i style="margin-left: 8px" class="fa-solid fa-user-check"></i>
</button>
</div>
</div>
<div class="col">
<div class="row">
<button class="btn btn-secondary border-0" @click="openStudentPage" style="width: 80%;">
Studentenansicht
<i style="margin-left: 8px" class="fa-solid fa-eye"></i>
</button>
</div>
</div>
</div>
</div>
<div id="uploadWrapper">
<div class="row" style="margin-bottom: 12px;">
<div style="width: 100px">{{$p.t('abgabetool/c4fixtermin')}}</div>
<div class="col-2">{{$p.t('abgabetool/c4zieldatum')}}</div>
<div class="col-2">{{$p.t('abgabetool/c4abgabetyp')}}</div>
<div class="col-4">{{$p.t('abgabetool/c4abgabekurzbz')}}</div>
<div class="col-1">{{$p.t('abgabetool/c4abgabedatum')}}</div>
<div class="col">
</div>
</div>
<div v-if="!projektarbeit?.abgabetermine?.length">keine Termine gefunden!</div>
<div class="row" v-for="termin in projektarbeit.abgabetermine">
<div style="width: 100px" class="d-flex justify-content-center align-items-center">
<p class="fhc-bullet" :class="{ 'fhc-bullet-red': termin.fixtermin, 'fhc-bullet-green': !termin.fixtermin }"></p>
</div>
<div class="col-2 d-flex justify-content-center align-items-center">
<div :style="getDateStyle(termin)">
<VueDatePicker
style="width: 95%;"
v-model="termin.datum"
:clearable="false"
:disabled="!termin.allowedToSave"
:enable-time-picker="false"
:format="formatDate"
:text-input="true"
auto-apply>
</VueDatePicker>
</div>
</div>
<div class="col-2 d-flex justify-content-center align-items-center">
<Dropdown
:style="{'width': '100%'}"
:disabled="!termin.allowedToSave"
v-model="termin.bezeichnung"
:options="allAbgabeTypes"
:optionLabel="getOptionLabelAbgabetyp">
</Dropdown>
</div>
<div class="col-4 d-flex justify-content-center align-items-center">
<Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" rows="3" cols="60" :disabled="!termin.allowedToSave"></Textarea>
</div>
<div class="col-1 d-flex justify-content-center align-items-center">
{{ termin.abgabedatum?.split("-").reverse().join(".") }}
<a v-if="termin?.abgabedatum" @click="downloadAbgabe(termin)" style="margin-left:4px; cursor: pointer;">
<i class="fa-solid fa-file-pdf"></i>
</a>
</div>
<div class="col-2 align-content-center">
<div class="row">
<div class="col-6">
<button v-if="termin.allowedToSave" class="btn btn-primary border-0" @click="saveTermin(termin)">
Speichern
<i style="margin-left: 8px" class="fa-solid fa-floppy-disk"></i>
</button>
</div>
<div class="col-6">
<button v-if="termin.allowedToDelete && termin.paabgabe_id > 0" class="btn btn-primary border-0" @click="deleteTermin(termin)">
Löschen
<i style="margin-left: 8px" class="fa-solid fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`,
};
export default AbgabeMitarbeiterDetail;

Some files were not shown because too many files have changed in this diff Show More