>
diff --git a/composer.json b/composer.json
index c0dd03abf..370c2eda1 100644
--- a/composer.json
+++ b/composer.json
@@ -414,6 +414,17 @@
}
}
},
+ {
+ "type": "package",
+ "package": {
+ "name": "moment/luxonjs",
+ "version": "3.6.1",
+ "dist": {
+ "url": "https://unpkg.com/luxon@3.6.1/build/global/luxon.min.js",
+ "type": "file"
+ }
+ }
+ },
{
"type": "composer",
"url": "https://asset-packagist.org"
@@ -462,6 +473,7 @@
"michelf/php-markdown": "1.5.0",
"ml/json-ld": "1.*",
+ "moment/luxonjs": "3.6.1",
"moment/momentjs": "2.24.0",
"mottie/tablesorter": "2.*",
diff --git a/composer.lock b/composer.lock
index 70044eb58..f2ce3923c 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "1b8a408a43c62cb966fa89af338564a7",
+ "content-hash": "1de37a74ba51a66057eb2712b21340c8",
"packages": [
{
"name": "afarkas/html5shiv",
@@ -1296,6 +1296,15 @@
},
"time": "2022-09-29T08:45:17+00:00"
},
+ {
+ "name": "moment/luxonjs",
+ "version": "3.6.1",
+ "dist": {
+ "type": "file",
+ "url": "https://unpkg.com/luxon@3.6.1/build/global/luxon.min.js"
+ },
+ "type": "library"
+ },
{
"name": "moment/momentjs",
"version": "2.24.0",
diff --git a/public/css/Cis4/CoreCalendarEvents.css b/public/css/Cis4/CoreCalendarEvents.css
index d886e024e..c990c93d2 100644
--- a/public/css/Cis4/CoreCalendarEvents.css
+++ b/public/css/Cis4/CoreCalendarEvents.css
@@ -143,27 +143,27 @@
.dayPageContainer .feiertagEventContent #ferienEventTitle {}
-/* Minimized Container Styles */
+/* List Page Container Styles */
-/* Ferientage Events for Minimized */
+/* Ferientage Events for List Page */
-.minimizedContainer .feiertagEventContent{
+.listPageContainer .feiertagEventContent{
display: flex;
justify-content: center;
align-items: center;
color: var(--fhc-light);
}
-.minimizedContainer .feiertagEventContent #ferienEventIcon{
+.listPageContainer .feiertagEventContent #ferienEventIcon{
margin: 0.5rem;
}
-.minimizedContainer .feiertagEventContent #ferienEventTitle{
+.listPageContainer .feiertagEventContent #ferienEventTitle{
flex-grow: 1;
}
-/* Lehreinheits Events in Minimized */
-.dayPageContainer .lehreinheitEventContent {
+/* Lehreinheits Events in List Page */
+.listPageContainer .lehreinheitEventContent {
display: flex;
padding-left: 4rem;
justify-content: space-evenly;
@@ -171,15 +171,15 @@
}
-.dayPageContainer .lehreinheitEventContent #lehreinheitEventHeader {
+.listPageContainer .lehreinheitEventContent #lehreinheitEventHeader {
display: grid;
align-content: center;
}
-.dayPageContainer .lehreinheitEventContent #lehreinheitEventText {
+.listPageContainer .lehreinheitEventContent #lehreinheitEventText {
display: contents;
}
-.dayPageContainer .lehreinheitEventContent #lehreinheitEventText * {
+.listPageContainer .lehreinheitEventContent #lehreinheitEventText * {
font-weight: 500;
-}
\ No newline at end of file
+}
diff --git a/public/css/components/calendar.css b/public/css/components/calendar.css
index cb570cc46..f46554007 100644
--- a/public/css/components/calendar.css
+++ b/public/css/components/calendar.css
@@ -1,441 +1,38 @@
-:root{
- --fhc-calendar-pane-height: calc(100vh - 220px);
-
- --fhc-calendar-primary: var(--fhc-primary, #006095);
+@import './calendar/frame.css';
+@import './calendar/events.css';
+@import './calendar/backgrounds.css';
+
+/* Themable Variables */
+:root {
+ --fhc-calendar-gap: var(--fhc-border, #dee2e6);
+ --fhc-calendar-bg: var(--fhc-background, #fff);
--fhc-calendar-border: var(--fhc-border, #dee2e6);
- --fhc-calendar-border-highlight: var(--fhc-border-highlight, #495057);
- --fhc-calendar-text: var(--fhc-text, #212529);
- --fhc-calendar-text-light: var(--fhc-text, #212529);
- --fhc-calendar-background: var(--fhc-background, #fff);
- --fhc-calendar-dark: var(--fhc-dark, #212529);
- --fhc-calendar-hour-indicator-bg: var(--fhc-background, #fff);
- --fhc-calendar-week-page-header-background: var(--fhc-background, #fff);
- --fhc-calendar-week-page-header-color: var(--fhc-text, #212529);
- --fhc-calendar-week-page-header-border: var(--fhc-border, #dee2e6);
- --fhc-calendar-week-page-header-hover-background: var(--fhc-background-highlight, #d5dae0);
- --fhc-calendar-all-day-event-background: var(--fhc-background, #fff);
- --fhc-calendar-past: var(--fhc-beige-10, rgba(245, 233, 215, 0.5));
- --fhc-calendar-box-shadow: var(--fhc-box-shadow, #dee2e6);
-}
-:root.dark{
- --fhc-calendar-past: var(--fhc-beige-20, rgba(172, 153, 125, 0.5));
-}
+ --fhc-calendar-bg-event: var(--fhc-white-80, #ccc);
+ --fhc-calendar-border-event: var(--bs-secondary, #6c757d);
+ --fhc-calendar-radius-event: var(--bs-border-radius, .375rem);
+ --fhc-calendar-fontsize-event: .875em;
-.fhc-calendar-pane {
- height: var(--fhc-calendar-pane-height);
-}
+ --fhc-calendar-bg-markings-past: var(--fhc-beige-10, rgba(245, 233, 215, 0.5));
+ --fhc-calendar-border-markings-past: var(--fhc-calendar-border);
+ --fhc-calendar-bg-markings-past-label: var(--fhc-calendar-bg);
+ --fhc-calendar-border-markings-past-label: var(--fhc-calendar-border);
-.fhc-calendar-hour-indicator{
- border-top: 1px solid var(--fhc-calendar-border);
-}
+ --fhc-calendar-gap-header: transparent;
+ --fhc-calendar-gap-allday: transparent;
+ --fhc-calendar-gap-body: transparent;
+ --fhc-calendar-bg-header: var(--fhc-calendar-bg);
+ --fhc-calendar-bg-allday: var(--fhc-calendar-bg);
+ --fhc-calendar-bg-body: var(--fhc-calendar-bg);
-.fhc-calendar-hour-indicator span{
- background-color: var(--fhc-calendar-hour-indicator-bg) !important;
-}
-
-.fhc-calendar-week-page-header{
- color: var(--fhc-calendar-text);
- background-color: var(--fhc-calendar-week-page-header-background);
-}
-
-.fhc-calendar-week-page-header > div {
- border-right: 1px solid var(--fhc-calendar-border);
-}
-
-.fhc-calendar-week-page-header > div:hover{
- background-color: var(--fhc-calendar-week-page-header-hover-background);
-}
-
-.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);*/
- max-height: 100%;
-}
-.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%;*/
-}
-.fhc-calendar-week-page .events .day {
- gap: 1px;
-}
-.fhc-calendar-week-page .events .hours, .fhc-calendar-week-page .events .day {
- display: grid;
-}
-
-.fhc-calendar-week-page .all-day-event-border{
- box-shadow: 1px 1px 0 var(--fhc-calendar-box-shadow);
-}
-
-.fhc-calendar-week-page .all-day-event {
- max-height: 75px;
- overflow: auto;
- overscroll-behavior: none;
-}
-
-.fhc-calendar-week-page .all-day-event-container{
- position: sticky;
- top: 44px;
- display:grid;
- grid-template-columns:repeat(7,1fr);
- width:100%;
- z-index:3;
- background-color:var(--fhc-calendar-all-day-event-background);
-}
-
-.fhc-calendar-week-page .all-day-event-container::before {
- position: absolute;
- content:'';
- top: 0;
- bottom: 0;
- left: -3em;
- right: 100%;
- background-color: var(--fhc-calendar-all-day-event-background);
- box-shadow: 1px 1px 0 var(--fhc-calendar-box-shadow);
-}
-
-.fhc-calendar-day-page .all-day-event-border {
- box-shadow: 0 0 0 1px var(--fhc-calendar-box-shadow);
-}
-
-.fhc-calendar-day-page .all-day-event {
- grid-column:2;
- max-height: 75px;
- overflow: auto;
-}
-
-.fhc-calendar-day-page .all-day-event-container {
- position: sticky;
- top:0;
- display: grid;
- grid-template-columns: 3em 1fr;
- width: 100%;
- z-index: 3;
- background-color:var(--fhc-calendar-all-day-event-background);
- box-shadow: 1px 1px 0 var(--fhc-calendar-box-shadow);
-}
-
-/* grid hour lines of the Stundenplan use box-shadow instead of border because box-shadow renders consistently on different viewports*/
-.box-shadow-border{
- box-shadow: 0 0 0 1px var(--fhc-calendar-border) !important;
-}
-
-.fhc-calendar-no-events-overlay{
- position: relative;
-}
-
-.fhc-calendar-no-events-overlay::before {
- content: "";
- position: absolute;
- top: 0;
- left: 3rem;
- margin:auto;
- width: 100%;
- height: 100%;
- background-image: linear-gradient(120deg, var(--fhc-calendar-background), var(--fhc-calendar-dark) );
- opacity: .7;
-}
-.fhc-calendar-day-page {
- /*aspect-ratio: 7/6;*/
- min-height: 0;
-}
-.fhc-calendar-day-page > div {
- height: calc(100% + 1.5em);
-}
-.fhc-calendar-day-page .flex-column > .flex-grow-1 {
- padding-left: 3em;
-}
-
-.fhc-calendar-day-page .events {
- display: grid;
- grid-template-columns: 3em 1fr;
-}
-.fhc-calendar-day-page .events .day {
- gap: 1px;
-}
-.fhc-calendar-day-page .events .hours, .fhc-calendar-day-page .events .day {
- display: grid;
-}
-
-.fhc-calendar-month-page-day{
- color: var(--fhc-calendar-text) !important;
-}
-
-.fhc-calendar-month-page-day.fhc-calendar-past:not(.fhc-calendar-month-page-day-focusday) {
- color: var(--fhc-calendar-text) !important;
-}
-
-.fhc-calendar-lg .fhc-calendar-month-page-day,
-.fhc-calendar-md .fhc-calendar-month-page-day {
- border: solid 1px var(--fhc-calendar-border);
-}
-
-.fhc-calendar-lg .fhc-calendar-month-page-weekday,
-.fhc-calendar-md .fhc-calendar-month-page-weekday{
- border: solid 1px var(--fhc-secondary);
-}
-
-
-.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(--fhc-calendar-border-highlight);
-}
-/*.fhc-calendar-lg .fhc-calendar-month-page-day .events,*/
-/*.fhc-calendar-md .fhc-calendar-month-page-day .events {*/
-/* display: block;*/
-/* overflow: auto;*/
-/* 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-month-page-weekday:hover {
- background-color:var(--fhc-calendar-primary);
- color:var(--fhc-calendar-text-light);
+ --fhc-calendar-bg-month-today: var(--fhc-primary);
+ --fhc-calendar-color-month-today: var(--fhc-white-10);
+ --fhc-calendar-bg-overlay1: var(--fhc-calendar-bg);
+ --fhc-calendar-bg-overlay2: var(--fhc-dark, #212529);
+
+ --fhc-calendar-radius-day-event: var(--fhc-calendar-radius-event);
+ --fhc-calendar-radius-week-event: var(--fhc-calendar-radius-event);
+ --fhc-calendar-radius-month-event: 0;
+ --fhc-calendar-radius-list-event: var(--fhc-calendar-radius-event);
}
-
-.fhc-calendar-sm .fhc-calendar-month-page-day,
-.fhc-calendar-xs .fhc-calendar-month-page-day {
- position: relative;
- aspect-ratio: 1;
-}
-
-
-
-.fhc-calendar-past {
- background-color: var(--fhc-calendar-past);
- border-color: var(--fhc-calendar-border);
- opacity: 0.75;
-}
-
-.fhc-calendar-month-page-day-focusday {
- border-color: var(--fhc-calendar-border-highlight) !important;
- background-color: var(--fhc-calendar-background);
- position: relative;
- animation: dash-animation 2.5s linear infinite;
-}
-
-@keyframes dash-animation {
- 0% {
- border-style: dashed;
- }
- 50% {
- border-style: solid;
- }
- 100% {
- border-style: dashed;
- }
-}
-
-.fhc-highlight-week {
- /*border-color: black !important;*/
-}
-
-.fhc-highlight-day {
- position:relative;
-}
-
-.fhc-highlight-day::before {
- content:'';
- position:absolute;
- top:0;
- bottom:0;
- left:0;
- right:0;
- box-shadow: inset 0 0 0 2px var(--fhc-calendar-border) !important;
- pointer-events: none;
- z-index: 2;
-}
-
-.fhc-calendar-sm .fhc-calendar-month-page-day.active .no,
-.fhc-calendar-xs .fhc-calendar-month-page-day.active .no {
- background-color: var(--fhc-calendar-primary);
- 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: var(--fhc-calendar-primary);
- 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;
-}
-
-.selectedEvent {
- background-color: var(--fhc-calendar-primary) !important;
- color: var(--fhc-calendar-text-light);
-}
-
-.fhc-calendar-day-page .curTimeIndicator{
- position: absolute;
- left: 0;
- right: 0;
- pointer-events: none;
- padding-left: 7rem;
- margin-top: -1px;
- z-index: 2;
- border-color: var(--fhc-calendar-border)!important;
-}
-
-.fhc-calendar-week-page .curTimeIndicator {
- position: absolute;
- left: 0;
- right: 0;
- pointer-events: none;
- padding-left: 1rem;
- margin-top: -1px;
- z-index: 2;
- border-color: var(--fhc-calendar-border) !important;
-}
-
-.fhc-calendar-week-page .past::before{
- content:"";
- background-color: var(--fhc-calendar-past);
- position: absolute;
- pointer-events: none;
- z-index: 2;
- opacity: 0.5;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
-}
-
-.fhc-calendar-week-page .overlay {
- background-color: var(--fhc-calendar-past);
- position: absolute;
- pointer-events: none;
- z-index: 2;
- opacity: 0.5;
-}
-
-.fhc-calendar-day-page .overlay {
- background-color: var(--fhc-calendar-past);
- position: absolute;
- pointer-events: none;
- z-index: 2;
- opacity: 0.5;
-}
-
-.dayPageContainer,
-.weekPageContainer {
- container-type: inline-size;
-}
-
-#calendarHeaderTitle{
- color: var(--fhc-text);
-}
-
-.fhc-calendar-day-page .hours,
-.fhc-calendar-day-page .curTimeIndicator,
-.fhc-calendar-day-page .fhc-calendar-hour-indicator,
-.fhc-calendar-week-page .hours,
-.fhc-calendar-week-page .curTimeIndicator,
-.fhc-calendar-week-page .fhc-calendar-hour-indicator,
-.fhc-calendar-day-page .date,
-.fhc-calendar-week-page .date {
- color: var(--fhc-text-secondary) !important;
-}
\ No newline at end of file
diff --git a/public/css/components/calendar/backgrounds.css b/public/css/components/calendar/backgrounds.css
new file mode 100644
index 000000000..2524be91f
--- /dev/null
+++ b/public/css/components/calendar/backgrounds.css
@@ -0,0 +1,18 @@
+.background-past {
+ background-color: var(--fhc-calendar-bg-markings-past);
+ border-bottom: solid 1px var(--fhc-calendar-border-markings-past);
+}
+.background-past > span {
+ display: none;
+}
+.background-past.bg-end > span {
+ display: block;
+ position: absolute;
+ top: 100%;
+ left: 1rem;
+ padding: 0 .5rem;
+ background-color: var(--fhc-calendar-bg-markings-past-label);
+ border: solid 1px var(--fhc-calendar-border-markings-past-label);
+ font-size: .875em;
+}
+
diff --git a/public/css/components/calendar/events.css b/public/css/components/calendar/events.css
new file mode 100644
index 000000000..f996b7339
--- /dev/null
+++ b/public/css/components/calendar/events.css
@@ -0,0 +1,70 @@
+.event {
+ cursor: pointer;
+}
+.fhc-calendar-mode-week .all-day-events .event .weekPageContainer {
+ padding: .25rem;
+}
+.fhc-calendar-mode-day .event,
+.fhc-calendar-mode-week .event {
+ margin: 0 .5rem;
+ font-size: var(--fhc-calendar-fontsize-event, .875rem);
+}
+.fhc-calendar-mode-day .all-day-events .event,
+.fhc-calendar-mode-week .all-day-events .event {
+ margin: .25rem;
+}
+/* Border */
+.fhc-calendar-mode-day .grid-main .event > *,
+.fhc-calendar-mode-week .grid-main .event > * {
+ border: solid 1px var(--fhc-calendar-border-event);
+}
+.fhc-calendar-mode-day .event {
+ --event-radius: var(--fhc-calendar-radius-day-event, 0);
+}
+.fhc-calendar-mode-week .event {
+ --event-radius: var(--fhc-calendar-radius-week-event, 0);
+}
+.fhc-calendar-mode-month .event {
+ --event-radius: var(--fhc-calendar-radius-month-event, 0);
+}
+.fhc-calendar-mode-list .event {
+ --event-radius: var(--fhc-calendar-radius-list-event, 0);
+}
+.fhc-calendar-mode-day .all-day-events .event > *,
+.fhc-calendar-mode-week .all-day-events .event > *,
+.fhc-calendar-mode-day .event.event-begin > *,
+.fhc-calendar-mode-week .event.event-begin > * {
+ border-top-left-radius: var(--event-radius);
+ border-top-right-radius: var(--event-radius);
+}
+.fhc-calendar-mode-day .all-day-events .event > *,
+.fhc-calendar-mode-week .all-day-events .event > *,
+.fhc-calendar-mode-day .event.event-end > *,
+.fhc-calendar-mode-week .event.event-end > * {
+ border-bottom-left-radius: var(--event-radius);
+ border-bottom-right-radius: var(--event-radius);
+}
+.fhc-calendar-mode-month .event.event-begin > * {
+ border-top-left-radius: var(--event-radius);
+ border-bottom-left-radius: var(--event-radius);
+}
+.fhc-calendar-mode-month .event.event-end > * {
+ border-top-right-radius: var(--event-radius);
+ border-bottom-right-radius: var(--event-radius);
+}
+.fhc-calendar-mode-list .event > * {
+ border-radius: var(--event-radius);
+}
+/* Colors */
+.fhc-calendar-base .event {
+ --event-default-bg: var(--fhc-calendar-bg-event, #ccc);
+}
+.fhc-calendar-base .event .event-type-reservierung {
+ --event-default-bg: var(--fhc-calendar-bg-reservierung, #fff);
+}
+.fhc-calendar-base .event > * {
+ background-color: var(--event-bg, var(--event-default-bg));
+}
+.fhc-calendar-base .event > *:hover {
+ filter: brightness(120%);
+}
diff --git a/public/css/components/calendar/frame.css b/public/css/components/calendar/frame.css
new file mode 100644
index 000000000..ab400c95d
--- /dev/null
+++ b/public/css/components/calendar/frame.css
@@ -0,0 +1,180 @@
+/* Labels */
+/* ====== */
+.fhc-calendar-base-label-dow .short,
+.fhc-calendar-base-label-dow .narrow,
+.fhc-calendar-base-label-day .full,
+.fhc-calendar-base-label-day .short,
+.fhc-calendar-base-label-day .narrow,
+.fhc-calendar-mode-month .fhc-calendar-base-label-day .long,
+.collapsed-header .fhc-calendar-base-label-day .long,
+.collapsed-header .fhc-calendar-base-label-dow .long {
+ display: none;
+}
+.fhc-calendar-mode-month .fhc-calendar-base-label-day .narrow,
+.collapsed-header .fhc-calendar-base-label-day .short,
+.collapsed-header .fhc-calendar-base-label-dow .short {
+ display: block;
+}
+.fhc-calendar-mode-day .fhc-calendar-base-label-day,
+.fhc-calendar-mode-week .fhc-calendar-base-label-day {
+ font-size: .875em;
+}
+.fhc-calendar-mode-day .main-header,
+.fhc-calendar-mode-week .main-header {
+ text-align: center;
+}
+.fhc-calendar-mode-month .fhc-calendar-base-label-week {
+ text-align: right;
+}
+.fhc-calendar-base-label-week {
+ margin-left: .5rem;
+}
+.fhc-calendar-base-label-time,
+.fhc-calendar-base-label-day,
+.fhc-calendar-base-label-week {
+ color: var(--bs-secondary-color);
+}
+.fhc-calendar-mode-day .fhc-calendar-base-label-dow,
+.fhc-calendar-mode-week .fhc-calendar-base-label-dow,
+.fhc-calendar-base-label-day,
+.fhc-calendar-base-label-week {
+ cursor: pointer;
+}
+.fhc-calendar-base-label-week span + span:before {
+ content: "/";
+}
+.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event.event-header {
+ height: 100%;
+}
+.fhc-calendar-mode-month .fhc-calendar-base-label-day {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event.event-header {
+ --event-bg: transparent;
+ font-weight: bold;
+ text-indent: 0;
+ font-size: var(--fhc-calendar-fontsize-month-day, 1rem);
+}
+.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event.event-header .disabled {
+ opacity: .3;
+}
+
+
+/* Base Grid */
+/* ========= */
+/* Grid */
+.fhc-calendar-base-grid {
+ --fhc-calendar-axis-collapsible: .2fr;
+ background-color: var(--fhc-calendar-gap);
+ grid-gap: 2px;
+}
+.fhc-calendar-base-grid .grid-header {
+ background-color: var(--fhc-calendar-gap-header);
+}
+.fhc-calendar-base-grid .grid-header:before,
+.fhc-calendar-base-grid .grid-allday:before {
+ content: "";
+}
+.fhc-calendar-base-grid .grid-header:before,
+.fhc-calendar-base-grid .grid-header .main-header {
+ background-color: var(--fhc-calendar-bg-header);
+}
+.fhc-calendar-base-grid .grid-allday {
+ background-color: var(--fhc-calendar-gap-allday);
+}
+.fhc-calendar-base-grid .grid-allday:before,
+.fhc-calendar-base-grid .grid-allday .all-day-events {
+ background-color: var(--fhc-calendar-bg-allday);
+}
+.fhc-calendar-base-grid .grid-main {
+ grid-gap: 1px;
+ background-color: var(--fhc-calendar-gap-body);
+}
+.fhc-calendar-base-grid .grid-main .part-header,
+.fhc-calendar-base-grid .grid-main .part-body {
+ background-color: var(--fhc-calendar-bg-body);
+}
+.fhc-calendar-base-grid-line {
+ grid-gap: 1px;
+}
+.fhc-calendar-base-label-time {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: end;
+}
+/* Header */
+.fhc-calendar-base-header > .row {
+ align-items: center;
+}
+
+/* Modes */
+/* ===== */
+/* Day */
+.fhc-calendar-mode-day .event-details {
+ padding: 1.5rem;
+ width: 100%;
+ border-left: solid 1px var(--fhc-calendar-gap);
+}
+.fhc-calendar-mode-day .today {
+ background-color: var(--fhc-calendar-bg-day-today);
+ color: var(--fhc-calendar-color-day-today);
+}
+/* Week */
+.fhc-calendar-mode-week .today {
+ background-color: var(--fhc-calendar-bg-week-today);
+ color: var(--fhc-calendar-color-week-today);
+}
+/* Month */
+.fhc-calendar-mode-month .fhc-calendar-base-grid-line {
+ overflow: hidden;
+ line-height: 1.5;
+ grid-template-rows: 2em;
+ grid-auto-rows: calc(var(--fhc-calendar-fontsize-event, .875rem) + 2 * var(--fhc-calendar-margin-event-icon, .5rem));
+ font-size: var(--fhc-calendar-fontsize-event, .875rem);
+}
+.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event {
+ text-indent: .5em;
+ height: calc(var(--fhc-calendar-fontsize-event, .875rem) + 2 * var(--fhc-calendar-margin-event-icon, .5rem));
+ overflow: hidden;
+}
+.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event .placeholder-glow {
+ text-indent: 0;
+}
+.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event * {
+ max-height: 100%;
+ max-width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.fhc-calendar-mode-month .event > .today {
+ background-color: var(--fhc-calendar-bg-month-today);
+ color: var(--fhc-calendar-color-month-today);
+}
+/* List */
+.fhc-calendar-mode-list .today {
+ background-color: var(--fhc-calendar-bg-list-today);
+ color: var(--fhc-calendar-color-list-today);
+}
+
+/* Overlays */
+/* ======== */
+/* No Events */
+.fhc-calendar-no-events-overlay {
+ background: linear-gradient(120deg, var(--fhc-calendar-bg-overlay1), var(--fhc-calendar-bg-overlay2));
+ opacity: .7;
+ text-align: center;
+ font-size: 2.5rem;
+ font-weight: 500;
+ line-height: 1.2;
+}
+.fhc-calendar-no-events-overlay:before {
+ content: "";
+ display: block;
+ height: 100px;
+}
diff --git a/public/css/components/dashboard.css b/public/css/components/dashboard.css
index ae2bb252f..8dc19128d 100644
--- a/public/css/components/dashboard.css
+++ b/public/css/components/dashboard.css
@@ -1,5 +1,6 @@
@import './calendar.css';
@import './dashboard/news.css';
+@import './dashboard/LvPlan.css';
:root{
--fhc-dashboard-danger: var(--fhc-danger, #842029);
diff --git a/public/css/components/dashboard/LvPlan.css b/public/css/components/dashboard/LvPlan.css
new file mode 100644
index 000000000..6e0e4a654
--- /dev/null
+++ b/public/css/components/dashboard/LvPlan.css
@@ -0,0 +1,27 @@
+.dashboard-widget-lvplan .fhc-calendar-base .card {
+ --bs-card-cap-padding-x: 0;
+ --bs-card-cap-padding-y: 0;
+ --bs-card-border-radius: 0;
+ --bs-card-border-width: 0;
+ --dp-input-padding: .375rem .75rem;
+ --dp-input-icon-padding: calc(1rem + 24px);
+}
+.dashboard-widget-lvplan .fhc-calendar-base .card-header {
+ border-bottom-width: var(--bs-border-width);
+}
+.dashboard-widget-lvplan .fhc-calendar-base .card-header > .row > .col-auto,
+.dashboard-widget-lvplan .fhc-calendar-base .card-header .btn-group {
+ width: 100%;
+}
+.dashboard-widget-lvplan .fhc-calendar-base .card-header input {
+ --dp-font-size: .7em;
+}
+.dashboard-widget-lvplan .fhc-calendar-base .card-header .btn {
+ --bs-btn-font-size: .7em;
+}
+.dashboard-widget-lvplan .fhc-calendar-base-label-day {
+ cursor: initial;
+}
+.dashboard-widget-lvplan .fhc-calendar-mode-list-view {
+ padding: 0 1rem;
+}
\ No newline at end of file
diff --git a/public/js/api/factory/lvPlan.js b/public/js/api/factory/lvPlan.js
index d8cad1c01..99f7961f6 100644
--- a/public/js/api/factory/lvPlan.js
+++ b/public/js/api/factory/lvPlan.js
@@ -18,7 +18,7 @@
export default {
getRoomInfo(ort_kurzbz, start_date, end_date) {
return {
- method: 'get',
+ method: 'post',
url: '/api/frontend/v1/LvPlan/getRoomplan',
params: { ort_kurzbz, start_date, end_date }
};
@@ -39,15 +39,15 @@ export default {
},
getOrtReservierungen(ort_kurzbz, start_date, end_date) {
return {
- method: 'get',
- url: `/api/frontend/v1/LvPlan/Reservierungen/${ort_kurzbz}`,
+ method: 'post',
+ url: `/api/frontend/v1/LvPlan/getReservierungen/${ort_kurzbz}`,
params: { start_date, end_date }
};
},
getLvPlanReservierungen(start_date, end_date) {
return {
- method: 'get',
- url: '/api/frontend/v1/LvPlan/Reservierungen',
+ method: 'post',
+ url: '/api/frontend/v1/LvPlan/getReservierungen',
params: { start_date, end_date }
};
},
diff --git a/public/js/api/lvPlan.js b/public/js/api/lvPlan.js
index a1d34b37c..2939b688c 100644
--- a/public/js/api/lvPlan.js
+++ b/public/js/api/lvPlan.js
@@ -1,7 +1,7 @@
export default {
getRoomInfo(ort_kurzbz, start_date, end_date) {
- return this.$fhcApi.get(
+ return this.$fhcApi.post(
'/api/frontend/v1/LvPlan/getRoomplan',
{ ort_kurzbz, start_date, end_date}
);
@@ -13,14 +13,14 @@ export default {
);
},
getOrtReservierungen(ort_kurzbz, start_date, end_date) {
- return this.$fhcApi.get(
- `/api/frontend/v1/LvPlan/Reservierungen/${ort_kurzbz}`,
+ return this.$fhcApi.post(
+ `/api/frontend/v1/LvPlan/getReservierungen/${ort_kurzbz}`,
{ start_date, end_date}
);
},
getLvPlanReservierungen(start_date, end_date) {
- return this.$fhcApi.get(
- '/api/frontend/v1/LvPlan/Reservierungen',
+ return this.$fhcApi.post(
+ '/api/frontend/v1/LvPlan/getReservierungen',
{ start_date, end_date }
);
},
diff --git a/public/js/components/Calendar/Base.js b/public/js/components/Calendar/Base.js
new file mode 100644
index 000000000..520948ecb
--- /dev/null
+++ b/public/js/components/Calendar/Base.js
@@ -0,0 +1,293 @@
+import BaseDraganddrop from './Base/DragAndDrop.js';
+import BaseHeader from './Base/Header.js';
+import BaseSlider from './Base/Slider.js';
+import BsModal from '../Bootstrap/Modal.js';
+
+import CalClick from '../../directives/Calendar/Click.js';
+
+export default {
+ name: "CalendarBase",
+ components: {
+ BaseDraganddrop,
+ BaseHeader,
+ BaseSlider,
+ BsModal
+ },
+ directives: {
+ CalClick
+ },
+ provide() {
+ return {
+ locale: Vue.computed(() => this.locale),
+ timezone: Vue.computed(() => this.timezone),
+ timeGrid: Vue.computed(() => this.timeGrid),
+ draggableEvents: Vue.computed(() => {
+ if (!this.draggableEvents)
+ return () => false;
+
+ if (Array.isArray(this.draggableEvents))
+ return event => this.draggableEvents.includes(event.type);
+ if (this.draggableEvents instanceof Function)
+ return this.draggableEvents;
+
+ return () => true;
+ }),
+ dropableEvents: Vue.computed(() => {
+ if (!this.onDrop)
+ return () => false;
+
+ if (Array.isArray(this.dropableEvents))
+ return item => this.dropableEvents.includes(item.type);
+ if (this.dropableEvents instanceof Function)
+ return this.dropableEvents;
+
+ return () => true;
+ }),
+ hasDragoverFunc: Vue.computed(() => this.onDragover),
+ mode: Vue.computed(() => this.mode)
+ };
+ },
+ props: {
+ locale: {
+ type: String,
+ default: 'de'
+ },
+ timezone: {
+ type: String,
+ required: true
+ },
+ date: {
+ type: [Date, String, Number, luxon.DateTime],
+ default: luxon.DateTime.local()
+ },
+ modes: {
+ type: Object,
+ required: true,
+ default: {}
+ // TODO(chris): verfication functions
+ },
+ mode: String,
+ modeOptions: Object,
+ events: {
+ type: Array,
+ default: []
+ },
+ backgrounds: {
+ type: Array,
+ default: []
+ },
+ showBtns: Boolean,
+ btnMonth: {
+ type: Boolean,
+ default: undefined
+ },
+ btnWeek: {
+ type: Boolean,
+ default: undefined
+ },
+ btnDay: {
+ type: Boolean,
+ default: undefined
+ },
+ btnList: {
+ type: Boolean,
+ default: undefined
+ },
+ timeGrid: Array,
+ draggableEvents: [Boolean, Array, Function],
+ dropableEvents: [Boolean, Array, Function],
+ onDragover: Function,
+ onDrop: Function
+ },
+ emits: [
+ "click:next",
+ "click:prev",
+ "click:mode",
+ "click:event",
+ "click:day",
+ "click:week",
+ "update:date",
+ "update:mode",
+ "update:range",
+ "drop"
+ ],
+ data() {
+ return {
+ internalView: null,
+ internalDate: null,
+ modalEvent: null
+ };
+ },
+ computed: {
+ convertedEvents() {
+ return this.events.map(orig => ({
+ id: orig.type + orig[orig.type + '_id'],
+ type: orig.type,
+ start: luxon.DateTime.fromISO(orig.isostart).setZone(this.timezone),
+ end: luxon.DateTime.fromISO(orig.isoend).setZone(this.timezone),
+ orig
+ }));
+ },
+ convertedBackgrounds() {
+ return this.backgrounds.map(bg => {
+ const res = { ...bg };
+ if (res.start) {
+ if (Number.isInteger(res.start))
+ res.start = luxon.DateTime.fromMillis(res.start, { zone: this.timezone, locale: this.locale });
+ else if (res.start instanceof Date)
+ res.start = luxon.DateTime.fromJSDate(res.start, { zone: this.timezone, locale: this.locale });
+ else if (typeof res.start ===
+ 'string' || res.start instanceof String)
+ res.start = luxon.DateTime.fromISO(res.start, { zone: this.timezone, locale: this.locale });
+ }
+ if (res.end) {
+ if (Number.isInteger(res.end))
+ res.end = luxon.DateTime.fromMillis(res.end, { zone: this.timezone, locale: this.locale });
+ else if (res.end instanceof Date)
+ res.end = luxon.DateTime.fromJSDate(res.end, { zone: this.timezone, locale: this.locale });
+ else if (typeof res.end ===
+ 'string' || res.end instanceof String)
+ res.end = luxon.DateTime.fromISO(res.end, { zone: this.timezone, locale: this.locale });
+ }
+ return res;
+ });
+ },
+ cDate: {
+ get() {
+ if (this.internalDate) {
+ return this.internalDate.setLocale(this.locale);
+ }
+ return luxon.DateTime.fromJSDate(new Date(this.date)).setZone(this.timezone).setLocale(this.locale);
+ },
+ set(value) {
+ this.internalDate = value;
+ this.$emit('update:date', value);
+ }
+ },
+ cMode: {
+ get() {
+ if (!this.internalView) {
+ // choose default mode
+ let mode = this.mode;
+ if (!mode || !this.modes[mode])
+ mode = Object.keys(this.modes).find(Boolean); // start with first entry as active mode
+ return mode || '';
+ }
+ return this.internalView;
+ },
+ set(value) {
+ this.internalView = value;
+ this.$emit('update:mode', value);
+ }
+ }
+ },
+ methods: {
+ clickPrev() {
+ const evt = new Event('click:prev', {cancelable: true});
+ this.$emit('click:prev', evt);
+ if (evt.defaultPrevented)
+ return;
+
+ // default: switch page
+ this.$refs.mode.prevPage();
+ },
+ clickNext() {
+ const evt = new Event('click:next', {cancelable: true});
+ this.$emit('click:next', evt);
+ if (evt.defaultPrevented)
+ return;
+
+ // default: switch page
+ this.$refs.mode.nextPage();
+ },
+ handleClickDefaults(evt) {
+ // TODO(chris): implement
+ switch (evt.detail.source) {
+ case 'day':
+ if (this.cMode != 'day' && this.modes['day']) {
+ evt.stopPropagation();
+ this.cDate = evt.detail.value;
+ this.cMode = 'day';
+ }
+ break;
+ case 'week':
+ if (this.cMode != 'week' && this.modes['week']) {
+ evt.stopPropagation();
+ this.cDate = luxon.DateTime.fromObject({
+ localWeekNumber: evt.detail.value.number,
+ localWeekYear: evt.detail.value.year
+ }, {
+ zone: this.cDate.zoneName,
+ locale: this.cDate.locale
+ });
+ this.cMode = 'week';
+ }
+ break;
+ }
+ },
+ onDropItem(evt, start, end) {
+ this.$emit('drop', evt, start, end);
+ },
+ showEventModal(eventObj) {
+ this.modalEvent = eventObj;
+ this.$refs.modal.show();
+ },
+ hideEventModal() {
+ if (this.modalEvent)
+ this.modalEvent.closeFn = undefined;
+ this.$refs.modal.hide();
+ this.modalEvent = null;
+ },
+ onModalHidden() {
+ if (this.modalEvent.closeFn)
+ this.modalEvent.closeFn();
+ }
+ },
+ template: /* html */`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+}
diff --git a/public/js/components/Calendar/Base/DragAndDrop.js b/public/js/components/Calendar/Base/DragAndDrop.js
new file mode 100644
index 000000000..0be17ddb4
--- /dev/null
+++ b/public/js/components/Calendar/Base/DragAndDrop.js
@@ -0,0 +1,162 @@
+import DragAndDrop from '../../../helpers/DragAndDrop.js';
+
+import CalDnd from '../../../directives/Calendar/DragAndDrop.js';
+
+/**
+ * TODO(chris): this needs serious rework!
+ */
+
+export default {
+ name: "CalendarDragAndDrop",
+ directives: {
+ CalDnd
+ },
+ provide() {
+ return {
+ events: Vue.computed(() => this.correctedEvents),
+ backgrounds: Vue.computed(() => this.backgrounds),
+ dropAllowed: Vue.computed(() => this.dragging && this.dropAllowed)
+ };
+ },
+ inject: {
+ mode: "mode",
+ dropableEvents: "dropableEvents"
+ },
+ props: {
+ events: Array,
+ backgrounds: Array
+ },
+ emits: [
+ "drop"
+ ],
+ data() {
+ return {
+ dragging: false,
+ allowed: false,
+ draggedInternalEvent: null,
+ draggedExternalEvent: null,
+ targetTimestamp: 0,
+ targetGridEnds: null,
+ dropAllowed: false,
+
+ shadowPreview: false // TODO(chris): IMPLEMENT! (use background instead of event as preview)
+ };
+ },
+ computed: {
+ correctedEvents() {
+ if (this.dragging) {
+ if (this.draggedInternalEvent) {
+ const index = this.events.findIndex(e => e.id == this.draggedInternalEvent.id);
+ if (this.previewEvent && !this.shadowPreview)
+ return this.events.toSpliced(index, 1, this.previewEvent);
+ else
+ return this.events.toSpliced(index, 1);
+ }
+ if (this.previewEvent && !this.shadowPreview)
+ return [...this.events, this.previewEvent];
+ }
+
+ return this.events;
+ },
+ correctedBackgrounds() {
+ if (this.dragging) {
+ if (this.shadowPreview) {
+ // TODO(chris): how to get the length
+ return [...this.backgrounds, {
+ start: new Date(this.targetTimestamp),
+ class: 'shadow-preview'
+ }];
+ }
+ }
+
+ return this.backgrounds;
+ },
+ previewEvent() {
+ if (!this.dragging || !this.dropAllowed)
+ return null;
+ if (!this.targetTimestamp)
+ return null;
+
+ const event = this.draggedInternalEvent || this.draggedExternalEvent;
+
+ if (!event)
+ return null;
+
+ // TODO(chris): calculate length correctly from orig
+ let length = event.end - event.start;
+ if (this.targetGridEnds)
+ length = this.targetGridEnds.find(end => end >= this.targetTimestamp + length) - this.targetTimestamp;
+
+ return {
+ orig: event.orig,
+ start: this.targetTimestamp,
+ end: this.targetTimestamp + length
+ };
+ }
+ },
+ methods: {
+ onDragstart(evt) {
+ DragAndDrop.setTransferData(evt.detail.originalEvent, evt.detail.item.orig);
+ this.draggedInternalEvent = evt.detail.item;
+ },
+ onDragend() {
+ this.draggedInternalEvent = null;
+ this.dragging = false;
+ },
+ onDragenter(evt) {
+ this.dragging = true;
+
+ if (!this.draggedInternalEvent) {
+ const event = DragAndDrop.getValidTransferData(evt.detail.originalEvent);
+ if (event) {
+ this.draggedExternalEvent = {
+ id: event.id,
+ type: event.type,
+ start: event.isostart
+ ? luxon.DateTime.fromISO(event.isostart).setZone(this.timezone)
+ : luxon.DateTime.local().setZone(this.timezone),
+ end: event.isoend
+ ? luxon.DateTime.fromISO(event.isoend).setZone(this.timezone)
+ : luxon.DateTime.local().setZone(this.timezone),
+ orig: event
+ };
+ } else {
+ this.draggedExternalEvent = null;
+ }
+ this.dropAllowed = this.dropableEvents(event, this.mode);
+ } else {
+ this.dropAllowed = this.dropableEvents(this.draggedInternalEvent, this.mode);
+ }
+ },
+ onDragleave() {
+ this.dragging = false;
+ },
+ onDragchange(evt) {
+ this.targetTimestamp = evt.detail.timestamp;
+
+ this.targetGridEnds = evt.detail.ends || null;
+ },
+ onDrop(evt) {
+ if (!this.dragging || !this.dropAllowed)
+ return;
+
+ this.$emit('drop', evt, this.previewEvent.start, this.previewEvent.end);
+ this.dropAllowed = false;
+ this.dragging = false;
+ }
+ },
+ template: `
+
+
+
+ `
+}
diff --git a/public/js/components/Calendar/Base/Grid.js b/public/js/components/Calendar/Base/Grid.js
new file mode 100644
index 000000000..37b31e784
--- /dev/null
+++ b/public/js/components/Calendar/Base/Grid.js
@@ -0,0 +1,429 @@
+import GridLine from './Grid/Line.js';
+import GridLineEvent from './Grid/Line/Event.js';
+
+import CalDnd from '../../../directives/Calendar/DragAndDrop.js';
+
+export default {
+ name: "CalendarGrid",
+ components: {
+ GridLine,
+ GridLineEvent
+ },
+ directives: {
+ CalDnd
+ },
+ inject: {
+ originalEvents: "events",
+ originalBackgrounds: "backgrounds",
+ dropAllowed: "dropAllowed"
+ },
+ provide() {
+ return {
+ flipAxis: Vue.computed(() => this.flipAxis),
+ axisRow: Vue.computed(() => this.axisRow)
+ };
+ },
+ props: {
+ axisMain: {
+ type: Array,
+ required: true,
+ validator(value) {
+ return value.every(item => item instanceof luxon.DateTime);
+ }
+ },
+ axisParts: {
+ type: Array,
+ required: true,
+ validator(value) {
+ return value.every(item =>
+ item instanceof luxon.Duration
+ || Number.isInteger(item)
+ || (
+ (
+ item.start instanceof luxon.Duration
+ || Number.isInteger(item.start)
+ ) && (
+ item.end instanceof luxon.Duration
+ || Number.isInteger(item.end)
+ )
+ )
+ );
+ }
+ },
+ flipAxis: Boolean,
+ allDayEvents: Boolean,
+ axisMainCollapsible: Boolean,
+ snapToGrid: Boolean
+ },
+ data() {
+ return {
+ dragging: false,
+ resizeObserver: null,
+ mutationObserver: null,
+ userScroll: true
+ };
+ },
+ computed: {
+ axisRow() {
+ return this.flipAxis ? 'column' : 'row';
+ },
+ axisCol() {
+ return this.flipAxis ? 'row' : 'column';
+ },
+ axisPartsWithBreaks() {
+ return this.axisParts.reduce((res, tu, index) => {
+ const start = tu.start || tu;
+ const end = tu.end;
+
+ if (res.length) {
+ const lastTuEnd = res.pop();
+ if (Array.isArray(lastTuEnd)) {
+ res.push({
+ start: lastTuEnd[0],
+ end: start,
+ index: lastTuEnd[1]
+ });
+ } else if (lastTuEnd != start) {
+ // add pause
+ res.push({
+ start: lastTuEnd,
+ end: start
+ });
+ }
+ }
+
+ if (!end) {
+ res.push([start, index]);
+ } else {
+ res.push({
+ start,
+ end,
+ index
+ });
+ res.push(end);
+ }
+ return res;
+ }, []).slice(0, -1);
+ },
+ axisPartsSave() {
+ if (!this.axisParts[this.axisParts.length - 1].end)
+ return this.axisParts.slice(0, -1);
+ return this.axisParts;
+ },
+ start() {
+ return this.axisPartsWithBreaks[0].start;
+ },
+ end() {
+ return this.axisPartsWithBreaks[this.axisPartsWithBreaks.length - 1].end;
+ },
+ ends() {
+ const ends = [];
+ const partsEnds = this.axisPartsWithBreaks
+ .filter(p => p.index !== undefined)
+ .map(p => p.end);
+ for (var date of this.axisMain)
+ for (var part of partsEnds)
+ ends.push(date.plus(part));
+
+ return ends;
+ },
+ axisMainBorders() {
+ return this.axisMain.reduce(
+ (res, curr) => res.concat([curr.plus(this.start), curr.plus(this.end)]),
+ []
+ );
+ },
+ eventsAllDay() {
+ if (!this.allDayEvents)
+ return [];
+ return this.mapIntoMainAxis(this.originalEvents.filter(event => event.orig.allDayEvent));
+ },
+ eventsNormal() {
+ if (!this.allDayEvents)
+ return this.events;
+ return this.mapIntoMainAxis(this.originalEvents.filter(event => !event.orig.allDayEvent));
+ },
+ events() {
+ return this.mapIntoMainAxis(this.originalEvents);
+ },
+ backgrounds() {
+ return this.mapIntoMainAxis(this.originalBackgrounds);
+ },
+ hasValidEvents() {
+ return this.events.find(e => e.length);
+ },
+ styleGridCols() {
+ let cols = 'repeat(' + this.axisMain.length + ', 1fr)';
+ if (this.axisMainCollapsible) {
+ if (this.hasValidEvents)
+ cols = this.events
+ .map(e => e.length
+ ? '1fr'
+ : 'var(--fhc-calendar-axis-collapsible, .5fr)')
+ .join(' ');
+ }
+ return cols;
+ },
+ styleGridRows() {
+ const gridlines = {};
+
+ this.axisPartsWithBreaks.forEach(part => {
+ let ts = part.start.toMillis();
+ if (!gridlines[ts])
+ gridlines[ts] = ['t_' + ts];
+ if (part.index !== undefined)
+ gridlines[ts].push('ps_' + part.index);
+ ts = part.end.toMillis();
+ if (!gridlines[ts])
+ gridlines[ts] = ['t_' + ts];
+ if (part.index !== undefined)
+ gridlines[ts].push('pe_' + part.index);
+ });
+
+ this.eventsNormal.forEach((events, mainIndex) => {
+ let day = this.axisMain[mainIndex];
+ events.forEach(event => {
+ if (!event.startsHere && !event.endsHere)
+ return;
+
+ if (event.startsHere) {
+ let ts = event.start.diff(day).toMillis();
+ if (!gridlines[ts])
+ gridlines[ts] = ['t_' + ts, 'e_' + ts];
+ }
+ if (event.endsHere) {
+ let ts = event.end.diff(day).toMillis();
+ if (!gridlines[ts])
+ gridlines[ts] = ['t_' + ts, 'e_' + ts];
+ }
+ });
+ });
+
+ return Object.keys(gridlines).sort((a,b) => parseInt(a)-parseInt(b)).map((start, i, keys) => {
+ let end = keys[i + 1];
+ if (!end) {
+ gridlines[start].push('end');
+ return '[' + gridlines[start].join(' ') + ']';
+ }
+ return '[' + gridlines[start].join(' ') + '] ' + (end - start) + 'fr';
+ }).join(' ');
+ }
+ },
+ methods: {
+ mapIntoMainAxis(target) {
+ const result = Array.from({length: this.axisMain.length}, () => Array());
+
+ target.forEach(event => {
+ const start = event.start || this.axisMainBorders[0].plus(-1);
+ const end = event.end || this.axisMainBorders[this.axisMainBorders.length - 1].plus(1);
+
+ for (var i = 0; i < this.axisMain.length; i++) {
+ if (start < this.axisMainBorders[i * 2 + 1] && end > this.axisMainBorders[i * 2]) {
+ const startsHere = start >= this.axisMainBorders[i * 2];
+ const endsHere = end <= this.axisMainBorders[i * 2 + 1];
+ result[i].push({
+ ...event,
+ startsHere,
+ endsHere
+ });
+ }
+ }
+ });
+
+ return result;
+ },
+
+ /* DRAG AND DROP */
+ getPageTop(el) {
+ let pageTop = el.offsetTop;
+ if (el.offsetParent)
+ pageTop += this.getPageTop(el.offsetParent);
+ return pageTop;
+ },
+ getPageLeft(el) {
+ let pageLeft = el.offsetLeft;
+ if (el.offsetParent)
+ pageLeft += this.getPageLeft(el.offsetParent);
+ return pageLeft;
+ },
+ getTimestampFromMouse(evt, dayTimestamp) {
+ let mouse, mouseFrac;
+ if (this.flipAxis) {
+ mouse = evt.pageX - this.getPageLeft(this.$refs.body) + this.$refs.main.scrollLeft;
+ mouseFrac = mouse / this.$refs.body.offsetWidth;
+ } else {
+ mouse = evt.pageY - this.getPageTop(this.$refs.body) + this.$refs.main.scrollTop;
+ mouseFrac = mouse / this.$refs.body.offsetHeight;
+ }
+
+ return dayTimestamp + this.start + Math.floor((this.end - this.start) * mouseFrac);
+ },
+
+ /* SCROLLING */
+ enableAutoScroll() {
+ if (!this.resizeObserver)
+ this.resizeObserver = new ResizeObserver(this.scrollToEarliestEvent);
+ this.resizeObserver.observe(this.$refs.body);
+
+ if (!this.mutationObserver)
+ this.mutationObserver = new MutationObserver(mutations => {
+ if (mutations.some(m => m.addedNodes.length && [].some.call(m.addedNodes, el => el.matches && el.matches('.fhc-calendar-base-grid-line-event'))))
+ this.scrollToEarliestEvent();
+ });
+ this.mutationObserver.observe(this.$refs.body, {
+ subtree: true,
+ childList: true
+ });
+
+ this.scrollToEarliestEvent();
+ },
+ disableAutoScroll() {
+ if (this.resizeObserver)
+ this.resizeObserver.disconnect();
+ this.resizeObserver = null;
+
+ if (this.mutationObserver)
+ this.mutationObserver.disconnect();
+ this.mutationObserver = null;
+ },
+ scrollToEarliestEvent() {
+ const eventElements = this.$refs.scroller.querySelectorAll('.fhc-calendar-base-grid-line-event');
+ const earliestEventOffset = eventElements.values()
+ .reduce((res, el) => {
+ const top = el.offsetTop;
+ if (!res[1] || top < res[0])
+ return [top, el];
+ return res;
+ }, [0, null]);
+
+ this.userScroll = false;
+ if (earliestEventOffset[1]) {
+ earliestEventOffset[1].scrollIntoView({ behavior: "smooth" });
+ } else {
+ this.$refs.scroller.scrollTo(0, 0);
+ }
+ }
+ },
+ beforeUnmount() {
+ this.disableAutoScroll();
+ },
+ template: /* html */`
+
+ `
+}
diff --git a/public/js/components/Calendar/Base/Grid/Line.js b/public/js/components/Calendar/Base/Grid/Line.js
new file mode 100644
index 000000000..9a6c2f579
--- /dev/null
+++ b/public/js/components/Calendar/Base/Grid/Line.js
@@ -0,0 +1,85 @@
+import LineEvent from './Line/Event.js';
+import LineBackground from './Line/Background.js';
+
+/**
+ * TODO(chris):
+ * Event overflow for Month mode (more-button)
+ */
+
+export default {
+ name: "GridLine",
+ components: {
+ LineEvent,
+ LineBackground
+ },
+ inject: {
+ axisRow: "axisRow"
+ },
+ props: {
+ date: {
+ type: luxon.DateTime,
+ required: true
+ },
+ start: {
+ type: luxon.DateTime,
+ required: true
+ },
+ end: {
+ type: luxon.DateTime,
+ required: true
+ },
+ events: {
+ type: Array,
+ default: []
+ },
+ backgrounds: {
+ type: Array,
+ default: []
+ }
+ },
+ computed: {
+ eventsWithRowInfo() {
+ const events = [];
+ this.events.forEach(event => {
+ const rows = [1, -1];
+ if (event.startsHere) {
+ rows[0] = 't_' + event.start.diff(this.date).toMillis();
+ }
+ if (event.endsHere) {
+ rows[1] = 't_' + event.end.diff(this.date).toMillis();
+ }
+
+ events.push({
+ ...event,
+ rows
+ });
+ });
+ return events;
+ }
+ },
+ template: /* html */`
+
+
+
+
+
+
+
+
+
+ `
+}
diff --git a/public/js/components/Calendar/Base/Grid/Line/Background.js b/public/js/components/Calendar/Base/Grid/Line/Background.js
new file mode 100644
index 000000000..e87737ffc
--- /dev/null
+++ b/public/js/components/Calendar/Base/Grid/Line/Background.js
@@ -0,0 +1,72 @@
+export default {
+ name: "GridLineBackground",
+ inject: {
+ flipAxis: "flipAxis"
+ },
+ props: {
+ start: {
+ type: luxon.DateTime,
+ required: true
+ },
+ end: {
+ type: luxon.DateTime,
+ required: true
+ },
+ background: {
+ type: Object,
+ required: true,
+ validator(value) {
+ if (!value.start && !value.end)
+ return false;
+ if (value.start && !(value.start instanceof luxon.DateTime))
+ return false;
+ if (value.end && !(value.end instanceof luxon.DateTime))
+ return false;
+ return true;
+ }
+ }
+ },
+ computed: {
+ styles() {
+ if (!this.background.endsHere && !this.background.startsHere)
+ return this.background.style;
+
+ const perc = (this.end.ts - this.start.ts) / 100;
+
+ let border = {};
+ if (this.background.startsHere)
+ border[this.flipAxis ? 'left' : 'top'] = (this.background.start.diff(this.start)) / perc + '%';
+ if (this.background.endsHere)
+ border[this.flipAxis ? 'right' : 'bottom'] = (this.end.diff(this.background.end)) / perc + '%';
+
+ if (!this.background.style)
+ return border;
+
+ return [this.background.style, border];
+ },
+ classes() {
+ if (!this.background.endsHere && !this.background.startsHere)
+ return this.background.class;
+
+ const result = [];
+ if (this.background.class)
+ result.push(this.background.class);
+ if (this.background.startsHere)
+ result.push('bg-begin');
+ if (this.background.endsHere)
+ result.push('bg-end');
+ return result;
+ }
+ },
+ template: /* html */`
+
+ {{ background.label }}
+
+ `
+}
diff --git a/public/js/components/Calendar/Base/Grid/Line/Event.js b/public/js/components/Calendar/Base/Grid/Line/Event.js
new file mode 100644
index 000000000..6512d1fa1
--- /dev/null
+++ b/public/js/components/Calendar/Base/Grid/Line/Event.js
@@ -0,0 +1,57 @@
+import CalDnd from '../../../../../directives/Calendar/DragAndDrop.js';
+import CalClick from '../../../../../directives/Calendar/Click.js';
+
+export default {
+ name: "GridLineEvent",
+ directives: {
+ CalDnd,
+ CalClick
+ },
+ inject: {
+ draggableEvents: "draggableEvents",
+ mode: "mode"
+ },
+ props: {
+ event: {
+ type: Object,
+ required: true,
+ validator(value) {
+ return (value.start && value.end && value.orig);
+ }
+ }
+ },
+ computed: {
+ isHeaderOrFooter() {
+ return ['header', 'footer'].includes(this.event.orig);
+ },
+ draggable() {
+ return !this.isHeaderOrFooter && this.draggableEvents(this.event.orig, this.mode);
+ },
+ classes() {
+ const classes = [];
+ if (this.isHeaderOrFooter) {
+ classes.push('event-' + this.event.orig);
+ } else {
+ if (this.event.startsHere)
+ classes.push('event-begin');
+ if (this.event.endsHere)
+ classes.push('event-end');
+ }
+ return classes
+ }
+ },
+ template: /* html */`
+
+
+ {{ event.orig }}
+
+
+ `
+}
diff --git a/public/js/components/Calendar/Base/Header.js b/public/js/components/Calendar/Base/Header.js
new file mode 100644
index 000000000..c1dae4bc0
--- /dev/null
+++ b/public/js/components/Calendar/Base/Header.js
@@ -0,0 +1,123 @@
+/**
+ * TODO(chris): use click-directive
+ */
+import DatePicker from './Header/Datepicker.js';
+
+export default {
+ name: "CalendarHeader",
+ components: {
+ DatePicker
+ },
+ props: {
+ date: {
+ type: luxon.DateTime,
+ required: true
+ },
+ mode: {
+ type: String,
+ required: true
+ },
+ modeOptions: {
+ type: Object,
+ default: {}
+ },
+ btnMonth: Boolean,
+ btnWeek: Boolean,
+ btnDay: Boolean,
+ btnList: Boolean
+ },
+ emits: [
+ "next",
+ "prev",
+ "click:mode",
+ "update:date",
+ "update:mode"
+ ],
+ data() {
+ return {
+ open: false
+ };
+ },
+ methods: {
+ clickMode(evt, mode) {
+ this.$emit('click:mode', evt);
+ if (!evt.defaultPrevented)
+ this.$emit('update:mode', mode);
+ }
+ },
+ template: /* html */`
+
+ `
+}
diff --git a/public/js/components/Calendar/Base/Header/Datepicker.js b/public/js/components/Calendar/Base/Header/Datepicker.js
new file mode 100644
index 000000000..2d3237643
--- /dev/null
+++ b/public/js/components/Calendar/Base/Header/Datepicker.js
@@ -0,0 +1,116 @@
+// TODO(chris): translate aria-labels
+
+export default {
+ name: "CalendarHeaderDatepicker",
+ components: {
+ VueDatePicker
+ },
+ inject: [
+ "locale",
+ "timezone"
+ ],
+ props: {
+ date: {
+ type: luxon.DateTime,
+ required: true
+ },
+ mode: {
+ type: String,
+ required: true
+ },
+ listLength: {
+ type: Number,
+ default: 7
+ }
+ },
+ emits: [
+ "update:date"
+ ],
+ computed: {
+ convertedDate() {
+ // convert to target TZ then strip TZ Information
+ // so the datepicker can work with local times
+ return this.date.setZone(this.timezone).setZone('local', { keepLocalTime: true });
+ },
+ current() {
+ switch (this.mode) {
+ case "month":
+ return {month: this.convertedDate.month-1, year: this.convertedDate.year};
+ case "list":
+ return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.listLength }).ts - 1];
+ case "week":
+ return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts];
+ case "day":
+ return this.convertedDate;
+ default:
+ return null;
+ }
+ },
+ title() {
+ switch (this.mode) {
+ case "month":
+ return this.date.toLocaleString({ month: 'long', year: 'numeric' });
+ case "week":
+ var year = this.date.localWeekYear;
+ var week = this.date.toFormat('nn');
+ return this.$p.t('calendar/year_kw', { year, week });
+ case "list":
+ return this.date.toLocaleString(luxon.DateTime.DATE_FULL) + '-' + this.date.plus({ days: this.listLength - 1 }).toLocaleString(luxon.DateTime.DATE_FULL);
+ case "day":
+ return this.date.toLocaleString(luxon.DateTime.DATE_FULL);
+ default:
+ return 'View not Supported';
+ }
+ },
+ weekStart() {
+ return luxon.Info.getStartOfWeek(this.date)%7;
+ }
+ },
+ methods: {
+ update(value) {
+ let date;
+ switch (this.mode) {
+ case "month":
+ value.month++;
+ date = luxon.DateTime.fromObject(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
+ break;
+ case "list":
+ case "week":
+ date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
+ break;
+ case "day":
+ date = luxon.DateTime.fromJSDate(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
+ break;
+ default:
+ return; // Don't update if the value is invalid!
+ }
+ this.$emit("update:date", date);
+ },
+ weekNumbers(date) {
+ return luxon.DateTime.fromJSDate(date, { locale: this.locale }).localWeekNumber;
+ }
+ },
+ template: /* html */`
+
+ `
+}
diff --git a/public/js/components/Calendar/Base/Label/Day.js b/public/js/components/Calendar/Base/Label/Day.js
new file mode 100644
index 000000000..731beebd2
--- /dev/null
+++ b/public/js/components/Calendar/Base/Label/Day.js
@@ -0,0 +1,39 @@
+import CalClick from '../../../../directives/Calendar/Click.js';
+
+export default {
+ name: "LabelDay",
+ directives: {
+ CalClick
+ },
+ props: {
+ date: {
+ type: luxon.DateTime,
+ required: true
+ }
+ },
+ computed: {
+ titleFull() {
+ return this.date.toLocaleString({day: 'numeric', month: 'long', year: 'numeric'});
+ },
+ titleLong() {
+ return this.date.toLocaleString({day: '2-digit', month: '2-digit', year: 'numeric'});
+ },
+ titleShort() {
+ return this.date.toLocaleString({day: 'numeric', month: 'numeric'});
+ },
+ titleNarrow() {
+ return this.date.toLocaleString({day: 'numeric'});
+ }
+ },
+ template: /* html */`
+
+ {{ titleFull }}
+ {{ titleLong }}
+ {{ titleShort }}
+ {{ titleNarrow }}
+
+ `
+}
diff --git a/public/js/components/Calendar/Base/Label/Dow.js b/public/js/components/Calendar/Base/Label/Dow.js
new file mode 100644
index 000000000..4da02de64
--- /dev/null
+++ b/public/js/components/Calendar/Base/Label/Dow.js
@@ -0,0 +1,35 @@
+import CalClick from '../../../../directives/Calendar/Click.js';
+
+export default {
+ name: "LabelDow",
+ directives: {
+ CalClick
+ },
+ props: {
+ date: {
+ type: luxon.DateTime,
+ required: true
+ }
+ },
+ computed: {
+ titleLong() {
+ return this.date.toLocaleString({weekday: 'long'});
+ },
+ titleShort() {
+ return this.date.toLocaleString({weekday: 'short'});
+ },
+ titleNarrow() {
+ return this.date.toLocaleString({weekday: 'narrow'});
+ }
+ },
+ template: /* html */`
+
+ {{ titleLong }}
+ {{ titleShort }}
+ {{ titleNarrow }}
+
+ `
+}
diff --git a/public/js/components/Calendar/Base/Label/Time.js b/public/js/components/Calendar/Base/Label/Time.js
new file mode 100644
index 000000000..a7fc21ae7
--- /dev/null
+++ b/public/js/components/Calendar/Base/Label/Time.js
@@ -0,0 +1,58 @@
+export default {
+ name: "LabelTime",
+ props: {
+ part: {
+ type: [luxon.Duration, Number, Object],
+ required: true,
+ validator(value) {
+ if (value instanceof Object) {
+ if (value instanceof luxon.Duration)
+ return true;
+ let start_ok = true;
+ let end_ok = true;
+ if (value.start) {
+ start_ok = (
+ value.start instanceof luxon.Duration
+ || Number.isInteger(value.start)
+ );
+ }
+ if (value.end) {
+ end_ok = (
+ value.end instanceof luxon.Duration
+ || Number.isInteger(value.end)
+ );
+ }
+ return start_ok && end_ok;
+ }
+ return true;
+ }
+ }
+ },
+ computed: {
+ sanitizedTimestamps() {
+ return this.part.start || this.part.end ? this.part : { start: this.part };
+ },
+ start() {
+ if (!this.sanitizedTimestamps.start)
+ return null;
+ return this.formatTime(this.sanitizedTimestamps.start);
+ },
+ end() {
+ if (!this.sanitizedTimestamps.end)
+ return null;
+ return this.formatTime(this.sanitizedTimestamps.end);
+ }
+ },
+ methods: {
+ formatTime(date) {
+ return date.toISOTime({ suppressSeconds: true });
+ }
+ },
+ template: `
+
+ {{ start }}
+ -
+ {{ end }}
+
+ `
+}
diff --git a/public/js/components/Calendar/Base/Label/Week.js b/public/js/components/Calendar/Base/Label/Week.js
new file mode 100644
index 000000000..76849f2c6
--- /dev/null
+++ b/public/js/components/Calendar/Base/Label/Week.js
@@ -0,0 +1,38 @@
+import CalClick from '../../../../directives/Calendar/Click.js';
+
+export default {
+ name: "LabelWeek",
+ directives: {
+ CalClick
+ },
+ props: {
+ date: {
+ type: luxon.DateTime,
+ required: true
+ }
+ },
+ computed: {
+ weeks() {
+ const firstDay = this.date.startOf('week', { useLocaleWeeks: true });
+ const lastDay = this.date.endOf('week', { useLocaleWeeks: true });
+
+ const weeks = [
+ { number: firstDay.localWeekNumber, year: firstDay.localWeekYear },
+ { number: lastDay.localWeekNumber, year: lastDay.localWeekYear }
+ ];
+ if (weeks[0].number == weeks[1].number)
+ weeks.pop();
+ return weeks;
+ }
+ },
+ template: `
+
+
+ {{ week.number }}
+
+
+ `
+}
diff --git a/public/js/components/Calendar/Base/Slider.js b/public/js/components/Calendar/Base/Slider.js
new file mode 100644
index 000000000..192eccd5f
--- /dev/null
+++ b/public/js/components/Calendar/Base/Slider.js
@@ -0,0 +1,129 @@
+export default {
+ name: 'CalendarSlider',
+ inject: {
+ time: {
+ from: "sliderTime",
+ default: ".3s"
+ }
+ },
+ emits: [
+ 'slid'
+ ],
+ data() {
+ return {
+ target: 0,
+ extrasAfter: 0,
+ extrasBefore: 0,
+ running: false,
+ promiseResolve: null
+ }
+ },
+ computed: {
+ itemsAfter() {
+ return [...Array(this.extrasAfter)].map((i, k) => 1+k);
+ },
+ itemsBefore() {
+ return [...Array(this.extrasBefore)].map((i, k) => k-this.extrasBefore);
+ },
+ styleSlider() {
+ const style = {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%'
+ };
+ if (this.running) {
+ style.left = (-this.target * 100) + '%';
+ style.transition = 'left ' + this.time + ' ease-in-out';
+ }
+ return style;
+ },
+ styleBefore() {
+ return {
+ position: 'absolute',
+ top: 0,
+ height: '100%',
+ display: 'flex',
+ right: '100%',
+ width: (this.extrasBefore * 100) + '%'
+ };
+ },
+ styleAfter() {
+ return {
+ position: 'absolute',
+ top: 0,
+ height: '100%',
+ display: 'flex',
+ left: '100%',
+ width: (this.extrasAfter * 100) + '%'
+ };
+ }
+ },
+ methods: {
+ prevPage() {
+ return this.slidePages(-1);
+ },
+ nextPage() {
+ return this.slidePages(1);
+ },
+ slidePages(dir) {
+ return new Promise(resolve => {
+ this.promiseResolve = resolve;
+ this.running = true;
+ const newTarget = this.target + dir;
+ if (newTarget > 0) {
+ if (this.extrasAfter < newTarget)
+ this.extrasAfter = newTarget;
+ } else if (newTarget < 0) {
+ if (-this.extrasBefore > newTarget)
+ this.extrasBefore = -newTarget;
+ }
+ this.target = newTarget;
+ });
+ },
+ endSlide() {
+ if (this.promiseResolve) {
+ this.promiseResolve(this.target);
+ this.promiseResolve = null;
+ }
+ this.$emit('slid', this.target);
+ this.running = false;
+ this.target = 0;
+ this.extrasAfter = this.extrasBefore = 0;
+ }
+ },
+ template: /* html */`
+
+ `
+}
diff --git a/public/js/components/Calendar/Mode/Day.js b/public/js/components/Calendar/Mode/Day.js
new file mode 100644
index 000000000..68ca1fa89
--- /dev/null
+++ b/public/js/components/Calendar/Mode/Day.js
@@ -0,0 +1,101 @@
+import BaseSlider from '../Base/Slider.js';
+import DayView from './Day/View.js';
+
+export default {
+ name: "ModeDay",
+ components: {
+ BaseSlider,
+ DayView
+ },
+ props: {
+ currentDate: {
+ type: luxon.DateTime,
+ required: true
+ }
+ },
+ emits: [
+ "update:currentDate",
+ "update:range",
+ "click",
+ "requestModalOpen",
+ "requestModalClose"
+ ],
+ data() {
+ return {
+ focusDate: this.currentDate,
+ rangeOffset: 0
+ };
+ },
+ computed: {
+ range() {
+ let first = this.focusDate.startOf('day');
+ let last = this.focusDate.endOf('day');
+
+ if (this.rangeOffset != 0) {
+ if (this.rangeOffset < 0) {
+ first = first.plus({ days: this.rangeOffset });
+ } else {
+ last = last.plus({ days: this.rangeOffset });
+ }
+ }
+
+ return luxon.Interval.fromDateTimes(first, last);
+ }
+ },
+ watch: {
+ currentDate() {
+ this.rangeOffset = this.currentDate.startOf('day').diff(this.focusDate.startOf('day'), 'days').days;
+ if (this.rangeOffset) {
+ this.$refs.view.$refs.grid.disableAutoScroll();
+ this.$emit('update:range', this.range);
+ this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
+ }
+ }
+ },
+ methods: {
+ prevPage() {
+ this.rangeOffset = this.$refs.slider.target - 1;
+ this.$refs.view.$refs.grid.disableAutoScroll();
+ this.$emit('update:range', this.range);
+ this.$refs.slider.prevPage().then(this.updatePage);
+ },
+ nextPage() {
+ this.rangeOffset = this.$refs.slider.target + 1;
+ this.$refs.view.$refs.grid.disableAutoScroll();
+ this.$emit('update:range', this.range);
+ this.$refs.slider.nextPage().then(this.updatePage);
+ },
+ updatePage(days) {
+ const newFocusDate = this.focusDate.plus({ days });
+ this.focusDate = newFocusDate;
+ this.rangeOffset = 0;
+ this.$emit('update:currentDate', this.focusDate);
+ this.$emit('update:range', this.range);
+ this.$refs.view.$refs.grid.enableAutoScroll();
+ },
+ viewAttrs(days) {
+ const day = this.focusDate.plus({ days });
+ return { ...this.$attrs, day };
+ }
+ },
+ mounted() {
+ this.$emit('update:range', this.range);
+ this.$refs.view.$refs.grid.enableAutoScroll();
+ },
+ template: `
+
+
+
+
+
+
+
+ `
+}
diff --git a/public/js/components/Calendar/Mode/Day/View.js b/public/js/components/Calendar/Mode/Day/View.js
new file mode 100644
index 000000000..09738f1d0
--- /dev/null
+++ b/public/js/components/Calendar/Mode/Day/View.js
@@ -0,0 +1,164 @@
+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: "DayView",
+ components: {
+ CalendarGrid,
+ LabelDay,
+ LabelDow,
+ LabelTime
+ },
+ inject: {
+ timeGrid: "timeGrid",
+ originalEvents: "events",
+ timezone: "timezone"
+ },
+ props: {
+ day: {
+ type: luxon.DateTime,
+ required: true
+ },
+ emptyMessage: String,
+ emptyMessageDetails: String,
+ compact: Boolean
+ },
+ emits: [
+ "requestModalOpen",
+ "requestModalClose"
+ ],
+ data() {
+ return {
+ chosenEvent: null,
+ gridMainRef: null
+ };
+ },
+ computed: {
+ axisMain() {
+ return [this.day.startOf('day')];
+ },
+ axisParts() {
+ if (this.timeGrid) {
+ // create {start, end} array
+ return this.timeGrid.map(tu => {
+ return {
+ start: luxon.Duration.fromISOTime(tu.start),
+ end: luxon.Duration.fromISOTime(tu.end)
+ };
+ });
+ } else {
+ // create 07:00-23:00
+ return Array.from({ length: 17 }, (e, i) => luxon.Duration.fromObject({ hours: i + 7 }));
+ }
+ },
+ events() {
+ return this.originalEvents
+ .filter(event => event.start < this.day.plus({ days: 1 }) && event.end > this.day)
+ .sort((a, b) => a.start.ts - b.start.ts)
+ .map(evt => evt.orig);
+ },
+ currentEvent() {
+ if (this.chosenEvent) {
+ if (this.events.find(e => e == this.chosenEvent))
+ return this.chosenEvent;
+ }
+ let first = null;
+ if (this.events)
+ first = this.events.find(Boolean); // undefined => none found
+
+ if (first && first.type == 'loading')
+ return null; // null => loading
+
+ return first;
+ },
+ isToday() {
+ return this.day.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
+ }
+ },
+ watch: {
+ compact() {
+ if (this.compact) {
+ if (this.chosenEvent) {
+ this.$emit('requestModalOpen', {
+ event: this.chosenEvent,
+ closeFn: () => { this.chosenEvent = null; }
+ });
+ }
+ } else {
+ this.$emit('requestModalClose');
+ }
+ }
+ },
+ methods: {
+ handleClickDefaults(evt) {
+ if (evt.detail.source == 'event') {
+ this.chosenEvent = evt.detail.value;
+ if (this.compact) {
+ this.$emit('requestModalOpen', {
+ event: this.chosenEvent,
+ closeFn: () => { this.chosenEvent = null; }
+ });
+ }
+ }
+ }
+ },
+ mounted() {
+ this.gridMainRef = this.$refs.grid.$refs.main;
+ },
+ template: /* html */`
+
+
+
+
+ evt.detail.source = 'day'"
+ v-bind="{ date }"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ emptyMessage }}
+
+
+
+
+
+
+
{{ emptyMessageDetails }}
+
+
+
+ `
+}
diff --git a/public/js/components/Calendar/Mode/List.js b/public/js/components/Calendar/Mode/List.js
new file mode 100644
index 000000000..97990a5a3
--- /dev/null
+++ b/public/js/components/Calendar/Mode/List.js
@@ -0,0 +1,103 @@
+import BaseSlider from '../Base/Slider.js';
+import ListView from './List/View.js';
+
+export default {
+ name: "ModeList",
+ components: {
+ BaseSlider,
+ ListView
+ },
+ props: {
+ currentDate: {
+ type: luxon.DateTime,
+ required: true
+ },
+ length: {
+ type: Number,
+ default: 7
+ }
+ },
+ emits: [
+ "update:currentDate",
+ "update:range",
+ "click",
+ "requestModalOpen"
+ ],
+ data() {
+ return {
+ focusDate: this.currentDate,
+ rangeOffset: 0
+ };
+ },
+ computed: {
+ range() {
+ let first = this.focusDate;
+ let last = first.plus({ days: this.length });
+
+ if (this.rangeOffset != 0) {
+ if (this.rangeOffset < 0) {
+ first = first.plus({ days: this.rangeOffset });
+ } else {
+ last = first.plus({ days: this.rangeOffset + this.length });
+ }
+ }
+
+ return luxon.Interval.fromDateTimes(first, last);
+ }
+ },
+ watch: {
+ currentDate() {
+ this.rangeOffset = this.currentDate.startOf('day').diff(this.focusDate.startOf('day'), 'days').days;
+ if (this.rangeOffset) {
+ this.$emit('update:range', this.range);
+ this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
+ }
+ }
+ },
+ methods: {
+ prevPage() {
+ this.rangeOffset = this.$refs.slider.target - 1;
+ this.$emit('update:range', this.range);
+ this.$refs.slider.prevPage().then(this.updatePage);
+ },
+ nextPage() {
+ this.rangeOffset = this.$refs.slider.target + 1;
+ this.$emit('update:range', this.range);
+ this.$refs.slider.nextPage().then(this.updatePage);
+ },
+ updatePage(offset) {
+ const newFocusDate = this.focusDate.plus({ days: offset });
+ this.focusDate = newFocusDate;
+ this.rangeOffset = 0;
+ this.$emit('update:currentDate', this.focusDate);
+ this.$emit('update:range', this.range);
+ },
+ viewAttrs(offset) {
+ const day = this.focusDate.plus({ days: offset });
+ return { day, length: this.length };
+ },
+ handleClickDefaults(evt) {
+ switch (evt.detail.source) {
+ case 'event':
+ // default: Request Modal
+ this.$emit('requestModalOpen', { event: evt.detail.value });
+ break;
+ }
+ }
+ },
+ mounted() {
+ this.$emit('update:range', this.range);
+ },
+ template: `
+
+
+
+
+
+
+
+ `
+}
diff --git a/public/js/components/Calendar/Mode/List/View.js b/public/js/components/Calendar/Mode/List/View.js
new file mode 100644
index 000000000..76b4c887d
--- /dev/null
+++ b/public/js/components/Calendar/Mode/List/View.js
@@ -0,0 +1,98 @@
+import LabelDay from '../../Base/Label/Day.js';
+import LabelDow from '../../Base/Label/Dow.js';
+
+import CalDnd from '../../../../directives/Calendar/DragAndDrop.js';
+import CalClick from '../../../../directives/Calendar/Click.js';
+
+// TODO(chris): drag and drop
+
+export default {
+ name: "ListView",
+ components: {
+ LabelDay,
+ LabelDow
+ },
+ directives: {
+ CalDnd,
+ CalClick
+ },
+ inject: {
+ draggableEvents: "draggableEvents",
+ events: "events",
+ timezone: "timezone"
+ },
+ props: {
+ day: {
+ type: luxon.DateTime,
+ required: true
+ },
+ length: {
+ type: Number,
+ required: true
+ }
+ },
+ data() {
+ return {
+ chosenEvent: null
+ };
+ },
+ computed: {
+ days() {
+ return Array.from({ length: this.length }, (e, days) => this.day.plus({ days }));
+ },
+ eventsPerDay() {
+ const eventsPerDay = this.days.map(day => {
+ return {
+ day,
+ events: this.events
+ .filter(event => event.start < day.plus({ days: 1 }) && event.end > day)
+ .sort((a, b) => a.start.ts - b.start.ts)
+ };
+ });
+ return eventsPerDay.filter(day => day.events.length);
+ }
+ },
+ methods: {
+ draggable(event) {
+ return this.draggableEvents(event.orig, 'list');
+ },
+ isToday(date) {
+ return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
+ }
+ },
+ template: /* html */`
+
+
+
+
+
+
+ evt.detail.source = 'day'"
+ />
+ ,
+
+
+
+
+
+ `
+}
diff --git a/public/js/components/Calendar/Mode/Month.js b/public/js/components/Calendar/Mode/Month.js
new file mode 100644
index 000000000..a352ccc98
--- /dev/null
+++ b/public/js/components/Calendar/Mode/Month.js
@@ -0,0 +1,126 @@
+import BaseSlider from '../Base/Slider.js';
+import MonthView from './Month/View.js';
+
+export default {
+ name: "ModeMonth",
+ components: {
+ BaseSlider,
+ MonthView
+ },
+ props: {
+ currentDate: {
+ type: luxon.DateTime,
+ required: true
+ }
+ },
+ emits: [
+ "update:currentDate",
+ "update:range",
+ "click",
+ "requestModalOpen"
+ ],
+ data() {
+ return {
+ focusDate: this.currentDate,
+ rangeOffset: 0
+ };
+ },
+ computed: {
+ range() {
+ let first = this.focusDate.startOf('month').startOf('week', { useLocaleWeeks: true });
+ let last = first.plus({ days: 41 }).endOf('day'); // NOTE(chris): 6 weeks minus 1 day
+
+ if (this.rangeOffset != 0) {
+ const nextFocusDate = this.focusDate.plus({ months: this.rangeOffset});
+ const nextRangeStart = nextFocusDate.startOf('month').startOf('week', { useLocaleWeeks: true });
+ if (this.rangeOffset < 0) {
+ first = nextRangeStart;
+ } else {
+ last = nextRangeStart.plus({ days: 41 }).endOf('day');
+ }
+ }
+
+ return luxon.Interval.fromDateTimes(first, last);
+ }
+ },
+ watch: {
+ currentDate() {
+ if (this.currentDate.locale != this.focusDate.locale) {
+ this.focusDate = this.currentDate;
+ this.$emit('update:range', this.range);
+ } else {
+ this.rangeOffset = this.currentDate.startOf('month').diff(this.focusDate.startOf('month'), 'months').months;
+ if (this.rangeOffset) {
+ this.$emit('update:range', this.range);
+ this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
+ }
+ }
+ }
+ },
+ methods: {
+ prevPage() {
+ this.rangeOffset = this.$refs.slider.target - 1;
+ this.$emit('update:range', this.range);
+ this.$refs.slider.prevPage().then(this.updatePage);
+ },
+ nextPage() {
+ this.rangeOffset = this.$refs.slider.target + 1;
+ this.$emit('update:range', this.range);
+ this.$refs.slider.nextPage().then(this.updatePage);
+ },
+ updatePage(months) {
+ const newFocusDate = this.focusDate.plus({ months });
+ this.focusDate = newFocusDate;
+ this.rangeOffset = 0;
+ this.$emit('update:currentDate', this.focusDate);
+ this.$emit('update:range', this.range);
+ },
+ viewAttrs(months) {
+ const day = this.focusDate.plus({ months });
+ return { day };
+ },
+ handleClickDefaults(evt) {
+ switch (evt.detail.source) {
+ case 'week':
+ // default: Move to week if not in month
+ let dayInWeek = luxon.DateTime.fromObject({
+ localWeekNumber: evt.detail.value.number,
+ localWeekYear: evt.detail.value.year
+ }, {
+ zone: this.currentDate.zoneName,
+ locale: this.currentDate.locale
+ });
+
+ if (!this.focusDate.hasSame(dayInWeek.startOf('week', { useLocaleWeeks: true }), 'month')) {
+ this.$emit('update:currentDate', dayInWeek.startOf('week', { useLocaleWeeks: true }));
+ } else if (!this.focusDate.hasSame(dayInWeek.endOf('week', { useLocaleWeeks: true }), 'month')) {
+ this.$emit('update:currentDate', dayInWeek.endOf('week', { useLocaleWeeks: true }));
+ }
+ break;
+ case 'day':
+ // default: Set current-date
+ this.$emit('update:currentDate', evt.detail.value);
+ break;
+ case 'event':
+ // default: Request Modal
+ this.$emit('requestModalOpen', { event: evt.detail.value });
+ break;
+ }
+ }
+ },
+ mounted() {
+ this.$emit('update:range', this.range);
+ },
+ template: `
+
+
+
+
+
+
+
+ `
+}
diff --git a/public/js/components/Calendar/Mode/Month/View.js b/public/js/components/Calendar/Mode/Month/View.js
new file mode 100644
index 000000000..ea83b6c96
--- /dev/null
+++ b/public/js/components/Calendar/Mode/Month/View.js
@@ -0,0 +1,95 @@
+import CalendarGrid from '../../Base/Grid.js';
+import LabelWeek from '../../Base/Label/Week.js';
+import LabelDow from '../../Base/Label/Dow.js';
+import LabelDay from '../../Base/Label/Day.js';
+
+export default {
+ name: "MonthView",
+ components: {
+ CalendarGrid,
+ LabelWeek,
+ LabelDow,
+ LabelDay
+ },
+ provide() {
+ return {
+ // NOTE(chris): snap events to day
+ events: Vue.computed(() => {
+ //const events = [];
+ const events = this.events.map(event => {
+ const start = event.start.startOf('day');
+ const end = event.end.plus({ days: 1 }).startOf('day');
+ return {
+ ...event,
+ start,
+ end
+ };
+ });
+ for (var w = 5; w > -1; w--) {
+ for (var d = 6; d > -1; d--) {
+ const startdate = this.axisMain[w].plus(this.axisParts[d]);
+ events.unshift({
+ start: startdate,
+ end: startdate.plus({ days: 1 }),
+ orig: 'header'
+ });
+ }
+ }
+ return events;
+ })
+ };
+ },
+ inject: {
+ events: "events",
+ timezone: "timezone"
+ },
+ props: {
+ day: {
+ type: luxon.DateTime,
+ required: true
+ }
+ },
+ computed: {
+ axisMain() {
+ const start = this.day.startOf('month').startOf('week', { useLocaleWeeks: true });
+ return Array.from({ length: 6 }, (e, i) => start.plus({ weeks: i }));
+ },
+ axisParts() {
+ return Array.from({ length: 8 }, (e, i) => luxon.Duration.fromObject({ days: i }));
+ }
+ },
+ methods: {
+ isToday(date) {
+ return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
+ }
+ },
+ template: /* html */`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+}
diff --git a/public/js/components/Calendar/Mode/Week.js b/public/js/components/Calendar/Mode/Week.js
new file mode 100644
index 000000000..f8653b9e9
--- /dev/null
+++ b/public/js/components/Calendar/Mode/Week.js
@@ -0,0 +1,113 @@
+import BaseSlider from '../Base/Slider.js';
+import WeekView from './Week/View.js';
+
+export default {
+ name: "ModeWeek",
+ components: {
+ BaseSlider,
+ WeekView
+ },
+ props: {
+ currentDate: {
+ type: luxon.DateTime,
+ required: true
+ }
+ },
+ emits: [
+ "update:currentDate",
+ "update:range",
+ "click",
+ "requestModalOpen"
+ ],
+ data() {
+ return {
+ focusDate: this.currentDate,
+ rangeOffset: 0
+ };
+ },
+ computed: {
+ range() {
+ let first = this.focusDate.startOf('week', { useLocaleWeeks: true });
+ let last = this.focusDate.endOf('week', { useLocaleWeeks: true });
+
+ if (this.rangeOffset != 0) {
+ if (this.rangeOffset < 0) {
+ first = first.plus({ weeks: this.rangeOffset });
+ } else {
+ last = last.plus({ weeks: this.rangeOffset });
+ }
+ }
+
+ return luxon.Interval.fromDateTimes(first, last);
+ }
+ },
+ watch: {
+ currentDate() {
+ if (this.currentDate.locale != this.focusDate.locale) {
+ this.focusDate = this.currentDate;
+ this.$emit('update:range', this.range);
+ } else {
+ this.rangeOffset = this.currentDate.startOf('week', { useLocaleWeeks: true }).diff(this.focusDate.startOf('week', { useLocaleWeeks: true }), 'weeks').weeks;
+ if (this.rangeOffset) {
+ this.$refs.view.$refs.grid.disableAutoScroll();
+ this.$emit('update:range', this.range);
+ this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
+ }
+ }
+ }
+ },
+ methods: {
+ prevPage() {
+ this.rangeOffset = this.$refs.slider.target - 1;
+ this.$refs.view.$refs.grid.disableAutoScroll();
+ this.$emit('update:range', this.range);
+ this.$refs.slider.prevPage().then(this.updatePage);
+ },
+ nextPage() {
+ this.rangeOffset = this.$refs.slider.target + 1;
+ this.$refs.view.$refs.grid.disableAutoScroll();
+ this.$emit('update:range', this.range);
+ this.$refs.slider.nextPage().then(this.updatePage);
+ },
+ updatePage(weeks) {
+ const newFocusDate = this.focusDate.plus({ weeks });
+ this.focusDate = newFocusDate;
+ this.rangeOffset = 0;
+ this.$emit('update:currentDate', this.focusDate);
+ this.$emit('update:range', this.range);
+ this.$refs.view.$refs.grid.enableAutoScroll();
+ },
+ viewAttrs(weeks) {
+ const day = this.focusDate.plus({ weeks });
+ return { ...this.$attrs, day };
+ },
+ handleClickDefaults(evt) {
+ switch (evt.detail.source) {
+ case 'day':
+ // default: Set current-date
+ this.$emit('update:currentDate', evt.detail.value);
+ break;
+ case 'event':
+ // default: Request Modal
+ this.$emit('requestModalOpen', { event: evt.detail.value });
+ break;
+ }
+ }
+ },
+ mounted() {
+ this.$emit('update:range', this.range);
+ this.$refs.view.$refs.grid.enableAutoScroll();
+ },
+ template: `
+
+
+
+
+
+
+
+ `
+}
diff --git a/public/js/components/Calendar/Mode/Week/View.js b/public/js/components/Calendar/Mode/Week/View.js
new file mode 100644
index 000000000..612c4ad55
--- /dev/null
+++ b/public/js/components/Calendar/Mode/Week/View.js
@@ -0,0 +1,85 @@
+import CalendarGrid from '../../Base/Grid.js';
+import LabelDay from '../../Base/Label/Day.js';
+import LabelDow from '../../Base/Label/Dow.js';
+import LabelTime from '../../Base/Label/Time.js';
+
+export default {
+ name: "WeekView",
+ components: {
+ CalendarGrid,
+ LabelDay,
+ LabelDow,
+ LabelTime
+ },
+ inject: {
+ timeGrid: "timeGrid",
+ timezone: "timezone"
+ },
+ props: {
+ day: {
+ type: luxon.DateTime,
+ required: true
+ },
+ collapseEmptyDays: Boolean
+ },
+ computed: {
+ start() {
+ return this.day.startOf('week', { useLocaleWeeks: true });
+ },
+ axisMain() {
+ return Array.from({ length: 7 }, (e, i) => this.start.plus({ days: i }));
+ },
+ axisParts() {
+ if (this.timeGrid) {
+ // create {start, end} array
+ return this.timeGrid.map(tu => {
+ return {
+ start: luxon.Duration.fromISOTime(tu.start),
+ end: luxon.Duration.fromISOTime(tu.end)
+ };
+ });
+ } else {
+ // create 07:00-23:00
+ return Array.from({ length: 17 }, (e, i) => luxon.Duration.fromObject({ hours: i + 7 }));
+ }
+ }
+ },
+ methods: {
+ isToday(date) {
+ return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
+ }
+ },
+ template: /* html */`
+
+
+
+
+ evt.detail.source = 'day'"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+}
diff --git a/public/js/components/Cis/LvPlan/LvPlan.js b/public/js/components/Cis/LvPlan/LvPlan.js
index 14c9b77a2..cdb2c38c1 100644
--- a/public/js/components/Cis/LvPlan/LvPlan.js
+++ b/public/js/components/Cis/LvPlan/LvPlan.js
@@ -1,353 +1,246 @@
-import FhcCalendar from "../../Calendar/Calendar.js";
-import CalendarDate from "../../../composables/CalendarDate.js";
-import LvModal from "../Mylv/LvModal.js";
-import LvMenu from "../Mylv/LvMenu.js"
+import FhcCalendar from "../../Calendar/Base.js";
+
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
+import { useEventLoader } from '../../../composables/EventLoader.js';
+
+import ModeDay from '../../Calendar/Mode/Day.js';
+import ModeWeek from '../../Calendar/Mode/Week.js';
+import ModeMonth from '../../Calendar/Mode/Month.js';
+
export const DEFAULT_MODE_LVPLAN = 'Week'
-const LvPlan = {
+export default {
name: 'LvPlan',
+ components: {
+ FhcCalendar
+ },
+ inject: [
+ "renderers"
+ ],
+ props: {
+ viewData: Object, // NOTE(chris): this is inherited from router-view
+ propsViewData: Object
+ },
data() {
+ const now = luxon.DateTime.now().setZone(this.viewData.timezone);
return {
- events: null,
+ modes: {
+ day: Vue.markRaw(ModeDay),
+ week: Vue.markRaw(ModeWeek),
+ month: Vue.markRaw(ModeMonth)
+ },
+ modeOptions: {
+ day: {
+ emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
+ emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')),
+ compact: false
+ },
+ week: {
+ collapseEmptyDays: false
+ }
+ },
+ currentDay: this.propsViewData?.focus_date,
calendarMode: this.propsViewData?.mode ?? DEFAULT_MODE_LVPLAN,
- calendarDate: new CalendarDate(new Date()),
- eventCalendarDate: new CalendarDate(new Date()),
- currentlySelectedEvent: null,
- currentDay: this.propsViewData?.focus_date ? new Date(this.propsViewData.focus_date) : new Date(),
- lv: null,
- minimized: false,
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
- uid: null,
- isModalContentResolved: false,
- isModalTitleResolved: false,
- isShowModal: false,
- }
- },
- props: {
- viewData: Object, // NOTE(chris): this is inherited from router-view
- propsViewData: Object,
- rowMinHeight: {
- type: String,
- default: '100px'
- },
- eventMaxHeight: {
- type: String,
- default: '125px'
- }
- },
- provide() {
- return {
- rowMinHeight: this.rowMinHeight,
- eventMaxHeight: this.eventMaxHeight
- }
- },
- inject:["renderers"],
- watch: {
- /* events:{
- handler: function(newValue){
- if(newValue == null)
- setTimeout(()=>{
- if(this.events == null){
- this.loadEvents();
- }
- },500);
- },
- immediate: true,
- }, */
- modalLoaded:{
- handler: function (newValue) {
- if (this.isShowModal && newValue.isModalContentResolved && newValue.isModalTitleResolved) {
- this.$nextTick(() => {
- if(this.$refs.lvmodal) this.$refs.lvmodal.show();
- this.isShowModal = false;
- });
- }
- },
- immediate: true
- },
- weekFirstDay: {
- handler: async function (newValue) {
- let data = await this.fetchStudiensemesterDetails(newValue);
- let { studiensemester_kurzbz, start, ende } = data.data;
- this.studiensemester_kurzbz = studiensemester_kurzbz;
- this.studiensemester_start = start;
- this.studiensemester_ende = ende;
- },
- immediate: true,
- },
- // forward/backward on history entries happening in lvplan
- 'propsViewData.lv_id'(newVal) {
- // relevant if lv_id can be changed from within this component
- },
- 'propsViewData.mode'(newVal) {
- if(this.$refs.calendar) this.$refs.calendar.setMode(newVal)
- },
- 'propsViewData.focus_date'(newVal) {
- this.currentDate = new Date(newVal)
- }
- },
- components: {
- FhcCalendar, LvModal, LvMenu
+ uid: null
+ };
},
computed:{
- modalLoaded: function(){
- return { isModalContentResolved: this.isModalContentResolved, isModalTitleResolved:this.isModalTitleResolved};
- },
-
- downloadLinks: function(){
- if(!this.studiensemester_start || !this.studiensemester_ende || !this.uid )return;
- let start = new Date(this.studiensemester_start);
- start = Math.floor(start.getTime()/1000);
- let ende = new Date(this.studiensemester_ende);
- ende = Math.floor(ende.getTime() / 1000);
+ backgrounds() {
+ let now = luxon.DateTime.now().setZone(this.viewData.timezone);
+
+ if (this.calendarMode == 'Month')
+ return [
+ {
+ class: 'background-past',
+ end: now.startOf('day')
+ }
+ ];
- let download_link =
- (format, version = "", target = "") =>
- `${FHC_JS_DATA_STORAGE_OBJECT.app_root}cis/private/lvplan/stpl_kalender.php?type=student&pers_uid=
- ${this.uid}&begin=${start}&ende=${ende}&format=${format}
- ${version ? '&version=' + version : ''}${target ? '&target=' + target : ''}`;
return [
- { title: "excel", icon: 'fa-solid fa-file-excel', link: download_link('excel') },
- { title: "csv", icon: 'fa-solid fa-file-csv', link: download_link('csv') },
- { title: "ical1", icon: 'fa-regular fa-calendar', link: download_link('ical', '1', 'ical') },
- { title: "ical2", icon: 'fa-regular fa-calendar', link: download_link('ical', '2', 'ical') }
+ {
+ class: 'background-past',
+ end: now,
+ label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
+ }
];
},
- weekFirstDay: function () {
- return this.calendarDateToString(this.calendarDate.cdFirstDayOfWeek);
- },
- weekLastDay: function () {
- return this.calendarDateToString(this.calendarDate.cdLastDayOfWeek);
- },
- monthFirstDay: function () {
- return this.calendarDateToString(this.eventCalendarDate.cdFirstDayOfCalendarMonth);
- },
- monthLastDay: function () {
- return this.calendarDateToString(this.eventCalendarDate.cdLastDayOfCalendarMonth);
- },
- },
- methods:{
- modalTitleResolved: function () {
- this.isModalTitleResolved = true;
+ downloadLinks() {
+ if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
+ return false;
- },
- modalContentResolved: function () {
- this.isModalContentResolved = true;
-
- },
- // component renderers fetches from different addons
- modalTitleComponent(type){
- return this.renderers[type]?.modalTitle;
- },
- modalContentComponent(type) {
- return this.renderers[type]?.modalContent;
- },
- calendarEventComponent(type){
- return this.renderers[type]?.calendarEvent;
- },
-
+ const opts = { zone: this.viewData.timezone };
+ const start = luxon.DateTime
+ .fromISO(this.studiensemester_start, opts)
+ .toUnixInteger();
+ const ende = luxon.DateTime
+ .fromISO(this.studiensemester_ende, opts)
+ .toUnixInteger();
- fetchStudiensemesterDetails: async function (date) {
- return this.$api.call(ApiLvPlan.studiensemesterDateInterval(date));
+ const download_link = FHC_JS_DATA_STORAGE_OBJECT.app_root
+ + 'cis/private/lvplan/stpl_kalender.php'
+ + '?type=student'
+ + '&pers_uid=' + this.uid
+ + '&begin=' + start
+ + '&ende=' + ende;
+
+ return [
+ { title: "excel", icon: 'fa-solid fa-file-excel', link: download_link + '&format=excel' },
+ { title: "csv", icon: 'fa-solid fa-file-csv', link: download_link + '&format=csv' },
+ { title: "ical1", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=1&target=ical' },
+ { title: "ical2", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=2&target=ical' }
+ ];
+ }
+ },
+ methods: {
+ eventStyle(event) {
+ if (!event.farbe)
+ return undefined;
+ return '--event-bg:#' + event.farbe;
},
-
- setSelectedEvent: function (event) {
- this.currentlySelectedEvent = event;
- },
- selectDay: function(day){
- const date = day.getFullYear() + "-" +
- String(day.getMonth() + 1).padStart(2, "0") + "-" +
- String(day.getDate()).padStart(2, "0");
- const capitalizedMode = this.calendarMode[0].toUpperCase() + this.calendarMode.slice(1);
+ handleChangeDate(day) {
+ const focus_date = day.toISODate();
+ const mode = this.calendarMode[0].toUpperCase() + this.calendarMode.slice(1);
this.$router.push({
name: "LvPlan",
params: {
- mode: capitalizedMode,
- focus_date: date,
+ mode,
+ focus_date,
lv_id: this.propsViewData?.lv_id || null
}
})
this.currentDay = day;
},
- handleChangeMode(mode) {
- let m = mode[0].toUpperCase() + mode.slice(1)
- if(m === this.calendarMode) return; // TODO(chris): check for date and lv_id too!
- const date = this.currentDay.getFullYear() + "-" +
- String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
- String(this.currentDay.getDate()).padStart(2, "0");
-
- if (m == 'Weeks' || m == 'Years' || m == 'Months') return;
+ handleChangeMode(newMode) {
+ const mode = newMode[0].toUpperCase() + newMode.slice(1)
+ const focus_date = (this.currentDay instanceof luxon.DateTime)
+ ? this.currentDay.toISODate()
+ : this.currentDay;
this.$router.push({
name: "LvPlan",
params: {
- mode: m,
- focus_date: date,
+ mode,
+ focus_date,
lv_id: this.propsViewData?.lv_id ?? null
}
- })
- this.calendarMode = m
- },
- showModal: function(event){
- this.currentlySelectedEvent = event;
- Vue.nextTick(()=>{
- if(this.isModalContentResolved && this.isModalTitleResolved){
- if(this.$refs.lvmodal) this.$refs.lvmodal.show();
- }
- else{
- this.isShowModal = true;
- }
- })
- },
- updateRange: function ({start,end, mounted}) {
- let checkDate = (date) => {
- return date.m != this.eventCalendarDate.m || date.y != this.eventCalendarDate.y;
- }
- this.calendarDate = new CalendarDate(end);
-
- // only load month data if the month or year has changed
- // or we receive a reload flag from the mounted routine of the components
- // or this handler is being called from the mounted lifecycle of a component
- if (mounted || (checkDate(new CalendarDate(start)) && checkDate(new CalendarDate(end)))){
- // reset the events before querying the new events to activate the loading spinner
- this.events = null;
- this.eventCalendarDate = new CalendarDate(end);
- this.loadEvents();
- }
- },
- calendarDateToString: function (calendarDate) {
- return calendarDate instanceof CalendarDate ?
- [calendarDate.y, calendarDate.m + 1, calendarDate.d].join('-') :
- null;
-
- },
- loadEvents: function(){
- Promise.allSettled([
- this.$api.call(ApiLvPlan.LvPlanEvents(this.monthFirstDay, this.monthLastDay, this.propsViewData.lv_id)),
- this.$api.call(ApiLvPlan.getLvPlanReservierungen(this.monthFirstDay, this.monthLastDay))
- ]).then((result) => {
- let promise_events = [];
- result.forEach((promise_result) => {
- if (promise_result.status === 'fulfilled' && promise_result.value.meta.status === "success") {
-
- if(promise_result.value.meta?.lv) this.lv = promise_result.value.meta.lv
-
- let data = promise_result.value.data;
- // adding additional information to the events
- if (data && data.forEach) {
-
- data.forEach((el, i) => {
- el.id = i;
- if (el.type === 'reservierung') {
- el.color = '#' + (el.farbe || 'FFFFFF');
- } else {
- el.color = '#' + (el.farbe || 'CCCCCC');
- }
-
- el.start = new Date(el.datum + ' ' + el.beginn);
- el.end = new Date(el.datum + ' ' + el.ende);
-
- });
- }
- promise_events = promise_events.concat(data);
- }
- })
- this.events = promise_events;
});
+
+ this.calendarMode = mode;
},
+ updateRange(rangeInterval) {
+ this.rangeInterval = rangeInterval;
+ this.$api
+ .call(ApiLvPlan.studiensemesterDateInterval(
+ this.rangeInterval.end.startOf('week').toISODate()
+ ))
+ .then(res => {
+ this.studiensemester_kurzbz = res.data.studiensemester_kurzbz;
+ this.studiensemester_start = res.data.start;
+ this.studiensemester_ende = res.data.ende;
+ });
+ }
+ },
+ setup(props) {
+ const $api = Vue.inject('$api');
+
+ const rangeInterval = Vue.ref(null);
+
+ const { events, lv } = useEventLoader(rangeInterval, (start, end) => {
+ return [
+ $api.call(ApiLvPlan.LvPlanEvents(start.toISODate(), end.toISODate(), props.propsViewData.lv_id)),
+ $api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
+ ];
+ });
+
+ return {
+ rangeInterval,
+ events,
+ lv
+ };
},
created() {
-
-
this.$api
.call(ApiAuthinfo.getAuthUID())
- .then(res => res.data)
- .then(data => {
- this.uid = data.uid;
+ .then(res => {
+ this.uid = res.data.uid;
});
-
- // this.loadEvents();
- },
- beforeUnmount() {
- if(this.$refs.lvmodal) this.$refs.lvmodal.hide()
},
template:/*html*/`
-
-
- {{$p.t('lehre/stundenplan')}}
- {{studiensemester_kurzbz}}
- {{ $p.user_language.value === 'German' ? lv?.bezeichnung : lv?.bezeichnung_english}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-