Compare commits

..
Author SHA1 Message Date
adisposkofh 4ecf1845d4 resolved calendar range view rangeLength prop type inconsistency 2026-09-09 15:20:58 +02:00
adisposkofh 07d04d5701 Merge branch 'master' into feature-78496_lv-plan-range-view 2026-09-07 11:25:12 +02:00
adisposkofh 3c373aac25 fixed error in base calendar with inconsistent date updates 2026-09-07 11:19:39 +02:00
adisposkofh 4e2254dfc7 fixed error with missing btnRange prop in base calendar 2026-09-07 11:18:40 +02:00
Andreas Österreicher 658c8d6bd4 Schreibberechtigungsprüfung bei Anrechnungen angepasst damit diese korrekt angezeigt wird wenn oe_kurzb nicht gleich der Kurzbz des Studiegangs ist 2026-09-03 08:44:10 +02:00
adisposkofh e976c825c9 removed redundant classes 2026-09-02 16:05:44 +02:00
adisposkofh 88b5a0af46 lv plan range view print css 2026-09-02 16:04:44 +02:00
Andreas Österreicher bf29faa5a7 Merge branch 'feature-82292/Cis4StudiumLVohneModulAnzeigen' 2026-09-02 15:20:28 +02:00
Andreas Österreicher 2ada167557 Merge branch 'bug-82614/StudVw_International_Incoming_Outgoing_GS_wird_nicht_geladen' 2026-09-02 14:56:27 +02:00
adisposkofh a53551b462 lv plan range view print css 2026-09-02 13:01:13 +02:00
adisposkofh 989ce6d707 limit lv plan range view to personal lv plan page 2026-09-01 13:11:43 +02:00
adisposkofh 018169b06a lv plan range view styling 2026-09-01 13:00:42 +02:00
chfhtw cc9d1f983e bugfix: missing call to getStudiensemesterKurzbz 2026-09-01 10:39:02 +02:00
adisposkofh 0416a2ce7d lv plan range view styling 2026-08-31 14:11:51 +02:00
adisposkofh dde95c8e01 lv plan range view styling 2026-08-31 14:03:12 +02:00
adisposkofh 7260a0a00e added semester date presets on lv plan range view 2026-08-29 17:11:31 +02:00
adisposkofh 5cfa398ea6 implemented lv plan range view 2026-08-29 15:46:03 +02:00
Johann Hoffmann 57c48f2c4a cis4 studium page dont filter away lehrveranstaltungen without a module and give them their own box in the ui; 2026-08-26 13:15:16 +02:00
23 changed files with 622 additions and 142 deletions
@@ -167,7 +167,9 @@ class Studium extends FHCAPI_Controller
if($aktuelles_studienplan){
$lehrveranstaltungen = $this->computeStudienplanLehrveranstaltungen($aktuelles_studienplan->studienplan_id, $aktuelles_semester);
foreach($lehrveranstaltungen as $lehrv){
foreach($lehrv->lehrveranstaltungen as $lv){
// a lehrveranstaltung without a modul has no children and gets its lektoren directly
$lvs = isset($lehrv->lehrveranstaltungen) ? $lehrv->lehrveranstaltungen : [$lehrv];
foreach($lvs as $lv){
$lvLektoren =$this->computeLektorenFromLehrveranstaltung($lv->lehrveranstaltung_id,$aktuelles_semester, $aktuelles_studiengang->studiengang_kz, $aktuelles_studiensemester->studiensemester_kurzbz);
$lv->lektoren = $lvLektoren;
}
@@ -286,6 +288,7 @@ SELECT tbl_lehrveranstaltung.*,
$lehrveranstaltungen = $this->LehrveranstaltungModel->execReadOnlyQuery($query,[$studienplan_id, $semester]);
$lehrveranstaltungen = $this->getDataOrTerminateWithError($lehrveranstaltungen);
usort($lehrveranstaltungen, function($a, $b){
if($a->lehrtyp_kurzbz == "modul"){
return -1;
@@ -302,12 +305,17 @@ SELECT tbl_lehrveranstaltung.*,
}
else{
$parent =array_filter($carry, function($item)use($lehrv){
return $item->studienplan_lehrveranstaltung_id == $lehrv->studienplan_lehrveranstaltung_id_parent;
return isset($item->lehrveranstaltungen)
&& $item->studienplan_lehrveranstaltung_id == $lehrv->studienplan_lehrveranstaltung_id_parent;
});
$parent = current($parent);
if($parent){
$parent->lehrveranstaltungen[] = $lehrv;
}
else{
// lehrveranstaltung without a modul: keep it on the top level
array_push($carry, $lehrv);
}
}
return $carry;
}, []);
@@ -60,8 +60,8 @@ class LvTermine extends FHCAPI_Controller
$semester_range,
$benutzer_gruppen,
$student_lehrverband,
$dbStundenplanTable,
true
true,
$dbStundenplanTable
);
if(!$stundenplan_query)
@@ -108,6 +108,7 @@ class Students extends FHCAPI_Controller
'studiensemester_kurzbz' => $studiensemester_kurzbz
]);
$studiensemester_kurzbz = $this->getStudiensemesterKurzbz($studiensemester_kurzbz);
$this->studentlistlib->addJoin(
"(
@@ -153,7 +154,8 @@ class Students extends FHCAPI_Controller
$this->addMeta('ci_params', [
'studiensemester_kurzbz' => $studiensemester_kurzbz
]);
$studiensemester_kurzbz = $this->getStudiensemesterKurzbz($studiensemester_kurzbz);
$this->studentlistlib->addJoin(
"(
@@ -213,6 +215,7 @@ class Students extends FHCAPI_Controller
'studiensemester_kurzbz' => $studiensemester_kurzbz
]);
$studiensemester_kurzbz = $this->getStudiensemesterKurzbz($studiensemester_kurzbz);
$this->studentlistlib->addJoin(
"(
+15
View File
@@ -82,6 +82,21 @@ function generateCSSsInclude($CSSs)
}
}
/**
* Generates tags for the style sheets you want to include, the parameter could by a string or an array of strings
*/
function generatePrintCSS($path)
{
$cssLink = '<link rel="stylesheet" media="print" href="%s" />';
$ci =& get_instance();
$cachetoken = '?'.$ci->config->item('fhcomplete_build_version');
if (isset($path)) {
echo sprintf($cssLink, base_url($path).$cachetoken) . PHP_EOL;
}
}
/**
* Generates global JS-Object to pass parms to other javascripts
*/
@@ -532,12 +532,12 @@ class Stundenplan_model extends DB_Model
/**
* NO STANDALONE FUNCTION - Generates a SQL query string to fetch 'stundenplan' events for a specific student within the current semester.
*
* @param showLvsStundenplan if Lvs of stundenplan should be shown
* @param isLvList if condition needed for Tab LV Termine is given
* @param db_stpl_table enables switch to db 'stundenplandev'
*
* @return mixed
*/
public function getStundenplanQuery($start_date, $end_date, $semester, $gruppen, $studentlehrverbaende, $db_stpl_table='stundenplan', $showLvsStundenplan=false){
public function getStundenplanQuery($start_date, $end_date, $semester, $gruppen, $studentlehrverbaende, $isLvList=false, $db_stpl_table='stundenplan'){
// helper function to check if either $gruppen or $studentlehrverbaende are empty for each semester
$emptyCheck = function($toBeCheckedArray) use ($semester){
@@ -559,17 +559,17 @@ class Stundenplan_model extends DB_Model
}
$query =
"select sp.*
"select sp.*
from lehre.vw_".$db_stpl_table." sp
WHERE
sp.datum >= ".$this->escape($start_date)."
AND sp.datum <= ".$this->escape($end_date);
// adds the AND sql chain only if both $gruppen and $studentlehrverbaende are not empty
if(!$emptyCheck($gruppen) || !$emptyCheck($studentlehrverbaende))
{
$query .= " AND ( ";
}
}
foreach($semester as $sem => $semester_date_range)
{
@@ -583,10 +583,10 @@ class Stundenplan_model extends DB_Model
}
// converts the array of gruppen strings into a sql IN (_,_,_) chain
$query .="(sp.gruppe_kurzbz IN (" .implode(',',$gruppen[$sem_date]).") AND sp.datum BETWEEN ".$this->escape($sem_date_range->start)." AND ".$this->escape($sem_date_range->ende)." )";
$query .="OR";
}
}
}
// if there are no studentlehrverbaende and the gruppen are not empty, we can remove the last OR added after the groups
if($emptyCheck($studentlehrverbaende) && !$emptyCheck($gruppen))
@@ -594,15 +594,27 @@ class Stundenplan_model extends DB_Model
$query = substr($query, 0, -2);
}
//Condition for showLVList FHC4
if(!$isLvList)
$stringGroupLv = "AND gruppe_kurzbz is null";
else
$stringGroupLv ="";
foreach($semester as $sem=>$semester_date_range)
{
foreach($semester_date_range as $sem_date => $sem_date_range)
{
if(!array_key_exists($sem_date,$studentlehrverbaende) || count($studentlehrverbaende[$sem_date]) == 0)
if(!array_key_exists($sem,$studentlehrverbaende) || count($studentlehrverbaende[$sem]) == 0)
{
continue;
}
foreach($studentlehrverbaende[$sem_date] as $key=>$lehrverband)
$studlvbds = array_filter(
$studentlehrverbaende[$sem],
function($value) use ($sem_date) {
return $value->studiensemester_kurzbz === $sem_date;
}
);
foreach($studlvbds as $key=>$lehrverband)
{
$query .= "(((sp.studiengang_kz = ".$this->escape($lehrverband->studiengang_kz)." AND sp.semester = ".$this->escape($lehrverband->semester)." AND sp.verband = ".$this->escape($lehrverband->verband)." AND sp.gruppe = ".$this->escape($lehrverband->gruppe)." AND sp.datum BETWEEN ".$this->escape($sem_date_range->start)." AND ".$this->escape($sem_date_range->ende).")";
// Eintraege fuer den ganzen Verband
@@ -611,24 +623,9 @@ class Stundenplan_model extends DB_Model
$query .= "OR (sp.studiengang_kz = ".$this->escape($lehrverband->studiengang_kz)." AND sp.semester = ".$this->escape($lehrverband->semester)." AND (sp.verband is null OR sp.verband='') AND sp.datum BETWEEN ".$this->escape($sem_date_range->start)
." AND ".$this->escape($sem_date_range->ende).")) AND gruppe_kurzbz is null)";
// Eintraege vom Stundenplan
if($showLvsStundenplan)
{
$query .= "
OR EXISTS (
SELECT 1
FROM lehre.tbl_stundenplan tsp
WHERE tsp.lehreinheit_id = sp.lehreinheit_id
AND tsp.studiengang_kz = " . $this->escape($lehrverband->studiengang_kz) . "
AND (tsp.semester = " . $this->escape($lehrverband->semester) . " OR tsp.semester IS NULL)
AND (tsp.verband = " . $this->escape($lehrverband->verband) . " OR tsp.verband IS NULL OR tsp.verband = '0' OR tsp.verband = '')
AND (tsp.gruppe = " . $this->escape($lehrverband->gruppe) . " OR tsp.gruppe IS NULL OR tsp.gruppe = '0' OR tsp.gruppe = '')
AND tsp.gruppe_kurzbz IS NULL)";
}
$query .= "OR";
$query .="OR";
}
}
}
}
// if the studentlehrverbaende is not empty we can remove the last OR that was added to the query
@@ -641,7 +638,7 @@ class Stundenplan_model extends DB_Model
if(!$emptyCheck($gruppen) || !$emptyCheck($studentlehrverbaende))
{
$query .= ")";
}
}
return $query;
}
@@ -16,7 +16,7 @@ $query = '
anrechnung.begruendung_id,
anrechnung.dms_id,
CASE
WHEN stg.typ || stg.kurzbz IN (' . $ORGANISATIONSEINHEITEN_SCHREIBBERECHTIGT . ') THEN TRUE
WHEN stg.oe_kurzbz IN (' . $ORGANISATIONSEINHEITEN_SCHREIBBERECHTIGT . ') THEN TRUE
ELSE FALSE
END "schreibberechtigt",
anrechnung.studiensemester_kurzbz,
@@ -7,6 +7,7 @@ $includesArray = array(
'fontawesome6' => true,
'axios027' => true,
'primevue3' => true,
'printCssPath' => 'public/css/Cis4/print.css',
'customJSModules' => array_merge([
'public/js/apps/Cis/Menu.js'
], $customJSModules ?? []),
@@ -12,6 +12,8 @@
$customJSs = isset($customJSs) ? $customJSs : null;
$customJSModules = isset($customJSModules) ? $customJSModules : null;
$skipID = isset($skipID) ? $skipID : null;
$printCssPath = isset($printCssPath) ? $printCssPath : null;
?>
<!-- Header start -->
@@ -138,6 +140,9 @@
generateCSSsIncludeIfExtensionCssExists('tags.css');
}
// Print CSS
if ($printCssPath) generatePrintCSS($printCssPath);
$extapphelper = ExtendableAppsHelper::getInstance();
$extapphelper->init($customCSSs, $customJSs, $customJSModules);
+9
View File
@@ -59,12 +59,21 @@
padding: .25em .5em .25em .25em;
margin-inline-end: .25rem;
}
.rangePageContainer .calendar-event-default .event-time {
display: none !important;
}
.weekPageContainer .calendar-event-default .event-text {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
}
.rangePageContainer .calendar-event-default .event-text {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: start;
}
.dayPageContainer .calendar-event-default,
.listPageContainer .calendar-event-default {
display: flex;
+51
View File
@@ -0,0 +1,51 @@
@page {
size: landscape;
}
* {
background-color: white !important;
color: black !important;
font-size: x-small !important;
overflow: visible !important;
}
.fhc-calendar-base-slider > div {
position: relative !important;
}
header,
#skiplink,
#cis-lvplan-personal-heading,
.cis-lvplan-personal hr,
.fhc-calendar-base-header,
.fhc-calendar-mode-range .grid-allday,
.fhc-calendar-mode-range #rangePresetSelector,
.fhc-calendar-base-grid-line-background {
display: none !important;
}
.fhc-calendar-mode-range .main-header-sunday,
.fhc-calendar-mode-range .fhc-calendar-base-grid-line-sunday {
border-bottom-width: 0 !important;
}
.fhc-calendar-base-grid-line-sunday,
.fhc-calendar-mode-range .main-header-sunday {
break-after: page !important;
}
.fhc-calendar-mode-range .fhc-calendar-base-grid {
grid-template-rows: auto repeat(var(--fhc-grid-displayed-axis-main-count), auto) !important;
gap: 0px !important;
}
.fhc-calendar-mode-range .fhc-calendar-base-grid * {
gap: 0px !important;
}
.fhc-calendar-mode-range .main-header:not(.main-header-empty),
.fhc-calendar-mode-range .fhc-calendar-base-grid-line {
border-top: solid !important;
border-top-color: black !important;
border-top-width: 1px !important;
}
+7 -1
View File
@@ -104,11 +104,17 @@
.fhc-calendar-base-grid-line {
grid-gap: var(--fhc-calendar-gap-events, var(--fhc-calendar-gap, 1px));
}
.fhc-calendar-mode-range .main-header-sunday,
.fhc-calendar-mode-range .all-day-events-sunday,
.fhc-calendar-mode-range .fhc-calendar-base-grid-line-sunday {
border: solid;
border-width: 0 0 8px 0;
border-color: var(--fhc-calendar-gap);
}
.fhc-calendar-base-label-time {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: end;
}
/* Header */
.fhc-calendar-base-header .header-picker {
+1 -1
View File
@@ -190,7 +190,7 @@ const router = VueRouter.createRouter({
}
},
{
path: `/Cis/MyLvPlan/:mode?/:focus_date?`,
path: `/Cis/MyLvPlan/:mode?/:focus_date?/:range_length?`,
name: 'MyLvPlan',
component: MyLvPlan,
props(route) {
+16 -2
View File
@@ -93,6 +93,10 @@ export default {
type: Boolean,
default: undefined
},
btnRange: {
type: Boolean,
default: undefined
},
timeGrid: Array,
draggableEvents: [Boolean, Array, Function],
dropableEvents: [Boolean, Array, Function],
@@ -163,8 +167,16 @@ export default {
return date.setLocale(this.locale);
},
set(value) {
this.internalDate = value;
this.$emit('update:date', value, this.cMode);
let date;
let rangeLength;
if (value instanceof luxon.DateTime) {
date = value;
} else {
date = value.date;
rangeLength = value.rangeLength;
}
this.internalDate = date;
this.$emit('update:date', date, this.cMode, rangeLength);
}
},
sMode() {
@@ -282,6 +294,7 @@ export default {
:btn-week="!!modes['week'] && (btnWeek || (showBtns && btnWeek !== false))"
:btn-month="!!modes['month'] && (btnMonth || (showBtns && btnMonth !== false))"
:btn-list="!!modes['list'] && (btnList || (showBtns && btnList !== false))"
:btn-range="!!modes['range'] && (btnRange || (showBtns && btnRange !== false))"
:mode-options="modeOptions ? modeOptions[cMode] : undefined"
>
<slot name="actions" />
@@ -293,6 +306,7 @@ export default {
@update:range="$emit('update:range', $event)"
@request-modal-open="showEventModal"
@request-modal-close="hideEventModal"
@update:date="cDate = $event"
v-bind="modeOptions ? modeOptions[cMode] : null || {}"
>
<template v-slot="slot"><slot v-bind="slot" /></template>
+121 -70
View File
@@ -53,7 +53,11 @@ export default {
flipAxis: Boolean,
allDayEvents: Boolean,
axisMainCollapsible: Boolean,
snapToGrid: Boolean
snapToGrid: Boolean,
arePartHeadersRepeated: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -110,6 +114,19 @@ export default {
return this.axisParts.slice(0, -1);
return this.axisParts;
},
displayedAxisMain() {
if (!this.$props.arePartHeadersRepeated)
return this.$props.axisMain;
let displayedAxisMain = [];
this.$props.axisMain.forEach((part, index) => {
if (part.weekday === 1 && index > 0) {
displayedAxisMain.push(null);
}
displayedAxisMain.push(part);
});
return displayedAxisMain;
},
start() {
return this.axisPartsWithBreaks[0].start;
},
@@ -128,15 +145,17 @@ export default {
return ends;
},
axisMainBorders() {
return this.axisMain.reduce(
(res, curr) => res.concat([curr.plus(this.start), curr.plus(this.end)]),
return this.displayedAxisMain.reduce(
(res, curr) => res.concat(curr ? [curr.plus(this.start), curr.plus(this.end)] : [null, null]),
[]
);
},
eventsAllDay() {
if (!this.allDayEvents)
return [];
return this.mapIntoMainAxis(this.originalEvents.filter(event => event.orig.allDayEvent));
let eventsAllDay = this.mapIntoMainAxis(this.originalEvents.filter(event => event.orig.allDayEvent));
return eventsAllDay;
},
eventsNormal() {
if (!this.allDayEvents)
@@ -153,7 +172,7 @@ export default {
return this.events.find(e => e.length);
},
styleGridCols() {
let cols = 'repeat(' + this.axisMain.length + ', 1fr)';
let cols = 'repeat(' + this.displayedAxisMain.length + ', 1fr)';
if (this.axisMainCollapsible) {
if (this.hasValidEvents)
cols = this.events
@@ -181,7 +200,7 @@ export default {
});
this.eventsNormal.forEach((events, mainIndex) => {
let day = this.axisMain[mainIndex];
let day = this.displayedAxisMain[mainIndex];
events.forEach(event => {
if (!event.startsHere && !event.endsHere)
return;
@@ -211,15 +230,17 @@ export default {
},
methods: {
mapIntoMainAxis(target) {
const result = Array.from({length: this.axisMain.length}, () => Array());
const result = Array.from({length: this.displayedAxisMain.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];
this.displayedAxisMain.forEach((part, index) => {
if (!part) return;
let laneStart = this.axisMainBorders[index * 2];
let laneEnd = this.axisMainBorders[index * 2 + 1];
if (event.orig?.allDayEvent) {
laneStart = laneStart.startOf('day');
laneEnd = laneEnd.endOf('day');
@@ -227,13 +248,13 @@ export default {
if (start < laneEnd && end > laneStart) {
const startsHere = start >= laneStart;
const endsHere = end <= laneEnd;
result[i].push({
result[index].push({
...event,
startsHere,
endsHere
});
}
}
});
});
return result;
@@ -317,22 +338,31 @@ export default {
<div
class="fhc-calendar-base-grid"
style="display:grid;width:100%;height:100%;overflow:auto"
:style="'grid-template-' + axisRow + 's:auto' + (allDayEvents ? ' auto ' : ' ') + '1fr;grid-template-' + axisCol + 's:auto ' + styleGridCols"
:style="'--fhc-grid-displayed-axis-main-count: ' + displayedAxisMain.length + ';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>
<template v-for="(date, index) in displayedAxisMain" :key="index">
<div
v-if="date"
class="main-header"
:class="{
'collapsed-header': axisMainCollapsible && hasValidEvents && !events[index].length,
'main-header-sunday': date.weekday === 7,
}"
:style="'grid-' + axisCol + ':' + (2+index)"
>
<slot name="main-header" v-bind="{ index, date }" />
</div>
<div
v-else
class="main-header main-header-empty"
:style="'grid-' + axisCol + ':' + (2+index)"
></div>
</template>
</div>
<div
v-if="allDayEvents"
@@ -340,26 +370,34 @@ export default {
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-for="(events, index) in eventsAllDay" :key="index">
<div
v-if="displayedAxisMain[index]"
class="all-day-events"
:class="{'all-day-events-sunday': displayedAxisMain[index].weekday === 7}"
:style="'grid-' + axisCol + ':' + (2+index)"
>
<template v-slot="slot">
<slot name="event" v-bind="slot" />
</template>
</grid-line-event>
</div>
<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
v-else
class="all-day-events"
:style="'grid-' + axisCol + ':' + (2+index)"
></div>
</template>
</div>
<div
ref="scroller"
@scrollend="userScroll ? disableAutoScroll() : userScroll = true"
id="grid-main-scrollable"
style="display:grid;overflow:auto"
:style="'grid-' + axisCol + ':1/-1;grid-template-' + axisCol + 's:subgrid'"
>
@@ -371,9 +409,9 @@ export default {
>
<div
v-for="(part, index) in axisPartsSave"
:key="index"
:key="index"2
class="part-header"
:style="'grid-' + axisCol + ':1;grid-' + axisRow + ': ps_' + index + '/pe_' + index"
:style="'grid-' + axisCol + ':1;grid-' + axisRow + ': ps_' + index + '/pe_' + index + ';min-width:50px;'"
>
<slot name="part-header" v-bind="{ index, part }" />
</div>
@@ -389,43 +427,56 @@ export default {
@dragover="dropAllowed ? $event.preventDefault() : null"
>
<template
v-for="(date, index) in axisMain"
v-for="(date, index) in displayedAxisMain"
: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 }" />
<template v-if="date">
<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>
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"
v-if="snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1"
v-cal-dnd:dropzone="evt => getTimestampFromMouse(evt, date)"
v-cal-dnd:dropzone.once="{date: date.plus(part.start || part), ends: ends.slice(ends.findIndex(end => end > date))}"
></div>
</template>
</grid-line>
</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>
<template v-else>
<div
v-for="(part, i) in axisPartsSave"
:key="i"
class="part-header-repeat part-body"
style="position:relative"
:style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i"
>
<slot name="part-header-repeat" v-bind="{ index, part }" />
</div>
</template>
</template>
</div>
</div>
@@ -123,6 +123,9 @@ export default {
template: /* html */ `
<div
class="fhc-calendar-base-grid-line"
:class="{
'fhc-calendar-base-grid-line-sunday': $props.date.weekday === 7
}"
style="position:relative;display:grid;grid-auto-flow:dense"
:style="'grid-template-' + axisRow + 's:subgrid'"
>
+17 -4
View File
@@ -24,7 +24,8 @@ export default {
btnMonth: Boolean,
btnWeek: Boolean,
btnDay: Boolean,
btnList: Boolean
btnList: Boolean,
btnRange: Boolean,
},
emits: [
"next",
@@ -35,7 +36,8 @@ export default {
],
data() {
return {
open: false
open: false,
semesterOptions: [],
};
},
methods: {
@@ -53,6 +55,15 @@ export default {
</div>
<div class="header-modes">
<div class="d-flex gap-1 justify-content-end" role="group">
<button
v-if="btnRange"
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'range'}"
@click="clickMode($event, 'range')"
>
<i class="fa fa-calendar"></i>
</button>
<button
v-if="btnMonth"
type="button"
@@ -95,9 +106,10 @@ export default {
<div class="header-picker">
<div class="btn-group" role="group">
<button
class="btn btn-outline-secondary border-0"
v-if="$props.mode !== 'range'"
@click="$emit('prev')"
:disabled="open"
class="btn btn-outline-secondary border-0"
>
<i class="fa fa-chevron-left"></i>
</button>
@@ -110,9 +122,10 @@ export default {
:list-length="modeOptions.length"
/>
<button
class="btn btn-outline-secondary border-0"
v-if="$props.mode !== 'range'"
@click="$emit('next')"
:disabled="open"
class="btn btn-outline-secondary border-0"
>
<i class="fa fa-chevron-right"></i>
</button>
@@ -5,10 +5,11 @@ export default {
components: {
VueDatePicker
},
inject: [
"locale",
"timezone"
],
inject: {
locale: "locale",
timezone: "timezone",
rangeLength: {default: 30}
},
props: {
date: {
type: luxon.DateTime,
@@ -21,7 +22,7 @@ export default {
listLength: {
type: Number,
default: 7
}
},
},
emits: [
"update:date"
@@ -38,6 +39,8 @@ export default {
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 "range":
return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.rangeLength }).ts - 1];
case "week":
return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts];
case "day":
@@ -56,6 +59,8 @@ export default {
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 "range":
return this.date.toLocaleString(luxon.DateTime.DATE_FULL) + '-' + this.date.plus({ days: this.rangeLength - 1 }).toLocaleString(luxon.DateTime.DATE_FULL);
case "day":
return this.date.toLocaleString(luxon.DateTime.DATE_FULL);
default:
@@ -64,11 +69,21 @@ export default {
},
weekStart() {
return luxon.Info.getStartOfWeek(this.date)%7;
}
},
rangeConfig() {
if (this.$props.mode === "list") {
return { autoRange: listLength - 1 };
} else if (this.$props.mode === "range") {
return true;
} else {
return false;
}
},
},
methods: {
update(value) {
let date;
let rangeLength;
switch (this.mode) {
case "month":
value.month++;
@@ -78,13 +93,18 @@ export default {
case "week":
date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break;
case "range":
date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
let endDate = luxon.DateTime.fromJSDate(value[1]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
rangeLength = Math.floor(endDate.diff(date, "days").toObject().days) + 1;
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);
this.$emit("update:date", { date, rangeLength });
},
weekNumbers(date) {
return luxon.DateTime.fromJSDate(date, { locale: this.locale }).localWeekNumber;
@@ -97,7 +117,7 @@ export default {
:format="() => title"
:month-picker="mode == 'month'"
:week-picker="mode == 'week'"
:range="mode == 'list' ? { autoRange: listLength - 1 } : false"
:range="rangeConfig"
:text-input="mode == 'day'"
:week-start="weekStart"
:week-numbers="{ type: weekNumbers }"
@@ -26,7 +26,11 @@ export default {
}
return true;
}
}
},
alignItemsClassSuffix: {
type: String,
default: "end",
},
},
computed: {
sanitizedTimestamps() {
@@ -49,7 +53,7 @@ export default {
}
},
template: `
<div class="fhc-calendar-base-label-time">
<div class="fhc-calendar-base-label-time" :class="'align-items-' + $props.alignItemsClassSuffix">
<span v-if="start">{{ start }}</span>
<span v-if="end">-</span>
<span v-if="end">{{ end }}</span>
+33 -15
View File
@@ -9,13 +9,21 @@ import ModeDay from './Mode/Day.js';
import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js';
import ModeList from './Mode/List.js';
import ModeRange from './Mode/Range.js';
export default {
name: "CalendarLvPlan",
components: {
FhcCalendar
},
inject: ["isMobile"],
inject: {
isMobile: {
default: false,
},
rangeLength: {
default: 30,
}
},
props: {
date: {
type: [Date, String, Number, luxon.DateTime],
@@ -28,7 +36,11 @@ export default {
getPromiseFunc: {
type: Function,
required: true
}
},
shouldIncludeRangeMode: {
type: Boolean,
default: false,
},
},
provide() {
return {
@@ -68,31 +80,37 @@ export default {
backgrounds() {
let now = luxon.DateTime.now().setZone(this.timezone);
if (this.mode == 'Month')
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 })
}
];
} else if (this.mode == 'Range') {
return [];
} else {
return [
{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}
];
}
},
modes() {
let modes = {
day: Vue.markRaw(ModeDay),
month: Vue.markRaw(ModeMonth),
};
if (this.isMobile) {
modes.list = Vue.markRaw(ModeList);
} else {
if (!this.isMobile) {
modes.week = Vue.markRaw(ModeWeek);
if (this.$props.shouldIncludeRangeMode) {
modes.range = Vue.markRaw(ModeRange);
}
} else {
modes.list = Vue.markRaw(ModeList);
}
return modes;
@@ -163,7 +181,7 @@ export default {
:backgrounds="backgrounds"
:time-grid="teachingunits"
show-btns
@update:date="(newDate, newMode) => $emit('update:date', newDate, newMode)"
@update:date="(newDate, newMode, newRangeLength) => $emit('update:date', newDate, newMode, newRangeLength)"
@update:mode="(newMode, newDate) => $emit('update:mode', newMode, newDate)"
@update:range="updateRange"
>
+122
View File
@@ -0,0 +1,122 @@
import BaseSlider from "../Base/Slider.js";
import RangeView from "./Range/View.js";
import FormInput from "../../Form/Input.js";
export default {
name: "ModeRange",
components: {
BaseSlider,
RangeView,
FormInput,
},
inject: {
rangeLength: {
default: 30,
},
rangeViewPresets: {
default: {},
},
},
props: {
currentDate: {
type: luxon.DateTime,
required: true,
},
},
emits: ["update:currentDate", "update:range", "click", "requestModalOpen"],
data() {
return {
selectedRangePreset: null,
};
},
computed: {
range() {
let first = this.$props.currentDate;
let last = first.plus({ days: this.rangeLength });
return luxon.Interval.fromDateTimes(first, last);
},
},
watch: {
currentDate() {
this.$emit("update:range", this.range);
},
rangeLength() {
this.$emit("update:range", this.range);
},
selectedRangePreset() {
if (!this.selectedRangePreset) return;
const preset = this.rangeViewPresets.presets.find(
(preset) => preset.name === this.selectedRangePreset,
);
if (!preset) return;
this.$emit("update:date", {
date: preset.startDate,
rangeLength:
preset.endDate.diff(preset.startDate, "days").days + 1,
});
this.selectedRangePreset = null;
},
},
methods: {
viewAttrs() {
const day = this.$props.currentDate.startOf("day");
const rangeLength = parseInt(this.rangeLength);
return { ...this.$attrs, day, rangeLength };
},
handleClickDefaults(evt) {
switch (evt.detail.source) {
case "day":
// default: Set current-date
this.$emit("update:currentDate", {
date: 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: /*html*/ `
<div
class="fhc-calendar-mode-range flex-grow-1 position-relative d-flex flex-column"
@cal-click-default.capture="handleClickDefaults"
>
<div
v-if="rangeViewPresets?.presets?.length"
id="rangePresetSelector"
class="w-100 d-flex flex-row gap-2 justify-content-center align-items-center py-2"
>
<span>{{ rangeViewPresets.label }}</span>
<form-input
name="rangePresetSelector"
type="select"
v-model="selectedRangePreset"
>
<option
v-for="preset in rangeViewPresets.presets"
:key="preset.name"
:value="preset.name"
>
{{ preset.name }}
</option>
</form-input>
</div>
<div class="flex-grow-1">
<base-slider ref="slider" v-slot="slot">
<range-view ref="view" v-bind="viewAttrs()">
<template v-slot="slot"><slot v-bind="slot" mode="range" /></template>
</range-view>
</base-slider>
</div>
</div>
`,
};
@@ -0,0 +1,88 @@
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: "RangeView",
components: {
CalendarGrid,
LabelDay,
LabelDow,
LabelTime
},
inject: {
timeGrid: "timeGrid",
timezone: "timezone"
},
props: {
day: {
type: luxon.DateTime,
required: true
},
collapseEmptyDays: Boolean,
rangeLength: Number,
},
computed: {
axisMain() {
return Array.from({ length: this.$props.rangeLength }, (e, i) => this.$props.day.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-range-view h-100">
<calendar-grid
ref="grid"
:axis-main="axisMain"
:axis-parts="axisParts"
:axis-main-collapsible="collapseEmptyDays"
:snap-to-grid="!!timeGrid"
:flip-axis="true"
:arePartHeadersRepeated="true"
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 }" :alignItemsClassSuffix="'center'" />
</template>
<template #part-header-repeat="{ part }">
<label-time v-bind="{ part }" :alignItemsClassSuffix="'center'" />
</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>
`
}
+46 -6
View File
@@ -2,6 +2,7 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
import ApiStudiensemester from '../../../api/factory/studiensemester.js';
export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
export const DEFAULT_MODE_LVPLAN_MOBILE = "List";
@@ -23,9 +24,23 @@ export default {
isMitarbeiter: false,
isStudent: false,
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
semesterRangePresets: {
label: null,
presets: [],
},
};
},
inject: ["isMobile"],
provide() {
return {
rangeLength: Vue.computed(() => {
if (!this.$route.params.range_length) return 30;
else if (this.$route.params.range_length > 365) return 365;
else return this.$route.params.range_length;
}),
rangeViewPresets: Vue.computed(() => this.semesterRangePresets)
};
},
computed:{
currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
@@ -34,7 +49,11 @@ export default {
},
currentMode() {
let validModes = ["day", "month"];
validModes.push(this.isMobile ? "list" : "week");
if (!this.isMobile) {
validModes.push("week", "range");
} else {
validModes.push("list");
}
const defaultMode = this.isMobile
? DEFAULT_MODE_LVPLAN_MOBILE
@@ -94,10 +113,10 @@ export default {
},
},
methods: {
handleChangeDate(day, newMode) {
return this.handleChangeMode(newMode, day);
handleChangeDate(day, newMode, rangeLength) {
return this.handleChangeMode(newMode, day, rangeLength);
},
handleChangeMode(newMode, day) {
handleChangeMode(newMode, day, range_length = null) {
const mode = newMode[0].toUpperCase() + newMode.slice(1)
const focus_date = day.toISODate();
@@ -105,7 +124,8 @@ export default {
name: "MyLvPlan",
params: {
mode,
focus_date
focus_date,
range_length,
}
});
},
@@ -134,13 +154,32 @@ export default {
this.isMitarbeiter = authInfo.isMitarbeiter;
this.isStudent = authInfo.isStudent;
},
async fetchSemesters() {
const semestersResponse = await this.$api.call(ApiStudiensemester.getAll());
if (semestersResponse.meta.status === "success") {
this.semesterRangePresets = {
label: "View specific semester",
presets: semestersResponse.data.map((semester) => {
let startDate = luxon.DateTime.fromISO(semester.start);
let endDate = luxon.DateTime.fromISO(semester.ende);
return {
startDate,
endDate,
name: semester.studiensemester_kurzbz,
description: semester.bezeichnung,
};
})
};
}
},
},
async created() {
await this.fetchAuthInfo();
await this.fetchSemesters();
},
template: /*html*/`
<div class="cis-lvplan-personal d-flex flex-column h-100">
<h2>
<h2 id="cis-lvplan-personal-heading">
{{ $p.t('lehre/stundenplan') }}
<span v-if="studiensemester_kurzbz" class="ps-3">
{{ studiensemester_kurzbz }}
@@ -154,6 +193,7 @@ export default {
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
:shouldIncludeRangeMode="true"
@update:date="handleChangeDate"
@update:mode="handleChangeMode"
@update:range="updateRange"
@@ -206,6 +206,8 @@ export default {
});
this.lehrveranstaltungen.forEach((lehrveranstaltung)=>{
// a lehrveranstaltung without a modul has no children
if(!Array.isArray(lehrveranstaltung.lehrveranstaltungen)) return;
lehrveranstaltung.lehrveranstaltungen.sort((lv1,lv2)=>{
if (lv1.bezeichnung.toLowerCase() > lv2.bezeichnung.toLowerCase()) {
return 1;
@@ -393,6 +395,16 @@ export default {
</ul>
</div>
</div>
<div class="card" v-else-if="!Array.isArray(lehrveranstaltung.lehrveranstaltungen)">
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="d-flex list-group-item">
<a class="fhc-link-color d-block me-auto" href="#" @click="openLvUebersicht(lehrveranstaltung)">{{isGermanLanguage ? lehrveranstaltung.bezeichnung : lehrveranstaltung.bezeichnung_english}}</a>
<p>{{lehrveranstaltung.lehrform_kurzbz}}</p>
</li>
</ul>
</div>
</div>
</template>
</div>