diff --git a/application/controllers/Cis/Stundenplan.php b/application/controllers/Cis/Stundenplan.php
new file mode 100644
index 000000000..1f1eca42e
--- /dev/null
+++ b/application/controllers/Cis/Stundenplan.php
@@ -0,0 +1,31 @@
+ ['student/anrechnung_beantragen:r','user:r'] // TODO(chris): permissions?
+ ]);
+ }
+
+ // -----------------------------------------------------------------------------------------------------------------
+ // Public methods
+
+ /**
+ * @return void
+ */
+ public function index()
+ {
+ $this->load->view('Cis/Stundenplan');
+ }
+
+}
diff --git a/application/controllers/components/Cis/Stundenplan.php b/application/controllers/components/Cis/Stundenplan.php
new file mode 100644
index 000000000..2b35e11ca
--- /dev/null
+++ b/application/controllers/components/Cis/Stundenplan.php
@@ -0,0 +1,68 @@
+ ['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));
+ }
+
+}
diff --git a/application/models/ressource/Reservierung_model.php b/application/models/ressource/Reservierung_model.php
index 37d0cb376..7a3d1a4a4 100644
--- a/application/models/ressource/Reservierung_model.php
+++ b/application/models/ressource/Reservierung_model.php
@@ -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]);
+ }
+
}
diff --git a/application/models/ressource/Stundenplan_model.php b/application/models/ressource/Stundenplan_model.php
index be3d520b5..5ec6ea302 100644
--- a/application/models/ressource/Stundenplan_model.php
+++ b/application/models/ressource/Stundenplan_model.php
@@ -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]);
+ }
+
}
diff --git a/application/views/Cis/Stundenplan.php b/application/views/Cis/Stundenplan.php
new file mode 100644
index 000000000..f4b3dbc15
--- /dev/null
+++ b/application/views/Cis/Stundenplan.php
@@ -0,0 +1,16 @@
+ ['public/js/apps/Cis/Stundenplan.js'],
+ 'customCSSs' => ['public/css/components/calendar.css']
+);
+
+$this->load->view('templates/CISHTML-Header', $includesArray);
+?>
+
+
+
Stundenplan
+
+
+
+
+load->view('templates/CISHTML-Footer', $includesArray); ?>
diff --git a/public/css/components/calendar.css b/public/css/components/calendar.css
new file mode 100644
index 000000000..f401f9cb7
--- /dev/null
+++ b/public/css/components/calendar.css
@@ -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 */
diff --git a/public/js/apps/Cis/Stundenplan.js b/public/js/apps/Cis/Stundenplan.js
new file mode 100644
index 000000000..eaa75bca3
--- /dev/null
+++ b/public/js/apps/Cis/Stundenplan.js
@@ -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');
\ No newline at end of file
diff --git a/public/js/components/Calendar/Abstract.js b/public/js/components/Calendar/Abstract.js
new file mode 100644
index 000000000..07eb03933
--- /dev/null
+++ b/public/js/components/Calendar/Abstract.js
@@ -0,0 +1,17 @@
+import CalendarHeader from './Header';
+
+export default {
+ components: {
+ CalendarHeader
+ },
+ inject: [
+ 'date',
+ 'focusDate',
+ 'size'
+ ],
+ emits: [
+ 'update:mode',
+ 'change:range',
+ 'input'
+ ]
+}
diff --git a/public/js/components/Calendar/Calendar.js b/public/js/components/Calendar/Calendar.js
new file mode 100644
index 000000000..73e11fa20
--- /dev/null
+++ b/public/js/components/Calendar/Calendar.js
@@ -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: `
+
+
+
`
+}
diff --git a/public/js/components/Calendar/Header.js b/public/js/components/Calendar/Header.js
new file mode 100644
index 000000000..0ead55215
--- /dev/null
+++ b/public/js/components/Calendar/Header.js
@@ -0,0 +1,23 @@
+export default {
+ inject: [
+ 'eventsAreNull',
+ 'size'
+ ],
+ props: {
+ title: String
+ },
+ emits: [
+ 'prev',
+ 'next',
+ 'click'
+ ],
+ template: `
+ `
+}
diff --git a/public/js/components/Calendar/Minimized.js b/public/js/components/Calendar/Minimized.js
new file mode 100644
index 000000000..dac648324
--- /dev/null
+++ b/public/js/components/Calendar/Minimized.js
@@ -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: `
+
+
+
`
+}
diff --git a/public/js/components/Calendar/Month.js b/public/js/components/Calendar/Month.js
new file mode 100644
index 000000000..a86ce10b6
--- /dev/null
+++ b/public/js/components/Calendar/Month.js
@@ -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: `
+
+
+
+
+
+
`
+}
diff --git a/public/js/components/Calendar/Month/Page.js b/public/js/components/Calendar/Month/Page.js
new file mode 100644
index 000000000..5cb9cdc53
--- /dev/null
+++ b/public/js/components/Calendar/Month/Page.js
@@ -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: `
+
+
+
+ {{day.toLocaleString(undefined, {weekday: size < 1 ? 'narrow' : (size < 3 ? 'short' : 'long')})}}
+
+
+ {{week.no}}
+
+ {{day.getDate()}}
+
+
+ {{event.title}}
+
+
+
+
+
`
+}
diff --git a/public/js/components/Calendar/Months.js b/public/js/components/Calendar/Months.js
new file mode 100644
index 000000000..7dcb96d87
--- /dev/null
+++ b/public/js/components/Calendar/Months.js
@@ -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: `
+ `
+}
diff --git a/public/js/components/Calendar/Pane.js b/public/js/components/Calendar/Pane.js
new file mode 100644
index 000000000..bbc21af0c
--- /dev/null
+++ b/public/js/components/Calendar/Pane.js
@@ -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: `
+ `
+}
diff --git a/public/js/components/Calendar/Week.js b/public/js/components/Calendar/Week.js
new file mode 100644
index 000000000..ca91382f5
--- /dev/null
+++ b/public/js/components/Calendar/Week.js
@@ -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: `
+
+
+
+
+
+
`
+}
diff --git a/public/js/components/Calendar/Week/Page.js b/public/js/components/Calendar/Week/Page.js
new file mode 100644
index 000000000..47d48b7f7
--- /dev/null
+++ b/public/js/components/Calendar/Week/Page.js
@@ -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: `
+ `
+}
diff --git a/public/js/components/Calendar/Weeks.js b/public/js/components/Calendar/Weeks.js
new file mode 100644
index 000000000..1ec8545bc
--- /dev/null
+++ b/public/js/components/Calendar/Weeks.js
@@ -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: `
+ `
+}
diff --git a/public/js/components/Calendar/Years.js b/public/js/components/Calendar/Years.js
new file mode 100644
index 000000000..a372f748d
--- /dev/null
+++ b/public/js/components/Calendar/Years.js
@@ -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: `
+
+
+
+
+
+
`
+}
diff --git a/public/js/components/Calendar/Years/Page.js b/public/js/components/Calendar/Years/Page.js
new file mode 100644
index 000000000..b65471f82
--- /dev/null
+++ b/public/js/components/Calendar/Years/Page.js
@@ -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: `
+ `
+}
diff --git a/public/js/composables/CalendarDate.js b/public/js/composables/CalendarDate.js
new file mode 100644
index 000000000..1b311739c
--- /dev/null
+++ b/public/js/composables/CalendarDate.js
@@ -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
\ No newline at end of file