Stundenplan & Calendar

This commit is contained in:
cgfhtw
2023-02-06 08:43:43 +01:00
parent d1f69d4696
commit 0f60ab2a54
21 changed files with 1254 additions and 0 deletions
@@ -0,0 +1,31 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
*
*/
class Stundenplan extends Auth_Controller
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct([
'index' => ['student/anrechnung_beantragen:r','user:r'] // TODO(chris): permissions?
]);
}
// -----------------------------------------------------------------------------------------------------------------
// Public methods
/**
* @return void
*/
public function index()
{
$this->load->view('Cis/Stundenplan');
}
}
@@ -0,0 +1,68 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
*
*/
class Stundenplan extends Auth_Controller
{
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'index' => ['student/anrechnung_beantragen:r','user:r'], // TODO(chris): permissions?
'Reservierungen' => ['student/anrechnung_beantragen:r','user:r'], // TODO(chris): permissions?
'Stunden' => ['student/anrechnung_beantragen:r','user:r'] // TODO(chris): permissions?
]);
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
*/
public function index()
{
$this->load->model('ressource/Stundenplan_model', 'StundenplanModel');
$result = $this->StundenplanModel->loadForUid(get_uid());
if (isError($result))
return $this->outputJsonError(getError($result));
$this->outputJsonSuccess(getData($result));
}
/**
*/
public function Reservierungen()
{
$this->load->model('ressource/Reservierung_model', 'ReservierungModel');
$result = $this->ReservierungModel->loadForUid(get_uid());
if (isError($result))
return $this->outputJsonError(getError($result));
$this->outputJsonSuccess(getData($result));
}
/**
*/
public function Stunden()
{
$this->load->model('ressource/Stunde_model', 'StundeModel');
$result = $this->StundeModel->load();
if (isError($result))
return $this->outputJsonError(getError($result));
$this->outputJsonSuccess(getData($result));
}
}
@@ -11,4 +11,25 @@ class Reservierung_model extends DB_Model
$this->dbTable = 'campus.tbl_reservierung';
$this->pk = 'reservierung_id';
}
/**
* @param $uid
*
* @return stdClass
*/
public function loadForUid($uid)
{
$this->addSelect('r.*');
$this->db->join('public.tbl_benutzergruppe bg', 'r.gruppe_kurzbz=bg.gruppe_kurzbz AND bg.uid=?', 'LEFT', false);
$this->addJoin('public.tbl_studiensemester ss1', 'bg.studiensemester_kurzbz=ss1.studiensemester_kurzbz AND ss1.start<=r.datum AND ss1.ende>=r.datum', 'LEFT');
$this->db->join('public.tbl_studentlehrverband slv', "r.studiengang_kz=slv.studiengang_kz AND slv.student_uid=? AND (slv.semester=r.semester OR r.semester IS NULL) AND (slv.verband=r.verband OR r.verband IS NULL OR r.verband='' OR r.verband='0') AND (slv.gruppe=r.gruppe OR r.gruppe IS NULL OR r.gruppe='' OR r.gruppe='0') AND r.gruppe_kurzbz IS NULL", 'LEFT', false);
$this->addJoin('public.tbl_studiensemester ss2', 'slv.studiensemester_kurzbz=ss2.studiensemester_kurzbz AND ss2.start<=r.datum AND ss2.ende>=r.datum', 'LEFT');
$this->db->or_where('ss1.studiensemester_kurzbz IS NOT NULL', null, false);
$this->db->or_where('ss2.studiensemester_kurzbz IS NOT NULL', null, false);
$query = $this->db->get_compiled_select('campus.vw_reservierung r');
return $this->execQuery($query, [$uid, $uid]);
}
}
@@ -11,4 +11,25 @@ class Stundenplan_model extends DB_Model
$this->dbTable = 'lehre.tbl_stundenplan';
$this->pk = 'stundenplan_id';
}
/**
* @param string $uid
*
* @return stdClass
*/
public function loadForUid($uid)
{
$this->addSelect('sp.*');
$this->db->join('public.tbl_benutzergruppe bg', 'sp.gruppe_kurzbz=bg.gruppe_kurzbz AND bg.uid=?', 'LEFT', false);
$this->addJoin('public.tbl_studiensemester ss1', 'bg.studiensemester_kurzbz=ss1.studiensemester_kurzbz AND ss1.start<=sp.datum AND ss1.ende>=sp.datum', 'LEFT');
$this->db->join('public.tbl_studentlehrverband slv', "sp.studiengang_kz=slv.studiengang_kz AND slv.student_uid=? AND (slv.semester=sp.semester OR sp.semester IS NULL) AND (slv.verband=sp.verband OR sp.verband IS NULL OR sp.verband='' OR sp.verband='0') AND (slv.gruppe=sp.gruppe OR sp.gruppe IS NULL OR sp.gruppe='' OR sp.gruppe='0') AND sp.gruppe_kurzbz IS NULL", 'LEFT', false);
$this->addJoin('public.tbl_studiensemester ss2', 'slv.studiensemester_kurzbz=ss2.studiensemester_kurzbz AND ss2.start<=sp.datum AND ss2.ende>=sp.datum', 'LEFT');
$this->db->or_where('ss1.studiensemester_kurzbz IS NOT NULL', null, false);
$this->db->or_where('ss2.studiensemester_kurzbz IS NOT NULL', null, false);
$query = $this->db->get_compiled_select('lehre.vw_stundenplan sp');
return $this->execQuery($query, [$uid, $uid]);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
$includesArray = array(
'customJSModules' => ['public/js/apps/Cis/Stundenplan.js'],
'customCSSs' => ['public/css/components/calendar.css']
);
$this->load->view('templates/CISHTML-Header', $includesArray);
?>
<div id="content">
<h2>Stundenplan</h2>
<hr>
<fhc-calendar :events="events" initial-mode="week" show-weeks></fhc-calendar>
</div>
<?php $this->load->view('templates/CISHTML-Footer', $includesArray); ?>
+160
View File
@@ -0,0 +1,160 @@
.fhc-calendar-week-page-header {
background-color: #e9ecef;
}
.fhc-calendar-months .col-4,
.fhc-calendar-years .col-4 {
padding: 0.1875em 0;
}
.show-weeks .fhc-calendar-months .col-4,
.show-weeks .fhc-calendar-years .col-4 {
padding: 0.1875em 0.25em;
}
.fhc-calendar-months .col-4 button,
.fhc-calendar-years .col-4 button {
aspect-ratio: 28/18;
}
.fhc-calendar-month-page {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: 1.5em repeat(6, 1fr);
}
.fhc-calendar-month-page.show-weeks {
grid-template-columns: 1.5em repeat(7, 1fr);
grid-template-rows: 1.5em repeat(6, 1fr);
}
.fhc-calendar-month-page-weekday,
.fhc-calendar-month-page-day {
color: inherit;
}
.fhc-calendar-week .carousel-item {
padding: 0.75em;
}
.fhc-calendar-week-page {
aspect-ratio: 7/6;
min-height: 0;
}
.fhc-calendar-week-page > div {
transform: translate(-0.75em, -0.75em);
width: calc(100% + 1.5em);
height: calc(100% + 1.5em);
}
.fhc-calendar-week-page > div > div {
padding-left: 3em;
}
.fhc-calendar-week-page .events {
display: grid;
grid-template-columns: 3em repeat(7, 1fr);
margin-left: -3em;
min-height: 266.6666666667%;
background-image: linear-gradient(#f8f9fa calc(100% / 24 - .5px), #dee2e6 calc(100% / 24 - .5px), #dee2e6 calc(100% / 24 + .5px), #f8f9fa calc(100% / 24 + .5px), #f8f9fa calc(200% / 24 - .5px), #dee2e6 calc(200% / 24 - .5px), #dee2e6 calc(200% / 24 + .5px), #f8f9fa calc(200% / 24 + .5px), #f8f9fa calc(300% / 24 - .5px), #dee2e6 calc(300% / 24 - .5px), #dee2e6 calc(300% / 24 + .5px), #f8f9fa calc(300% / 24 + .5px), #f8f9fa calc(400% / 24 - .5px), #dee2e6 calc(400% / 24 - .5px), #dee2e6 calc(400% / 24 + .5px), #f8f9fa calc(400% / 24 + .5px), #f8f9fa calc(500% / 24 - .5px), #dee2e6 calc(500% / 24 - .5px), #dee2e6 calc(500% / 24 + .5px), #f8f9fa calc(500% / 24 + .5px), #f8f9fa calc(600% / 24 - .5px), #dee2e6 calc(600% / 24 - .5px), #dee2e6 calc(600% / 24 + .5px), #f8f9fa calc(600% / 24 + .5px), #f8f9fa calc(700% / 24 - .5px), #dee2e6 calc(700% / 24 - .5px), #dee2e6 calc(700% / 24 + .5px), #f8f9fa calc(700% / 24 + .5px), #f8f9fa calc(800% / 24 - .5px), #dee2e6 calc(800% / 24 - .5px), #dee2e6 calc(800% / 24 + .5px), #fff calc(800% / 24 + .5px), #fff calc(900% / 24 - .5px), #dee2e6 calc(900% / 24 - .5px), #dee2e6 calc(900% / 24 + .5px), #fff calc(900% / 24 + .5px), #fff calc(1000% / 24 - .5px), #dee2e6 calc(1000% / 24 - .5px), #dee2e6 calc(1000% / 24 + .5px), #fff calc(1000% / 24 + .5px), #fff calc(1100% / 24 - .5px), #dee2e6 calc(1100% / 24 - .5px), #dee2e6 calc(1100% / 24 + .5px), #fff calc(1100% / 24 + .5px), #fff calc(1200% / 24 - .5px), #dee2e6 calc(1200% / 24 - .5px), #dee2e6 calc(1200% / 24 + .5px), #fff calc(1200% / 24 + .5px), #fff calc(1300% / 24 - .5px), #dee2e6 calc(1300% / 24 - .5px), #dee2e6 calc(1300% / 24 + .5px), #fff calc(1300% / 24 + .5px), #fff calc(1400% / 24 - .5px), #dee2e6 calc(1400% / 24 - .5px), #dee2e6 calc(1400% / 24 + .5px), #fff calc(1400% / 24 + .5px), #fff calc(1500% / 24 - .5px), #dee2e6 calc(1500% / 24 - .5px), #dee2e6 calc(1500% / 24 + .5px), #fff calc(1500% / 24 + .5px), #fff calc(1600% / 24 - .5px), #dee2e6 calc(1600% / 24 - .5px), #dee2e6 calc(1600% / 24 + .5px), #fff calc(1600% / 24 + .5px), #fff calc(1700% / 24 - .5px), #dee2e6 calc(1700% / 24 - .5px), #dee2e6 calc(1700% / 24 + .5px), #f8f9fa calc(1700% / 24 + .5px), #f8f9fa calc(1800% / 24 - .5px), #dee2e6 calc(1800% / 24 - .5px), #dee2e6 calc(1800% / 24 + .5px), #f8f9fa calc(1800% / 24 + .5px), #f8f9fa calc(1900% / 24 - .5px), #dee2e6 calc(1900% / 24 - .5px), #dee2e6 calc(1900% / 24 + .5px), #f8f9fa calc(1900% / 24 + .5px), #f8f9fa calc(2000% / 24 - .5px), #dee2e6 calc(2000% / 24 - .5px), #dee2e6 calc(2000% / 24 + .5px), #f8f9fa calc(2000% / 24 + .5px), #f8f9fa calc(2100% / 24 - .5px), #dee2e6 calc(2100% / 24 - .5px), #dee2e6 calc(2100% / 24 + .5px), #f8f9fa calc(2100% / 24 + .5px), #f8f9fa calc(2200% / 24 - .5px), #dee2e6 calc(2200% / 24 - .5px), #dee2e6 calc(2200% / 24 + .5px), #f8f9fa calc(2200% / 24 + .5px), #f8f9fa calc(2300% / 24 - .5px), #dee2e6 calc(2300% / 24 - .5px), #dee2e6 calc(2300% / 24 + .5px), #f8f9fa calc(2300% / 24 + .5px));
}
.fhc-calendar-week-page .events .day {
gap: 1px;
}
.fhc-calendar-week-page .events .hours, .fhc-calendar-week-page .events .day {
display: grid;
}
.fhc-calendar-lg .fhc-calendar-month-page-day,
.fhc-calendar-lg .fhc-calendar-month-page-weekday,
.fhc-calendar-md .fhc-calendar-month-page-day,
.fhc-calendar-md .fhc-calendar-month-page-weekday {
border: solid 1px #dee2e6;
}
.fhc-calendar-lg .fhc-calendar-month-page-day,
.fhc-calendar-md .fhc-calendar-month-page-day {
aspect-ratio: 1;
display: flex;
flex-direction: column;
}
.fhc-calendar-lg .fhc-calendar-month-page-day.active,
.fhc-calendar-md .fhc-calendar-month-page-day.active {
border-color: var(--bs-secondary);
}
.fhc-calendar-lg .fhc-calendar-month-page-day .events,
.fhc-calendar-md .fhc-calendar-month-page-day .events {
display: block;
overflow: scroll;
font-size: 0.7em;
}
.fhc-calendar-lg .fhc-calendar-month-page-day .events span,
.fhc-calendar-md .fhc-calendar-month-page-day .events span {
display: block;
margin: 0.2em;
padding: 0.1em 0.4em;
border-radius: 0.1em;
}
.fhc-calendar-lg .fhc-calendar-years .col-4,
.fhc-calendar-md .fhc-calendar-years .col-4 {
padding: 0.09375em 0;
}
.show-weeks .fhc-calendar-lg .fhc-calendar-years .col-4,
.show-weeks .fhc-calendar-md .fhc-calendar-years .col-4 {
padding: 0.09375em 0.25em;
}
.fhc-calendar-lg .fhc-calendar-years .col-4 button,
.fhc-calendar-md .fhc-calendar-years .col-4 button {
aspect-ratio: 28/9;
}
.fhc-calendar-xs {
font-size: 0.6em;
}
.fhc-calendar-sm .fhc-calendar-month-page {
font-size: 0.8em;
}
.fhc-calendar-sm .fhc-calendar-month-page-weekday,
.fhc-calendar-xs .fhc-calendar-month-page-weekday {
font-style: italic;
display: flex;
align-items: center;
justify-content: end;
}
.fhc-calendar-sm .fhc-calendar-month-page-day,
.fhc-calendar-xs .fhc-calendar-month-page-day {
position: relative;
aspect-ratio: 1;
}
.fhc-calendar-sm .fhc-calendar-month-page-day.active .no,
.fhc-calendar-xs .fhc-calendar-month-page-day.active .no {
background-color: rgba(var(--bs-info-rgb), 0.25);
border-radius: 50%;
font-weight: bold;
}
.fhc-calendar-sm .fhc-calendar-month-page-day:not(.active):hover .no,
.fhc-calendar-xs .fhc-calendar-month-page-day:not(.active):hover .no {
background-color: rgba(var(--bs-secondary-rgb), 0.25);
border-radius: 50%;
}
.fhc-calendar-sm .fhc-calendar-month-page-day .no,
.fhc-calendar-xs .fhc-calendar-month-page-day .no {
display: flex;
align-items: center;
justify-content: center;
width: 80%;
height: 80%;
margin: 10%;
}
.fhc-calendar-sm .fhc-calendar-month-page-day .events,
.fhc-calendar-xs .fhc-calendar-month-page-day .events {
position: absolute;
bottom: 0;
left: 10%;
width: 80%;
height: 10%;
overflow: hidden;
display: flex;
}
.fhc-calendar-sm .fhc-calendar-month-page-day .events span,
.fhc-calendar-xs .fhc-calendar-month-page-day .events span {
overflow: hidden;
}
/*# sourceMappingURL=calendar.css.map */
+53
View File
@@ -0,0 +1,53 @@
import FhcCalendar from "../../components/Calendar/Calendar.js";
const app = Vue.createApp({
components: {
FhcCalendar
},
data() {
return {
stunden: [],
events: null
}
},
created() {
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Stundenplan/Stunden').then(res => {
res.data.retval.forEach(std => {
this.stunden[std.stunde] = std; // TODO(chris): geht besser
});
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Stundenplan').then(res => {
let events;
if (res.data.retval && res.data.retval.forEach) {
res.data.retval.forEach((el, i) => {
el.id = i;
el.color = '#' + (el.farbe || 'CCCCCC');
el.start = new Date(el.datum + ' ' + this.stunden[el.stunde].beginn);
el.end = new Date(el.datum + ' ' + this.stunden[el.stunde].ende);
el.title = el.lehrfach;
if (el.lehrform)
el.title += '-' + el.lehrform;
});
events = res.data.retval;
}
// TODO(chris): do we need that
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Stundenplan/Reservierungen').then(res => {
if (res.data.retval && res.data.retval.forEach) {
res.data.retval.forEach((el, i) => {
el.id = i + events.length;
el.color = '#CCCCCC';
el.start = new Date(el.datum + ' ' + this.stunden[el.stunde].beginn);
el.end = new Date(el.datum + ' ' + this.stunden[el.stunde].ende);
el.title = el.lehrfach;
if (el.lehrform)
el.title += '-' + el.lehrform;
});
events = [...events, ...res.data.retval];
}
this.events = events;
});
});
});
}
});
app.config.unwrapInjectedRef = true;
app.mount('#content');
+17
View File
@@ -0,0 +1,17 @@
import CalendarHeader from './Header';
export default {
components: {
CalendarHeader
},
inject: [
'date',
'focusDate',
'size'
],
emits: [
'update:mode',
'change:range',
'input'
]
}
+131
View File
@@ -0,0 +1,131 @@
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 CalendarMinimized from './Minimized.js';
import CalendarDate from '../../composables/CalendarDate.js';
// TODO(chris): week/month toggle
export default {
components: {
CalendarMonth,
CalendarMonths,
CalendarYears,
CalendarWeek,
CalendarWeeks,
CalendarMinimized
},
provide() {
return {
date: this.date,
focusDate: this.focusDate,
size: Vue.computed({ get: () => this.size, set: v => this.size = v}),
events: Vue.computed(() => this.eventsPerDay),
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)
};
},
props: {
events: Array,
showWeeks: {
type: Boolean,
default: true
},
initialMode: {
type: String,
default: 'month'
},
minimized: Boolean,
noWeekView: Boolean,
noMonthView: Boolean
},
emits: [
'select:day',
'select:event',
'change:range',
'update:minimized'
],
data() {
return {
header: '',
prevMode: null,
currMode: null,
date: new CalendarDate(),
focusDate: new CalendarDate(),
size: 0
}
},
computed: {
sizeClass() {
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;
}, {});
}
},
methods: {
handleInput(day) {
this.$emit(day[0], day[1]);
}
},
created() {
const allowedInitialModes = ['years'];
if (!this.noWeekView)
allowedInitialModes.push('week');
if (!this.noMonthView)
allowedInitialModes.push('month');
this.mode = allowedInitialModes[allowedInitialModes.indexOf(this.initialMode)] || allowedInitialModes.pop();
this.date.set(new Date(2023,0,1));
this.focusDate.set(this.date);
},
mounted() {
if (this.$refs.container) {
new ResizeObserver(entries => {
for (const entry of entries) {
let w = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width;
// TODO(chris): rework sizing
if (w > 600)
this.size = 3;
else if (w > 350)
this.size = 2;
else if (w > 250)
this.size = 1;
else
this.size = 0;
}
}).observe(this.$refs.container);
}
},
template: `
<div ref="container" class="fhc-calendar card" :class="sizeClass">
<component :is="'calendar-' + mode" @update:mode="mode=$event" @change:range="$emit('change:range',$event)" @input="handleInput" />
</div>`
}
+23
View File
@@ -0,0 +1,23 @@
export default {
inject: [
'eventsAreNull',
'size'
],
props: {
title: String
},
emits: [
'prev',
'next',
'click'
],
template: `
<div class="calendar-header card-header btn-group w-100">
<button class="btn btn-outline-secondary border-0 flex-grow-0" :class="{'btn-sm': !this.size}" @click="$emit('prev')"><i class="fa fa-chevron-left"></i></button>
<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>
<button class="btn btn-outline-secondary border-0 flex-grow-0" :class="{'btn-sm': !this.size}" @click="$emit('next')"><i class="fa fa-chevron-right"></i></button>
</div>`
}
@@ -0,0 +1,29 @@
import CalendarAbstract from './Abstract.js';
export default {
mixins: [
CalendarAbstract
],
inject: [
'size',
'minimized',
'date'
],
data() {
return {
start: 0
}
},
methods: {
maximize() {
// NOTE(chris): set "app.config.unwrapInjectedRef = true" for this to work
this.minimized = false;
}
},
template: `
<div class="fhc-calendar-minimized">
<div class="card-header d-grid">
<button class="btn btn-link link-secondary text-decoration-none" @click="maximize">{{ date.format({dateStyle: ['long','full','full','full'][this.size]}) }}</button>
</div>
</div>`
}
+69
View File
@@ -0,0 +1,69 @@
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'});
}
},
methods: {
paneChanged(dir) {
if (this.syncOnNextChange) {
this.syncOnNextChange = false;
this.focusDate.set(this.date);
} else {
this.focusDate.m += 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();
},
next() {
this.$refs.pane.next();
},
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" @click="$emit('update:mode', 'months')" />
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-month-page :year="focusDate.y" :month="focusDate.m+slot.offset" @update:mode="$emit('update:mode', $event)" @page:back="prev" @page:forward="next" @input="selectDay" />
</calendar-pane>
</div>`
}
@@ -0,0 +1,75 @@
import CalendarDate from '../../../composables/CalendarDate.js';
export default {
inject: [
'date',
'focusDate',
'size',
'events',
'showWeeks',
'noWeekView'
],
props: {
year: Number,
month: Number
},
emits: [
'update:mode',
'page:back',
'page:forward',
'input'
],
computed: {
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[res.length ? 0 : 6]);
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: {
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('update:mode', 'week');
}
}
},
template: `
<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">
{{day.toLocaleString(undefined, {weekday: size < 1 ? 'narrow' : (size < 3 ? 'short' : 'long')})}}
</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="#" v-for="day in week.days" :key="day" class="fhc-calendar-month-page-day text-decoration-none overflow-hidden" :class="{active:date.compare(day),'opacity-50':day.getMonth() != month}" @click.prevent="selectDay(day)">
<span class="no">{{day.getDate()}}</span>
<span v-if="events[day.toDateString()] && events[day.toDateString()].length" class="events">
<span v-for="event in events[day.toDateString()]" :key="event.id" style="color:white" :style="{'background-color': event.color}">
{{event.title}}
</span>
</span>
</a>
</template>
</div>`
}
+34
View File
@@ -0,0 +1,34 @@
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(undefined, {month: this.size < 2 ? 'short' : 'long'}));
}
},
template: `
<div class="fhc-calendar-months">
<calendar-header :title="title" @prev="focusDate.y--" @next="focusDate.y++" @click="$emit('update:mode', 'years')" />
<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('update:mode', 'month')" class="btn btn-outline-secondary" :class="{'border-0': key != focusDate.m}">
{{month}}
</button>
</div>
</div>
</div>`
}
+55
View File
@@ -0,0 +1,55 @@
export default {
emits: [
'slid'
],
data() {
return {
carousel: null,
queue: 0,
offset: 0
}
},
computed: {
offsets() {
return [...Array(3).keys()].map(i => (3+i-this.offset)%3-1);
}
},
methods: {
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();
}
}
},
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
});
}
},
template: `
<div ref="carousel" class="calendar-pane carousel slide" @[\`slid.bs.carousel\`]="slid" :data-queue="queue">
<div class="carousel-inner">
<div v-for="i in [...Array(3).keys()]" :key="i" class="carousel-item">
<slot :index="i" :offset="offsets[i]" />
</div>
</div>
</div>`
}
+48
View File
@@ -0,0 +1,48 @@
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.format({year: 'numeric'}) + ' 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();
},
next() {
this.$refs.pane.next();
},
selectEvent(event) {
this.$emit('input', ['select:event',event]);
}
},
created() {
this.emitRangeChanged();
},
template: `
<div class="fhc-calendar-week">
<calendar-header :title="title" @prev="prev" @next="next" @click="$emit('update:mode', 'weeks')" />
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-week-page :year="focusDate.y" :week="focusDate.w+slot.offset" @update:mode="$emit('update:mode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" />
</calendar-pane>
</div>`
}
+115
View File
@@ -0,0 +1,115 @@
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 {
inject: [
'date',
'focusDate',
'size',
'events',
'noMonthView'
],
props: {
year: Number,
week: Number
},
emits: [
'update:mode',
'page:back',
'page:forward',
'input'
],
data() {
return {
hours: [...Array(24).keys()]
};
},
computed: {
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() {
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};
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];
}
},
methods: {
changeToMonth(day) {
if (!this.noMonthView) {
this.date.set(day);
this.focusDate.set(day);
this.$emit('update:mode', 'month');
}
},
dateToMinutesOfDay(day) {
return Math.floor((day.getHours() * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
}
},
mounted() {
setTimeout(() => this.$refs.eventcontainer.scrollTop = this.$refs.eventcontainer.scrollHeight / 3 + 1, 0);
},
template: `
<div class="fhc-calendar-week-page">
<div class="d-flex flex-column border-top">
<div class="fhc-calendar-week-page-header border-2 border-bottom text-center d-flex">
<div v-for="day in days" :key="day" class="flex-grow-1" :title="day.toLocaleString(undefined, {dateStyle:'short'})">
<div class="fw-bold">{{day.toLocaleString(undefined, {weekday: size < 2 ? 'narrow' : (size < 3 ? 'short' : 'long')})}}</div>
<a href="#" class="small text-secondary text-decoration-none" @click.prevent="changeToMonth(day)">{{day.toLocaleString(undefined, [{day:'numeric',month:'numeric'},{day:'numeric',month:'numeric'},{day:'numeric',month:'numeric'},{dateStyle:'short'}][this.size])}}</a>
</div>
</div>
<div ref="eventcontainer" class="flex-grow-1 overflow-auto">
<div class="events">
<div class="hours">
<div v-for="hour in hours" :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="{'grid-template-columns': 'repeat(' + day.lanes + ', 1fr)', 'grid-template-rows': 'repeat(' + (1440 / smallestTimeFrame) + ', 1fr)'}">
<a href="#" v-for="event in day.events" :key="event" class="small rounded overflow-hidden text-decoration-none text-dark" :style="{'grid-column-start': 1+(event.lane-1)*day.lanes/event.maxLane, 'grid-column-end': 1+event.lane*day.lanes/event.maxLane, 'grid-row-start': dateToMinutesOfDay(event.start), 'grid-row-end': dateToMinutesOfDay(event.end), '--test': dateToMinutesOfDay(event.end), background: event.orig.color}" @click.prevent="$emit('input', event.orig)">
{{event.orig.title}}
</a>
</div>
</div>
</div>
</div>
</div>`
}
+39
View File
@@ -0,0 +1,39 @@
import CalendarAbstract from './Abstract.js';
export default {
mixins: [
CalendarAbstract
],
inject: [
'size',
'focusDate'
],
data() {
return {
weeks: [...Array(this.focusDate.numWeeks).keys()].map(i => i+1)
}
},
computed: {
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('update:mode', 'week');
}
},
template: `
<div class="fhc-calendar-weeks">
<calendar-header :title="title" @prev="focusDate.y--" @next="focusDate.y++" @click="$emit('update:mode', 'years')" />
<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>`
}
+59
View File
@@ -0,0 +1,59 @@
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();
},
next() {
this.$refs.pane.next();
}
},
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" @click="$emit('update:mode')" />
<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" @update:mode="$emit('update:mode')"/>
</calendar-pane>
</div>`
}
@@ -0,0 +1,29 @@
export default {
inject: [
'focusDate'
],
props: {
start: Number,
end: Number
},
emits: [
'update:mode'
],
data() {
return {
}
},
computed: {
years() {
return [...Array(this.end - this.start).keys()].map(i => i + this.start);
}
},
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('update:mode')">
{{year}}
</button>
</div>
</div>`
}
+161
View File
@@ -0,0 +1,161 @@
class CalendarDate {
constructor(y, m, d) {
this.weekStart = CalendarDate.getWeekStart();
this.set(y, m, d);
this._clean();
}
get y() { return this._y }
set y(v) { this._y = v; this._clean() }
get m() { return this._m }
set m(v) { this._m = v; this._clean() }
get d() { return this._d }
set d(v) { this._d = v; this._clean() }
/**
* @see https://www.smart-rechner.de/kalenderwochen/rechner.php
*/
get w() {
if (this._w === null) {
if (this.weekStart == 1 && this._m == 11 && this._d > 28 && this.wd <= this._d-29) {
this._w = 1;
} else if (this.weekStart == 1 && this._m == 0 && this._d < 4 && 3-this.wd <= -this._d) {
let weekStartOfTheYear = new Date(this.y-1, 0, this.weekStart == 1 ? 4 : 1);
weekStartOfTheYear.setDate(weekStartOfTheYear.getDate() - (weekStartOfTheYear.getDay() + 7 - this.weekStart)%7);
this._w = Math.ceil((Math.floor((new Date(this.y, this.m, this.d) - weekStartOfTheYear) / 86400000) + 1) / 7);
} else {
let weekStartOfTheYear = new Date(this.y, 0, this.weekStart == 1 ? 4 : 1);
weekStartOfTheYear.setDate(weekStartOfTheYear.getDate() - (weekStartOfTheYear.getDay() + 7 - this.weekStart)%7);
this._w = Math.ceil((Math.floor((new Date(this.y, this.m, this.d) - weekStartOfTheYear) / 86400000) + 1) / 7);
}
}
return this._w;
}
set w(v) {
if (this.w != v) {
let lw = this.numWeeks;
this.d += (v - this.w) * 7;
if (v > 0 && v <= lw && this.w != v) {
if (this.weekStart != 1) {
if (this.w == 1) {
this.set(this.firstDayOfWeek);
} else {
this.set(this.lastDayOfWeek);
}
}
if (this.w != v) {
console.error('couldn\'t set the week', this, v);
}
}
}
}
get wd() {
if (this._wd === null) {
this._wd = ((new Date(this.y, this.m, this.d)).getDay()+7-this.weekStart)%7;
}
return this._wd;
}
get firstDayOfWeek() {
let firstDayOfWeek = new Date(this.y, this.m, this.d);
firstDayOfWeek.setDate(this.d -(firstDayOfWeek.getDay()+7-this.weekStart)%7);
return firstDayOfWeek;
}
get cdFirstDayOfWeek() {
return new CalendarDate(this.firstDayOfWeek);
}
get lastDayOfWeek() {
let lastDayOfWeek = new Date(this.y, this.m, this.d);
lastDayOfWeek.setDate(this.d -(lastDayOfWeek.getDay()+7-this.weekStart)%7 +6);
return lastDayOfWeek;
}
get cdLastDayOfWeek() {
return new CalendarDate(this.lastDayOfWeek);
}
get firstDayOfCalendarMonth() {
let firstDayOfMonth = new Date(this.y, this.m, 1);
return new Date(this.y, this.m, 1-(firstDayOfMonth.getDay() + 7 - this.weekStart)%7);
}
get cdFirstDayOfCalendarMonth() {
let firstDayOfMonth = new Date(this.y, this.m, 1);
return new CalendarDate(this.y, this.m, 1-(firstDayOfMonth.getDay() + 7 - this.weekStart)%7);
}
get lastDayOfCalendarMonth() {
let lastDayOfMonth = new Date(this.y, this.m+1, 0);
return new Date(lastDayOfMonth.getFullYear(), lastDayOfMonth.getMonth(), lastDayOfMonth.getDate()+6-(lastDayOfMonth.getDay() + 7 - this.weekStart)%7);
}
get cdLastDayOfCalendarMonth() {
let lastDayOfMonth = new Date(this.y, this.m+1, 0);
return new CalendarDate(lastDayOfMonth.getFullYear(), lastDayOfMonth.getMonth(), lastDayOfMonth.getDate()+6-(lastDayOfMonth.getDay() + 7 - this.weekStart)%7);
}
get numWeeks() {
return (new CalendarDate(this.y+1,0,this.weekStart == 1 ? -3 : 0)).w;
}
set(y,m,d,noClean) {
if (y !== undefined && (m === undefined || m === true) && d === undefined) {
if (Object.prototype.toString.call(y) === '[object Date]')
return this.set(y.getFullYear(), y.getMonth(), y.getDate(), m);
if (y.y !== undefined && y.m !== undefined && y.d !== undefined)
return this.set(y.y, y.m, y.d, m);
}
[this._y,this._m,this._d] = [y || 0, m || 0, d || 0];
if (!noClean)
this._clean();
}
_clean() {
this.set(new Date(this._y, this._m, this._d), true);
this._w = null;
this._wd = null;
}
format(options) {
return (new Date(this._y, this._m, this._d)).toLocaleString(undefined, options);
}
compare(d) {
if (Object.prototype.toString.call(d) === '[object Date]')
return (this.y === d.getFullYear() && this.m === d.getMonth() && this.d === d.getDate());
return (this.y === d.y && this.m === d.m && this.d === d.d);
}
isInWeek(w, y) {
if (this.y == y && this.w == w)
return true;
if (this.weekStart == 1)
return false;
let edgeDay = this.cdFirstDayOfWeek;console.log(edgeDay);
if (edgeDay.y == y && edgeDay.w == w)
return true;
edgeDay = this.cdLastDayOfWeek;
if (edgeDay.y == y && edgeDay.w == w)
return true;
return false;
}
setLocale(locale) {
this.weekStart = CalendarDate.getWeekStart(locale);
}
}
/**
* Returns the weekday number (Date.getDay()) on which the week starts depending on the locale.
* This can be Saturday(6), Sunday(0) or Monday(1)
*
* @see https://stackoverflow.com/questions/53382465/how-can-i-determine-if-week-starts-on-monday-or-sunday-based-on-locale-in-pure-j
*
* @param string locale
*
* @return integer
*/
CalendarDate.getWeekStart = function(locale) {
locale = locale || navigator.language;
const parts = locale.match(/^([a-z]{2,3})(?:-([a-z]{3})(?=$|-))?(?:-([a-z]{4})(?=$|-))?(?:-([a-z]{2}|\d{3})(?=$|-))?/i);
const regionSat = 'AEAFBHDJDZEGIQIRJOKWLYOMQASDSY'.match(/../g);
const regionSun = 'AGARASAUBDBRBSBTBWBZCACNCODMDOETGTGUHKHNIDILINJMJPKEKHKRLAMHMMMOMTMXMZNINPPAPEPHPKPRPTPYSASGSVTHTTTWUMUSVEVIWSYEZAZW'.match(/../g);
const languageSat = ['ar','arq','arz','fa'];
const languageSun = 'amasbndzengnguhehiidjajvkmknkolomhmlmrmtmyneomorpapssdsmsnsutatethtnurzhzu'.match(/../g);
return (
parts[4] ? (
regionSun.includes(parts[4]) ? 0 :
regionSat.includes(parts[4]) ? 6 : 1) : (
languageSun.includes(parts[1]) ? 0 :
languageSat.includes(parts[1]) ? 6 : 1));
}
export default CalendarDate