Merge branch 'feature-60973/komponente_fuer_lehrfaecherverteilung' into feature-61232/Studierendenverwaltung_Karteireiter_Projektarbeit_portieren

This commit is contained in:
Alexei Karpenko
2025-08-23 16:16:41 +02:00
362 changed files with 23905 additions and 7774 deletions
+331 -56
View File
@@ -1,59 +1,238 @@
/* basic */
@import '../components/searchbar/searchbar.css';
@import '../components/Sprachen.css';
@import './Cis/Calendar.css';
html {
font-size: 0.875em;
}
/* variables */
:root {
--fhc-cis-primary: #00649c;
--fhc-cis-primary-hover: #005585;
--fhc-cis-header-px: 1rem;
--fhc-cis-header-py: .5rem;
--fhc-cis-main-px: 1.5rem;
--fhc-cis-main-py: 1.5rem;
--fhc-cis-menu-lvl-1-bg: #00649c;
--fhc-cis-menu-lvl-1-color: #fff;
--fhc-cis-menu-lvl-1-bg-hover: #005585;
--fhc-cis-menu-lvl-1-color-hover: #fff;
--fhc-cis-menu-lvl-2-bg: #343a40;
--fhc-cis-menu-lvl-2-color: #fff;
--fhc-cis-menu-lvl-2-bg-hover: #2c3136;
--fhc-cis-menu-lvl-2-color-hover: #fff;
--fhc-cis-menu-lvl-2-color-active: #292b2a;
--fhc-cis-menu-lvl-3-bg: #72777A;
--fhc-cis-menu-lvl-3-color: #fff;
--fhc-cis-menu-lvl-3-bg-hover: #616568;
--fhc-cis-menu-lvl-3-color-hover: #fff;
--fhc-cis-menu-lvl-4-bg: #ced4da;
--fhc-cis-menu-lvl-4-color: #000;
--fhc-cis-menu-lvl-4-bg-hover: #d5dae0;
--fhc-cis-menu-lvl-4-color-hover: #000;
--fhc-cis-menu-lvl-5-bg: #e9ecef;
--fhc-cis-menu-lvl-5-color: #000;
--fhc-cis-menu-lvl-5-bg-hover: #eceff1;
--fhc-cis-menu-lvl-5-color-hover: #000;
--fhc-cis-grade-positive: rgb(0, 128, 0);
--fhc-cis-grade-negative: rgb(128, 0, 0);
.fhc-dark-bg{
background-color: var(--fhc-dark);
color:var(--fhc-light) !important;
}
.fhc-dark-color {
color: var(--fhc-dark);
}
.fhc-dark-bg:hover{
filter: brightness(120%);
}
.fhc-primary{
background-color: var(--fhc-primary) !important;
color: var(--fhc-light) !important;
}
.fhc-primary-color{
color: var(--fhc-primary) !important;
}
.fhc-primary-bg {
background-color: var(--fhc-primary) !important;
}
.fhc-primary-highlight-bg {
background-color: var(--fhc-primary-highlight) !important;
}
.fhc-primary-highlight-color {
color: var(--fhc-primary-highlight) !important;
}
.fhc-body-bg{
background-color:var(--fhc-background) !important;
}
.fhc-body-color{
color: var(--fhc-text) !important;
}
.fhc-secondary-bg{
background-color: var(--fhc-tertiary) !important;
}
.fhc-secondary-color {
color: var(--fhc-tertiary) !important;
}
.fhc-tertiary-bg{
background-color: var(--fhc-secondary) !important;
}
.fhc-tertiary-color {
color: var(--fhc-secondary) !important;
}
.fhc-fourth-bg {
background-color: var(--fhc-fourth) !important;
}
.fhc-fourth-color {
color: var(--fhc-fourth) !important;
}
.fhc-fifth-bg {
background-color: var(--fhc-fifth) !important;
}
.fhc-fifth-color {
color: var(--fhc-fifth) !important;
}
.fhc-body{
color:var(--fhc-text);
background-color: var(--fhc-background);
border-color: var(--fhc-border);
}
.fhc-secondary {
color: var(--fhc-text);
background-color: var(--fhc-tertiary);
border-color: var(--fhc-border);
}
.fhc-tertiary{
color: var(--fhc-text);
background-color: var(--fhc-secondary);
border-color:var(--fhc-border);
}
.fhc-link-color{
color: var(--fhc-link);
}
.fhc-text{
color: var(--fhc-text) !important;
}
.fhc-text-light{
color: var(--fhc-light) !important;
}
:root{
/* sidebar colors */
--fhc-cis-sidebar-bg: var(--fhc-secondary);
--fhc-cis-menu-bg: var(--fhc-secondary);
--fhc-cis-header-px: 1rem;
--fhc-cis-header-py: .5rem;
--fhc-cis-main-px: 1.5rem;
--fhc-cis-main-py: 1.5rem;
--fhc-cis-menu-lvl-1-bg: var(--fhc-primary);
--fhc-cis-menu-lvl-1-color: var(--fhc-light);
--fhc-cis-menu-lvl-1-bg-hover: var(--fhc-primary-highlight);
--fhc-cis-menu-lvl-1-color-hover: var(--fhc-light);
--fhc-cis-menu-lvl-2-bg: var(--fhc-secondary);
--fhc-cis-menu-lvl-2-color: var(--fhc-text);
--fhc-cis-menu-lvl-2-bg-hover: var(--fhc-secondary-highlight);
--fhc-cis-menu-lvl-2-color-hover: var(--fhc-text);
--fhc-cis-menu-lvl-3-bg: var(--fhc-tertiary);
--fhc-cis-menu-lvl-3-color: var(--fhc-text);
--fhc-cis-menu-lvl-3-bg-hover: var(--fhc-tertiary-highlight);
--fhc-cis-menu-lvl-3-color-hover: var(--fhc-text);
--fhc-cis-menu-lvl-4-bg: var(--fhc-fourth);
--fhc-cis-menu-lvl-4-color: var(--fhc-text);
--fhc-cis-menu-lvl-4-bg-hover: var(--fhc-fourth-highlight);
--fhc-cis-menu-lvl-4-color-hover: var(--fhc-text);
--fhc-cis-menu-lvl-5-bg: var(--fhc-fifth);
--fhc-cis-menu-lvl-5-color: var(--fhc-text);
--fhc-cis-menu-lvl-5-bg-hover: var(--fhc-fifth-highlight);
--fhc-cis-menu-lvl-5-color-hover: var(--fhc-text);
--fhc-cis-grade-positive: var(--fhc-success);
--fhc-cis-grade-negative: var(--fhc-danger);
}
#themeSwitch i{
color: var(fhc-light) !important;
}
@media (min-width: 992px) {
:root {
--fhc-cis-menu-width: 250px;
--fhc-cis-header-height: 60px;
--fhc-cis-menu-bg: #72777A;
}
:root {
--fhc-cis-menu-width: 250px;
--fhc-cis-header-height: 60px;
}
}
@media (max-width: 991.98px) {
:root {
--fhc-cis-menu-width: 400px;
--fhc-cis-header-height: 40px;
--fhc-cis-menu-bg: #343a40;
}
:root {
--fhc-cis-menu-width: 400px;
--fhc-cis-header-height: 40px;
}
}
/* lvMenu entry style */
#cis-menu .menu-entry {
color: var(--fhc-text);
}
#cis-menu .menu-entry .submenu{
background-color: var(--fhc-background-highlight);
color: inherit;
}
#cis-menu .menu-entry:hover .submenu{
background-color: var(--fhc-primary-highlight);
color: inherit;
}
#cis-menu .menu-entry:hover .submenu:hover{
color: var(--fhc-dark);
}
#cis-menu [disabled="true"].menu-entry .submenu{
opacity: 0.5;
background-color: var(--fhc-secondary)
}
[disabled="true"].menu-entry {
pointer-events: none;
cursor: default;
background-color: var(--fhc-disabled) !important;
color: var(--fhc-dark) !important;
opacity: 0.75;
-webkit-touch-callout: none;
/* iOS Safari */
-webkit-user-select: none;
/* Safari */
-khtml-user-select: none;
/* Konqueror HTML */
-moz-user-select: none;
/* Old versions of Firefox */
-ms-user-select: none;
/* Internet Explorer/Edge */
user-select: none;
}
#cis-menu .menu-entry:hover{
background-color: var(--fhc-primary);
color:var(--fhc-light);
}
/* lvMenu entry disabled style */
[disabled="true"].menu-entry {
pointer-events: none;
cursor: default;
background-color: #dadada !important;
color: black !important;
-webkit-touch-callout: none;
/* iOS Safari */
-webkit-user-select: none;
/* Safari */
-khtml-user-select: none;
/* Konqueror HTML */
-moz-user-select: none;
/* Old versions of Firefox */
-ms-user-select: none;
/* Internet Explorer/Edge */
user-select: none;
}
/* buttons */
#cis-header .btn-level-1,
@@ -195,6 +374,7 @@ html {
/* searchbar */
#nav-search {
background-color: var(--fhc-primary);
z-index: 1;
}
#nav-search.me-3 {
@@ -223,9 +403,16 @@ html {
}
#cis-main {
color: var(--fhc-text);
background-color: var(--fhc-background);
border-color: var(--fhc-border);
padding: var(--fhc-cis-main-py) var(--fhc-cis-main-px);
}
#cis-main .fa-arrow-up-right-from-square {
color: var(--fhc-link) !important;
}
#nav-main-sticky {
max-height: calc(100vh - var(--fhc-cis-header-height));
}
@@ -245,10 +432,6 @@ html {
display: flex;
}
.fhc-active {
background-color: var(--fhc-cis-menu-lvl-2-color-active) !important;
}
.fhc-seperator {
position: relative;
}
@@ -261,7 +444,7 @@ html {
margin-left: 12.5%;
width: 75%; /* Full width of the parent element */
height: 1px; /* Height of the border */
background: linear-gradient(to right, #ffffff, var(--fhc-cis-primary), #ffffff);
background: linear-gradient(to right, var(--fhc-background), var(--fhc-primary), var(--fhc-background));
opacity: 0.8; /* Adjust opacity for a subtle fade effect */
}
@@ -295,7 +478,7 @@ html {
left: 0;
height: var(--fhc-cis-header-height);
width: var(--fhc-cis-menu-width);
background-color: var(--fhc-cis-primary);
background-color: var(--fhc-primary);
padding: var(--fhc-cis-header-py) var(--fhc-cis-header-px);
z-index: 2;
}
@@ -327,7 +510,7 @@ html {
}
#nav-main-toggle .collapsed .fa-arrow-circle-left {
transform: scaleX(-1);
color: var(--bs-dark);
color: var(--fhc-black-40);
}
#nav-search {
position: fixed;
@@ -345,7 +528,7 @@ html {
#nav-user-btn {
border-width: 0;
padding: var(--fhc-cis-header-py) var(--fhc-cis-header-px);
background-color: var(--fhc-cis-primary);
background-color: var(--fhc-primary);
}
#nav-user-btn img {
object-fit: contain;
@@ -355,7 +538,7 @@ html {
#nav-user-menu {
position: absolute;
min-width: var(--fhc-cis-menu-width);
background-color: var(--fhc-cis-menu-lvl-2-bg);
background-color: var(--fhc-dark);
}
#nav-user-menu img {
@@ -385,12 +568,13 @@ html {
@media (max-width: 991.98px) {
#cis-main{
padding: 0.75rem 0.75rem;
height: calc(100vh - var(--fhc-cis-header-height));
}
#cis-main > :first-child {
margin-top: var(--fhc-cis-header-height);
}
#cis-header {
background-color: var(--fhc-cis-primary);
background-color: var(--fhc-primary);
height: var(--fhc-cis-header-height);
position: fixed;
width: 100vw;
@@ -427,18 +611,23 @@ html {
}
#nav-user-menu {
position: absolute;
background-color: var(--fhc-cis-menu-lvl-2-bg);
background-color: var(--fhc-dark);
}
#nav-main-menu {
display: block;
}
}
#nav-main{
background-color: var(--fhc-cis-sidebar-bg);
}
/* Typography */
.fhc-header {
display: flex;
justify-content: space-between;
align-items: end;
border-bottom: 1px solid #dee2e6;
border-bottom: 1px solid var(--fhc-border);
margin: 1rem 0;
}
.fhc-header > h1 {
@@ -450,7 +639,7 @@ html {
}
}
.fhc-header > h1 > small {
color: #6c757d;
color: var(--fhc-tertiary);
font-size: 1rem;
}
.fhc-header > h1 > small:before {
@@ -461,7 +650,7 @@ html {
text-decoration: none;
}
.fhc-header > a:after {
color: var(--bs-body-color);
color: var(--fhc-text);
content: "\f059";
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
@@ -478,7 +667,7 @@ html {
[disabled="true"].fhc-entry {
pointer-events: none;
cursor: default;
background-color: #dadada !important;
background-color: var(--fhc-disabled) !important;
color: black !important;
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none;/* Safari */
@@ -579,4 +768,90 @@ html {
.dashboard-item {
overscroll-behavior: none;
}
.abgabe-zieldatum-border-red,
.abgabe-zieldatum-border-yellow,
.abgabe-zieldatum-border-pink,
.abgabe-zieldatum-border-green {
border: solid 3px lightgrey;
}
.abgabe-zieldatum-border-red {
border-color: red;
}
.abgabe-zieldatum-border-yellow {
border-color: yellow;
}
.abgabe-zieldatum-border-pink {
border-color: pink;
}
.abgabe-zieldatum-border-green {
border-color: green;
}
.abgabe-zieldatum-overlay {
top: 3px;
right: 3px;
}
.abgabe-zieldatum-border-red .abgabe-zieldatum-overlay {
color: red;
}
.abgabe-zieldatum-border-yellow .abgabe-zieldatum-overlay {
color: yellow;
}
.abgabe-zieldatum-border-pink .abgabe-zieldatum-overlay {
color: pink;
}
.abgabe-zieldatum-border-green .abgabe-zieldatum-overlay {
color: green;
}
.stgkontaktinfo {
display: grid;
grid-template-columns: 25px auto;
word-wrap: anywhere;
}
.mylv-semester-studiengang-lv .card-header {
min-height: 55px;
}
#lektorenContainer{
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
}
@media(max-width: 990px) {
#lektorenContainer{
grid-template-columns: 1fr 1fr 1fr;
}
}
@media(max-width: 575px) {
#lektorenContainer {
grid-template-columns: 1fr 1fr;
}
}
#cis-main .modal-header{
background-color: var(--fhc-primary);
color: var(--fhc-light);
}
#cis-main .modal-header .btn-close{
filter: invert(1);
}
#cis-main .modal-footer {
background-color: var(--fhc-secondary);
}
+13
View File
@@ -0,0 +1,13 @@
/* mobile */
@media (max-width: 991.98px) {
.responsive-calendar .fhc-calendar-mode-week .fhc-calendar-base-label-day .short,
.responsive-calendar .fhc-calendar-mode-week .fhc-calendar-base-label-dow .short,
.responsive-calendar .fhc-calendar-mode-month .fhc-calendar-base-label-dow .short {
display: block;
}
.responsive-calendar .fhc-calendar-mode-week .fhc-calendar-base-label-day .long,
.responsive-calendar .fhc-calendar-mode-week .fhc-calendar-base-label-dow .long,
.responsive-calendar .fhc-calendar-mode-month .fhc-calendar-base-label-dow .long {
display: none;
}
}
+4 -4
View File
@@ -11,14 +11,14 @@
margin-inline-start: 0 !important;
}
#cms a.btn-outline-primary {
color: var(--fhc-cis-primary);
border-color: var(--fhc-cis-primary);
color: var(--fhc-primary);
border-color: var(--fhc-primary);
}
#cms a.btn-outline-primary:hover,
#cms a.btn-outline-primary:focus {
color: #fff;
background-color: var(--fhc-cis-primary);
border-color: var(--fhc-cis-primary);
background-color: var(--fhc-primary);
border-color: var(--fhc-primary);
}
#cms a.btn-primary {
color: var(--fhc-cis-menu-lvl-1-color);
+99
View File
@@ -0,0 +1,99 @@
/* All */
.weekPageContainer,
.dayPageContainer {
height: 100%;
}
/* Feiertage */
.cis-renderer-feiertage-calendar-event {
align-items: center;
color: var(--fhc-light);
}
.monthPageContainer .cis-renderer-feiertage-calendar-event,
.weekPageContainer .cis-renderer-feiertage-calendar-event,
.dayPageContainer .cis-renderer-feiertage-calendar-event {
display: grid;
grid-template-columns: auto 1fr;
justify-items: center;
}
.listPageContainer .cis-renderer-feiertage-calendar-event {
display: flex;
justify-content: center;
}
.monthPageContainer .cis-renderer-feiertage-calendar-event .event-icon {
margin: 0 .5rem;
}
.weekPageContainer .cis-renderer-feiertage-calendar-event .event-icon,
.dayPageContainer .cis-renderer-feiertage-calendar-event .event-icon,
.listPageContainer .cis-renderer-feiertage-calendar-event .event-icon {
margin: .5rem;
}
.listPageContainer .cis-renderer-feiertage-calendar-event .event-title {
flex-grow: 1;
}
/* Lehreinheit */
/* Reservierungen */
.monthPageContainer .calendar-event-default {
padding: 0!important;
}
.monthPageContainer .calendar-event-default .event-time,
.monthPageContainer .calendar-event-default .event-lectors {
display: none!important;
}
.monthPageContainer .calendar-event-default .event-text {
display: flex;
justify-content: space-evenly;
align-items: center;
}
.weekPageContainer .calendar-event-default {
display: grid;
grid-template-columns: auto 1fr;
}
.weekPageContainer .calendar-event-default .event-time {
border-inline-end: solid 1px var(--bs-secondary);
align-self: stretch;
align-content: center;
padding: .25em .5em .25em .25em;
margin-inline-end: .25rem;
}
.weekPageContainer .calendar-event-default .event-text {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
}
.dayPageContainer .calendar-event-default,
.listPageContainer .calendar-event-default {
display: flex;
align-items: center;
}
.dayPageContainer .calendar-event-default .event-time,
.listPageContainer .calendar-event-default .event-time {
align-content: center;
padding-inline-end: .5em;
border-inline-end: solid 1px;
margin-inline-end: .5em;
}
.dayPageContainer .calendar-event-default .event-text,
.listPageContainer .calendar-event-default .event-text {
flex: 1 1 auto;
display: flex;
flex-wrap: wrap;
gap: .25em;
justify-content: center;
}
.dayPageContainer .calendar-event-default .event-topic,
.dayPageContainer .calendar-event-default .event-place,
.listPageContainer .calendar-event-default .event-topic,
.listPageContainer .calendar-event-default .event-place {
flex: 0 0 100%;
text-align: center;
}
.fhc-roominformation .calendar-event-default .event-place {
display: none;
}
+60
View File
@@ -0,0 +1,60 @@
.lvUebersicht{
display:grid;
gap: 1rem;
}
@media (min-width: 1440px) {
.lvUebersicht {
grid-template-columns: 1fr 1fr 1fr;
}
}
@media (max-width: 1439px){
.lvUebersicht{
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 576px){
.lvUebersicht{
grid-template-columns: 1fr;
}
}
.lvOptions {
display: grid;
gap: 1rem;
}
.lvOptions > div {
flex-basis: auto;
flex-grow: 1;
flex-shrink: 1;
}
.lvOptions button {
flex-shrink: 0;
}
@media (min-width: 1440px) {
.lvOptions {
grid-template-columns: 1fr 1fr 1fr 1fr;
}
}
@media (max-width: 1439px) {
.lvOptions {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 576px) {
.lvOptions {
grid-template-columns: 1fr;
}
}
+54 -2
View File
@@ -10,7 +10,7 @@
font-size: calc(1.325rem + .9vw);
}
.fhc-header > :first-child > small {
color: var(--bs-secondary);
color: var(--fhc-tertiary);
font-size: .65em;
padding-inline-start: 1em;
}
@@ -104,4 +104,56 @@
}
.modal-msg .modal-content {
height: 95vh;
}
}
.fhcSkipLink {
position: absolute;
left: -10px;
top: -10px;
width: 1px;
height: 1px;
overflow: hidden;
}
.fhcSkipLink::before {
content: "Skip to main content"
}
.fhcSkipLink:focus {
position: absolute;
z-index: 9999999;
top: var(--fhc-cis-header-height);
left: var(--fhc-cis-menu-width);
height: 100px;
width: 800px;
font-size: 24px;
font-weight: bold;
color: #0044cc;
background-color: #ffeb3b;
text-decoration: none;
padding: 12px 18px;
border-radius: 6px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease-in-out;
}
.fhcSkipLink:hover {
background-color: #ffcc00;
color: #002b80;
transform: scale(1.05);
}
.fhc-bullet::before {
content: "• ";
font-weight: bold;
display: inline-block;
transform: scale(2);
}
.fhc-bullet-red::before {
color: green;
}
.fhc-bullet-green::before {
color: red;
}
+13 -6
View File
@@ -1,8 +1,15 @@
:root{
--filter-widget-border: var(--fhc-dark, black);
--filter-widget-color: var(--fhc-dark, black);
--filter-widget-primary: var(--fhc-primary, #428bca);
--filter-widget-border-light: var(--fhc-border, #ddd);
}
.filter-name-title {
font-family: inherit;
font-size: 16px;
line-height: 1.1;
color: black;
color: var(--filter-widget-color);
}
.panel-title {
@@ -26,7 +33,7 @@
}
.drag-and-drop-fields-span {
border: 1px solid black;
border: 1px solid var(--filter-widget-border);
border-radius: 7px;
margin-right: 6px;
margin-bottom: 10px;
@@ -64,7 +71,7 @@
right: 100%;
height: 100%;
margin-right: 3px;
border-left: 2px solid #428bca;
border-left: 2px solid var(--filter-widget-primary);
}
.selection-after::after {
@@ -74,7 +81,7 @@
left: 100%;
height: 100%;
margin-left: 3px;
border-right: 2px solid #428bca;
border-right: 2px solid var(--filter-widget-primary);
}
.applied-filter-operation {
@@ -139,10 +146,10 @@
.up-down-border {
border-top-width: 1px;
border-top-color: #ddd;
border-top-color: var(--filter-widget-border-light);
border-top-style: solid;
border-bottom-width: 1px;
border-bottom-color: #ddd;
border-bottom-color: var(--filter-widget-border-light);
border-bottom-style: solid;
}
+4 -1
View File
@@ -14,14 +14,17 @@ textarea[name="anmerkung"] {
.lv_table_icon.icon-modul::before
{
content: '\f468';
color: #4e4ed2;
}
.lv_table_icon.icon-lv::before
{
content: '\f466';
color: black;
}
.lv_table_icon.icon-::before
{
content: '\f073';
}
color: #f3c541;
}
+10 -4
View File
@@ -1,3 +1,9 @@
:root{
--fhc-filtercomponent-primary: var(--fhc-primary, #428bca);
--fhc-filtercomponent-bg:var(--fhc-background, white);
--fhc-filtercomponent-border:var(--fhc-border, black);
}
.filter-header-title {
text-align: right;
}
@@ -26,9 +32,9 @@
margin-left: 7px;
padding: 11px;
display: inline-block;
background-color: white;
background-color: var(--fhc-filtercomponent-bg);
border-style: solid;
border-color: black;
border-color: var(--fhc-filtercomponent-border);
border-width: 1px;
border-radius: 7px;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
@@ -47,7 +53,7 @@
right: 100%;
height: 100%;
margin-right: 3px;
border-left: 2px solid #428bca;
border-left: 2px solid var(--fhc-filtercomponent-primary);
}
.selection-after::after {
@@ -57,7 +63,7 @@
left: 100%;
height: 100%;
margin-left: 3px;
border-right: 2px solid #428bca;
border-right: 2px solid var(--fhc-filtercomponent-primary);
}
.filter-table-dataset {
+11 -2
View File
@@ -1,5 +1,11 @@
/* FORM UNDERLINE
*/
:root {
--fhc-formunderline-border: var(--fhc-border, #dee2e6);
--fhc-formunderline-link: var(--fhc-link, initial);
--fhc-formunderline-titel-color: var(--fhc-text,gray);
}
.form-underline{
display: flex;
flex-direction: column;
@@ -8,11 +14,13 @@
}
a.form-underline-content{
color: var(--fhc-formunderline-link);
}
.form-underline .form-underline-content{
border-width: 1px;
border-color: transparent transparent #dee2e6 transparent;
border-color: transparent transparent var(--fhc-formunderline-border) transparent;
border-style: solid;
padding-bottom: 0.25rem;
padding-left: 0.5rem;
@@ -32,4 +40,5 @@
opacity: 0.65;
font-size: .85rem;
padding-left: 0.5rem;
color: var(--fhc-formunderline-titel-color) !important;
}
+5 -1
View File
@@ -1,4 +1,8 @@
:root{
--fhc-myLv-disabled: var(--fhc-disabled, #565e647f);
}
.unavailable, .unavailable:hover{
color: #565e647f !important;
color: var(--fhc-myLv-disabled) !important;
cursor: default;
}
+19 -9
View File
@@ -1,13 +1,24 @@
/*
* Hederer
*/
:root{
--fhc-nav-component-bg: var(--fhc-tertiary, #f8f8f8);
--fhc-nav-component-border: var(--fhc-border, #e7e7e7);
--fhc-nav-component-color: var(--fhc-text, #5e5e5e);
--fhc-nav-left-component-border: var(--fhc-border, #e7e7e7);
--fhc-nav-left-component-bg: var(--fhc-tertiary, #f8f8f8);
--fhc-nav-left-component-color: var(--fhc-primary, #23527c);
--fhc-nav-left-component-hover: var(--fhc-secondary-highlight, #eee);
}
.navbar-header {
background-color: #f8f8f8;
background-color: var(--fhc-nav-component-bg);
padding-top: 0px;
padding-bottom: 0px;
padding-left: 10px;
border-style: solid;
border-color: #e7e7e7;
border-color: var(--fhc-nav-component-border);
border-width: 0 0 1px;
}
@@ -15,7 +26,7 @@
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 18px;
line-height: 1.42857143;
color: #5e5e5e !important;
color: var(--fhc-nav-component-color) !important;
padding-top: 15px !important;
padding-right: 7px !important;
padding-left: 7px !important;
@@ -34,8 +45,8 @@
z-index: 1;
position: absolute;
width: 250px;
border-right: 1px solid #e7e7e7;
background-color: #f8f8f8;
border-right: 1px solid var(--fhc-nav-left-component-border);
background-color:var(--fhc-nav-left-component-bg);
}
.navbar-left-side ul {
@@ -43,7 +54,7 @@
}
.navbar-left-side ul li {
border-bottom: 1px solid #e7e7e7;
border-bottom: 1px solid var(--fhc-nav-left-component-border);
}
.left-side-menu-second-level {
@@ -67,7 +78,7 @@
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 14px;
line-height: 1.42857143;
color: #23527c;
color: var(--fhc-nav-left-component-color);
padding-top: 10px !important;
padding-bottom: 10px !important;
padding-left: 7px !important;
@@ -76,7 +87,7 @@
}
.left-side-menu-link-entry:hover {
background-color: #eee;
background-color: var(--fhc-nav-left-component-hover);
}
/*
@@ -88,4 +99,3 @@
margin: 0 0 0 250px;
padding: 30px 30px;
}
+19 -12
View File
@@ -1,13 +1,14 @@
:root{
--fhc-profil-border: var(--fhc-background-highlight, gray);
--fhc-profil-tabulator-shadow: var(--fhc-shadow-20, rgba(0,0,0,0.1));
}
.image-lock{
height:22px;
width:21px;
background-color:white;
background-color:var(--fhc-profil-border);
position:absolute;
/* top: 1px is important, otherwise it goes over the border of the thumbnail*/
top:1px;
right:12px;
display:flex;
@@ -16,9 +17,22 @@
border-radius:3px;
}
.image-upload {
height: 22px;
width: 21px;
background-color: var(--fhc-profil-border);
position: absolute;
bottom: 4.5px;
left: 14.5px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px 3px 3px 0;
}
.tabulator-collapsed-row{
padding:15px;
background-color: rgba(0,0,0,0.1);
background-color: var(--fhc-profil-tabulator-shadow);
}
@@ -30,9 +44,6 @@
padding:0 !important;
}
.btn-circle {
width: 30px;
height: 30px;
@@ -42,7 +53,3 @@
font-size: 12px;
line-height: 1.42857;
}
+14 -2
View File
@@ -1,9 +1,21 @@
:root {
--fhc-sprachen-color: var(--fhc-text, white);
--fhc-sprachen-background: var(--fhc-primary, #005585);
--fhc-sprachen-background-highlight: var(--fhc-primary-highlight, #00649c);
}
.sprachen-entry{
background-color: var(--fhc-cis-primary-hover);
color: var(--fhc-sprachen-color);
background-color: var(--fhc-sprachen-background);
}
.sprachen-entry:hover {
background-color: var(--fhc-sprachen-background);
}
[selected="true"].sprachen-entry {
background-color: var(--fhc-cis-primary);
background-color: var(--fhc-sprachen-background-highlight);
}
.sprachen-entry.btn {
+32 -238
View File
@@ -1,247 +1,41 @@
:root{
--fhc-calendar-pane-height: calc(100vh - 220px);
--fhc-calendar-past: #F5E9D7
}
@import './calendar/frame.css';
@import './calendar/events.css';
@import './calendar/backgrounds.css';
.fhc-calendar-week-page-header {
background-color: #e9ecef;
}
/* Themable Variables */
:root {
--fhc-calendar-gap: var(--bs-border-width, 1px);
.fhc-calendar-week-page-header > div:hover{
background-color: #dddfe2;
}
--fhc-calendar-bg: var(--fhc-background, #fff);
--fhc-calendar-border: var(--fhc-border, #dee2e6);
--fhc-calendar-bg-gap: var(--fhc-border, #dee2e6);
.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-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-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-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-month-page-weekday,
.fhc-calendar-month-page-day {
color: inherit;
}
--fhc-calendar-bg-header: var(--fhc-calendar-bg);
--fhc-calendar-bg-header-gap: transparent;
--fhc-calendar-bg-allday: var(--fhc-calendar-bg);
--fhc-calendar-bg-allday-gap: transparent;
--fhc-calendar-bg-body: var(--fhc-calendar-bg);
--fhc-calendar-bg-body-gap: transparent;
.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;
}
/* grid hour lines of the Stundenplan use box-shadow instead of border because box-shadow renders consistently on different viewports*/
.box-shadow-border-top{
box-shadow: 0 0 0 1px #dee2e6 !important;
}
.fhc-calendar-no-events-overlay{
position: relative;
}
.fhc-calendar-no-events-overlay::before {
content: "";
position: absolute;
top: 0;
left: 0;
margin:auto;
width: 100%;
height: 100%;
background-image: linear-gradient(120deg, white, gray );
opacity: .7;
}
.fhc-calendar-day-page {
/*aspect-ratio: 7/6;*/
min-height: 0;
}
.fhc-calendar-day-page > div {
/*transform: translate(-0.75em, -0.75em);*/
/*width: calc(100% + 1.5em);*/
height: calc(100% + 1.5em);
}
.fhc-calendar-day-page .flex-column > .flex-grow-1 {
padding-left: 3em;
}
.fhc-calendar-day-page .flex-column {
border-right: 1px solid #dee2e6;
}
.fhc-calendar-day-page .events {
display: grid;
grid-template-columns: 3em 1fr;
/*min-height: 266.6666666667%;*/
}
.fhc-calendar-day-page .events .day {
gap: 1px;
}
.fhc-calendar-day-page .events .hours, .fhc-calendar-day-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: 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:#37789c;
color:white;
--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-maxheight-allday: 7.125rem;
}
.fhc-calendar-past {
background-color: var(--fhc-calendar-past);
border-color: #E8E8E8;
opacity: 0.5;
}
.fhc-calendar-month-page-day-highlight {
/*background-color: #f5f5f5;*/
/*background-color: red;*/
}
.fhc-highlight-week {
/*border-color: black !important;*/
}
.fhc-highlight-day {
box-shadow: inset 0 0 0 2px black !important;
}
.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;
}
.selectedEvent {
background-color: #00649c !important;
color: white;
}
@@ -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;
}
+71
View File
@@ -0,0 +1,71 @@
.event {
cursor: pointer;
font-size: var(--fhc-calendar-fontsize-event, .875rem);
}
.fhc-calendar-mode-day .fhc-calendar-base-grid-line,
.fhc-calendar-mode-week .fhc-calendar-base-grid-line {
padding: 0 var(--fhc-calendar-gap-events, var(--fhc-calendar-gap, 1px));
}
.fhc-calendar-mode-day .all-day-events .event,
.fhc-calendar-mode-week .all-day-events .event {
margin: var(--fhc-calendar-gap-events, var(--fhc-calendar-gap, 1px)) 0;
}
.fhc-calendar-mode-list .event {
margin-bottom: var(--fhc-calendar-gap-events, var(--fhc-calendar-gap, 1px));
}
/* 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));
color: var(--event-color, var(--fhc-calendar-color-event, #000));
}
.fhc-calendar-base .event > *:hover {
filter: brightness(120%);
}
+219
View File
@@ -0,0 +1,219 @@
/* 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;
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-bg-gap);
grid-gap: var(--fhc-calendar-gap-header, calc(var(--fhc-calendar-gap, 1px) * 2));
}
.fhc-calendar-base-grid .grid-header {
background-color: var(--fhc-calendar-bg-header-gap, var(--fhc-calendar-bg-gap));
}
.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-bg-allday-gap, var(--fhc-calendar-bg-gap));
}
.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-allday .all-day-events {
max-height: var(--fhc-calendar-maxheight-allday);
overflow-y: auto;
}
.fhc-calendar-base-grid .grid-main {
grid-gap: var(--fhc-calendar-gap, 1px);
background-color: var(--fhc-calendar-bg-body-gap, var(--fhc-calendar-bg-gap));
}
.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: var(--fhc-calendar-gap-events, var(--fhc-calendar-gap, 1px));
}
.fhc-calendar-base-label-time {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: end;
}
/* Header */
.fhc-calendar-base-header .header-picker {
display: flex;
justify-content: center;
}
.fhc-calendar-base-header .header-actions {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
align-items: center;
margin-bottom: .5rem;
row-gap: .5rem;
}
.fhc-calendar-base-header .header-userdefined:empty {
display: none;
}
@media (min-width: 768px) {
.fhc-calendar-base-header {
display: grid;
grid-template-columns: [userdefined] 1fr [picker] max-content [modes] 1fr;
align-items: center;
}
.fhc-calendar-base-header .header-actions {
grid-column: 1 / -1;
grid-row: 1;
display: grid;
grid-template-columns: subgrid;
margin-bottom: 0;
}
.fhc-calendar-base-header .header-userdefined {
grid-column: userdefined;
}
.fhc-calendar-base-header .header-modes {
grid-column: modes;
}
.fhc-calendar-base-header .header-picker {
grid-column: picker;
grid-row: 1;
}
}
/* Modes */
/* ===== */
/* Day */
.fhc-calendar-mode-day .event-details {
padding: 1.5rem;
width: 100%;
border-left: solid 1px var(--fhc-calendar-bg-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.8;
grid-template-rows: 2em;
grid-auto-rows: 1.8em;
font-size: var(--fhc-calendar-fontsize-event, .875rem);
}
.fhc-calendar-mode-month .fhc-calendar-base-grid-line-event {
font-size: 1em;
height: 1.8em;
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;
}
+165 -4
View File
@@ -1,5 +1,43 @@
@import './calendar.css';
@import './dashboard/news.css';
@import './dashboard/LvPlan.css';
:root{
--fhc-dashboard-danger: var(--fhc-danger, #842029);
--fhc-dashboard-grid-size: 4;
--fhc-dashboard-link: var(--fhc-link, #0a57ca);
--fhc-dashboard-gridrow-background: var(--fhc-background, white);
--fhc-dashboard-gridrow-background-highlight: var(--fhc-background-highlight, #6c757d);
--fhc-dashboard-draggeditem-background: var(--fhc-background, white);
--fhc-dashboard-item-overlay-background: var(--fhc-background-highlight, rgba(0, 100, 156, 1));
--fhc-dashboard-item-overly-border-color: var(--fhc-border, lightgray);
--fhc-dashboard-denied-dragging-animation-color: var(--fhc-danger, red);
--fhc-dashboard-pin-pinned-hover-color: var(--fhc-fifth-highlight, rgb(153, 116, 116));
--fhc-dashboard-section-info-color: var(--fhc-primary, #00649c);
--fhc-dashboard-section-info-color-hover: var(--fhc-primary-highlight, #005585);
}
@media(max-width: 577px) {
:root {
--fhc-dashboard-grid-size: 1;
}
}
.core-dashboard a{
color: var(--fhc-dashboard-link);
}
@media (max-width: 576px){
.widget-icon {
max-height: 250px;
object-fit: cover;
}
.widget-icon-container{
max-width: 250px;
margin-left: auto;
margin-right: auto;
}
}
.empty-tile-hover {
height: 100%;
@@ -11,18 +49,59 @@
cursor:pointer;
}
.dashboard-section > .newGridRow{
position:absolute;
width:20px;
height:20px;
padding:0;
bottom:0;
left:50%;
transform:translate(-50%, 50%);
background-color: var(--fhc-dashboard-gridrow-background);
}
.newGridRow:hover {
color:white;
background-color: var(--fhc-dashboard-gridrow-background-highlight);
}
.empty-tile-hover:hover {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-500 -500 1448 1512"><path fill="rgb(211, 211, 211)" d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM200 344V280H136c-13.3 0-24-10.7-24-24s10.7-24 24-24h64V168c0-13.3 10.7-24 24-24s24 10.7 24 24v64h64c13.3 0 24 10.7 24 24s-10.7 24-24 24H248v64c0 13.3-10.7 24-24 24s-24-10.7-24-24z"/></svg>');
}
.alert-danger .form-check-input:checked {
border-color: #842029;
background-color: #842029;
border-color: var(--fhc-dashboard-danger);
background-color: var(--fhc-dashboard-danger);
}
:root {
--fhc-dashboard-grid-size: 4;
}
@media(max-width: 1400px) {
:root {
--fhc-dashboard-grid-size: 4;
}
}
@media(max-width: 1200px) {
:root {
--fhc-dashboard-grid-size: 3;
}
}
@media(max-width: 992px) {
:root {
--fhc-dashboard-grid-size: 2;
}
}
@media(max-width: 768px) {
:root {
--fhc-dashboard-grid-size: 2;
}
}
@media(max-width: 577px) {
:root {
--fhc-dashboard-grid-size: 1;
@@ -34,11 +113,93 @@
}
.cursor-nw-resize {
cursor: nw-resize;
cursor: nw-resize !important;
}
.cursor-ew-resize {
cursor: ew-resize !important;
}
.cursor-ns-resize {
cursor: ns-resize !important;
}
.no-drag-drop {
cursor: not-allowed !important;
}
.cursor-move {
cursor: move;
cursor: move !important;
}
.draggedItem {
height: 100%;
width: 100%;
background-color: var(--fhc-dashboard-draggeditem-background);
position: relative;
}
.dashboard-item-overlay{
background-color: var(--fhc-dashboard-item-overlay-background);
}
.dashboard-item-overlay::before{
position:absolute;
content:"";
top:0.25rem;
left:0.25rem;
right:0.25rem;
bottom:0.25rem;
border:4px dashed var(--fhc-dashboard-item-overly-border-color);
opacity: 0.5;
}
#deleteBookmark i{
color: var(--fhc-dashboard-danger);
}
.pin:hover{
cursor: pointer;
}
.pin[pinned]:hover{
color: var(--fhc-dashboard-pin-pinned-hover-color);
}
.section-info{
color: var(--fhc-dashboard-section-info-color);
cursor:pointer;
}
.section-info:hover {
color: var(--fhc-dashboard-section-info-color-hover);
}
.denied-dragging-animation {
animation: wiggle 0.5s linear;
color: var(--fhc-dashboard-denied-dragging-animation-color) !important;
}
@keyframes wiggle {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(-30deg);
}
50% {
transform: rotate(0deg);
}
75% {
transform: rotate(30deg);
}
100% {
transform: rotate(0em);
}
}
@@ -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;
}
+96 -43
View File
@@ -1,5 +1,10 @@
:root{
--news-widget-height: 1;
--fhc-news-widget-height: 1;
--fhc-news-widget-bg: var(--fhc-primary);
--fhc-news-widget-highlight-color: var(--fhc-primary-highlight);
--fhc-news-widget-border: var(--fhc-border);
--fhc-news-widget-border-highlight: var(--fhc-border-highlight);
--fhc-news-widget-text-light: var(--fhc-light);
}
@@ -8,28 +13,68 @@
@media (min-width :576px) {
.fhc-news-menu-item {
/* padding: top right bottom left */
padding:0em 0em 0em 0em !important;
padding:0.125em 0.125em 0.125em 0.125em !important;
max-height:30px !important;
}
.fhc-news-menu-item-betreff p {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
line-clamp: 1;
/* Number of lines to show */
overflow: hidden;
}
}
/* media query for md sizes */
@media (min-width :768px) {
.fhc-news-menu-item {
/* padding: top right bottom left */
padding: 0.3em 0.3em 0.3em 0.3em !important;
padding: 0.25em 0.25em 0.25em 0.25em !important;
max-height: 50px !important;
}
.fhc-news-menu-item-betreff p {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
line-clamp: 1;
/* Number of lines to show */
overflow: hidden;
}
}
/* media query for lg sizes */
@media (min-width :992px) {
.fhc-news-menu-item {
/* padding: top right bottom left */
padding: 0.3em 0.3em 0.3em 0.3em !important;
padding: 0.25em 0.25em 0.25em 0.25em !important;
max-height: 75px !important;
}
.fhc-news-menu-item-betreff p {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
/* Number of lines to show */
overflow: hidden;
}
}
/* media query for xl sizes */
@media (min-width :1200px) {
.fhc-news-menu-item {
/* padding: top right bottom left */
padding: 1em 1em 0.3em 1em !important;
padding: 1em 0.25em 0.25em 0.25em !important;
height: 100px !important;
}
.fhc-news-menu-item-betreff p {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
/* Number of lines to show */
overflow: hidden;
}
}
@@ -37,42 +82,51 @@
@media (min-width :1400px) {
.fhc-news-menu-item {
/* padding: top right bottom left */
padding: 1.2em 1.2em 0.3em 1.2em !important;
padding: 1.2em 0.5em 0.5em 0.5em !important;
height: 125px !important;
}
.fhc-news-menu-item-betreff p {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
/* Number of lines to show */
overflow: hidden;
}
}
.widgets-news{
padding: .5rem;
overflow-y: auto;
}
.widgets-news img
{
max-width: 100%;
max-width: 100%;
height: auto;
}
.fhc-news-menu-item {
padding: 0.375rem;
color: var(--fhc-cis-menu-lvl-1-color);
min-height: 5%;
max-height: 30%;
color: var(--fhc-news-widget-text-light);
width: 100%;
justify-content: space-between;
align-items: center;
background-color: #00649c;
border: 1px solid #f1f1f1;
background-color: var(--fhc-news-widget-bg);
border: 1px solid var(--fhc-news-widget-border);
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease; /* Smooth transition */
}
.fhc-news-menu-item:hover {
background-color: var(--fhc-cis-menu-lvl-1-bg-hover);
border-color: #f1f1f1;
}
.fhc-news-menu-item:active {
background-color: var(--fhc-cis-menu-lvl-1-color-hover);
border-color: #f1f1f1;
background-color: var(--fhc-news-widget-highlight-color);
border-color: var(--fhc-news-widget-border-highlight);
}
.fhc-news-menu-item.selected {
background-color: var(--fhc-cis-menu-lvl-1-bg-hover);
border-right: 2px solid #fff;
background-color: var(--fhc-news-widget-highlight-color);
border: 2px solid var(--fhc-news-widget-border-highlight);
}
@@ -83,7 +137,6 @@
.fhc-news-menu-item-betreff
{
width: 100%;
text-align: center;
max-height: 100%;
height: 100%;
}
@@ -111,32 +164,32 @@
.carousel-control-prev,
.carousel-control-next {
opacity: 0;
transition: opacity 0.3s ease;
top: 0.5rem !important;
bottom: auto !important;
opacity: 1 !important;
}
/* Show the buttons when the carousel is hovered */
.carousel:hover .carousel-control-prev,
.carousel:hover .carousel-control-next {
opacity: 1;
.fhc-news-items-lg .row .news-truncate span{
color: var(--fhc-primary);
cursor: pointer;
}
.fhc-news-text{
font-size: 0.75em;
.fhc-news-items-lg .row:hover .news-truncate span{
text-decoration: underline;
cursor: pointer;
font-weight: bold;
}
.fhc-news-xs {
font-size: 0.6em;
.news-truncate {
display: -webkit-box;
-webkit-line-clamp: 2;
/* number of lines to show */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
.fhc-news-sm {
font-size: 0.7em;
}
.fhc-news-md {
font-size: 1.1em;
}
.fhc-news-lg {
font-size: 1.4em;
}
+124 -9
View File
@@ -5,28 +5,73 @@
@import url(./switches.css);
#search-filter{
border: 1px solid #ced4da;
:root{
--fhc-searchbar-shadow: var(--fhc-shadow-40, #ced4da);
--fhc-searchbar-settings-background-color: var(--fhc-dark, #343a40);
--fhc-searchbar-results-background-color: var(--fhc-background, #fff);
--fhc-searchbar-results-border-color: var(--fhc-border, lightgrey);
--fhc-searchbar-results-link-color: var(--fhc-link, #0d6efd);
--fhc-searchbar-results-btn-color: var(--bs-btn-color, white);
}
.searchbar_input_clear {
/* 1rem * 5 / 8 = width of icon */
margin-inline-start: calc(-1rem * 5 / 8 - var(--bs-btn-padding-x) * 2 - var(--bs-border-width)) !important;
border: 0;
margin-inline-end: 1px;
z-index: 1;
}
.searchbar_input_clear:hover,
.searchbar_input_clear:focus {
color: var(--bs-body-color);
background-color: transparent;
}
.searchbar_input:focus,
.searchbar_input:focus ~ .searchbar_input_clear {
z-index: 5; /* same as bootstrap .input-group > .form-control:focus */
}
.searchbar_setting_btn {
--bs-btn-color: var(--bs-body-color);
--bs-btn-bg: var(--bs-tertiary-bg);
--bs-btn-border-color: var(--bs-border-color);
}
.searchbar_searchbox.open {
z-index: 10000;
}
.searchbar_settings {
position: absolute;
position: absolute;
z-index: 9999;
background-color: #343a40;
background-color: var(--fhc-searchbar-settings-background-color);
}
.searchbar_results {
position: absolute;
z-index: 9998;
background-color: #fff;
border: 1px solid lightgrey;
background-color: var(--fhc-searchbar-results-background-color);
border: 1px solid var(--fhc-searchbar-results-border-color);
padding: .5rem;
top: 100%;
width: 100%;
left: 0;
right: 0;
box-shadow: 2px 2px 4px rgba(0, 0, 0, .4);
box-shadow: 2px 2px 4px var(--fhc-searchbar-shadow);
}
.searchbar_results a{
color: var(--fhc-searchbar-results-link-color);
}
/*
.searchbar_results a:not(a~i) {
color: inherit;
}
*/
.searchbar_results a.btn {
color: var(--fhc-searchbar-results-btn-color);
}
.searchbar_results_scroller {
@@ -35,7 +80,7 @@
}
.searchbar_result {
border-bottom: 1px solid lightgrey;
border-bottom: 1px solid var(--fhc-searchbar-results-border-color);
margin-bottom: 1rem;
}
@@ -104,6 +149,76 @@
list-style: none;
}
/* new variant with template/frame */
.searchbar-result-template-frame {
border-bottom: 1px solid lightgrey;
margin-bottom: 1rem;
padding-bottom: 1rem;
}
.searchbar-result-template-actions {
display: flex;
flex-wrap: wrap;
gap: .5rem;
padding: 0;
margin: 1rem 0 0;
}
.searchbar-square-image {
position: relative;
display: block;
height: 0;
padding-bottom: 100%;
}
.searchbar-square-image > * {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
.searchbar-square-image > img {
object-fit: cover;
}
.searchbar-square-image > div {
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
color: rgba(var(--bs-white-rgb),1);
background-color: rgba(var(--bs-primary-rgb), 1);
}
.searchbar-square-image.rounded-circle > img,
.searchbar-square-image.rounded-circle > div {
border-radius: 50%;
}
.searchbar-rounded-image {
display: block;
width: 100%;
}
.searchbar-rounded-image > img {
width: 100%;
}
.searchbar-rounded-image > div {
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
color: rgba(var(--bs-white-rgb),1);
background-color: rgba(var(--bs-primary-rgb), 1);
height: calc((100px - .75em) * 1.3);
}
.searchbar-rounded-image > img,
.searchbar-rounded-image > div {
border-radius: .25rem;
}
.no-margin-paragraphs p {
margin: 0;
}
.non-selectable {
user-select: none;
-webkit-user-select: none; /* Safari */
@@ -113,4 +228,4 @@
.searchbar_inaktiv {
opacity: .6;
}
}
+12 -11
View File
@@ -1,23 +1,24 @@
:root{
--fhc-switches-checked-bg: var(--fhc-light, white);
--fhc-switches-unchecked-bg: var(--fhc-disabled, var(--bs-gray-600));
--fhc-switches-shadow: var(--fhc-shadow-20, rgba(108, 117, 125,.25));
}
.fhc-switches:checked {
background-color: white !important;
border-color: white !important;
background-color: var(--fhc-switches-checked-bg) !important;
border-color: transparent;
background-position: right center !important;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23000000'/%3e%3c/svg%3e") !important;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='black'/%3e%3c/svg%3e") !important;
}
.fhc-switches {
background-color: var(--bs-gray-600) !important;
border-color: var(--bs-gray-600) !important;
background-color: var(--fhc-switches-unchecked-bg) !important;
border-color: transparent;
background-position: left center !important;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%238e99a3'/%3e%3c/svg%3e") !important;
}
.fhc-switches:focus {
box-shadow: 0 0 0 .25rem rgba(108, 117, 125,.25);
box-shadow: 0 0 0 .25rem var(--fhc-switches-shadow);
}
/*.form-switch .form-check-input:checked {
rgb(52, 58, 64)*/
+3 -3
View File
@@ -3,9 +3,9 @@
Author : bambi
*/
:root {
--fhc-verticalsplit-vsplitter-bg-color: #eee;
--fhc-verticalsplit-vsplitter-border-color: #eee;
--fhc-verticalsplit-vsplitter-splitactions-color: #000;
--fhc-verticalsplit-vsplitter-bg-color: var(--fhc-background, #eee);
--fhc-verticalsplit-vsplitter-border-color: var(--fhc-border, #eee);
--fhc-verticalsplit-vsplitter-splitactions-color: var(--fhc-dark, #000);
}
.verticalsplitted {
+9 -1
View File
@@ -43,4 +43,12 @@
--dp-border-color-hover: var(--bs-success);
}
.form-control.is-invalid ~ .dp__clear_icon,
.was-validated .form-control:invalid ~ .dp__clear_icon { margin-right: calc(1.5em + .75rem - 12px) }
.was-validated .form-control:invalid ~ .dp__clear_icon {
margin-right: calc(1.5em + .75rem - 12px);
}
.p-component .dp__input_icons {
box-sizing: content-box !important;
}
.input-group .dp__input_icons {
z-index: 3;
}
-40
View File
@@ -24,46 +24,6 @@
border-color: #DEE2E6 !important;
}
.bg-info-subtle{
background-color: #D9EDF7 !important;
color: #31708F !important;
border-style: solid !important;
border-width: 1px !important;
border-color: #BCE8F1 !important;
}
.bg-warning-subtle{
background-color: #FFF3CD !important;
color: #664D03 !important;
border-style: solid !important;
border-width: 1px !important;
border-color: #FFE69C !important;
}
.bg-success-subtle{
background-color: #D1E7DD !important;
color: #0A3622 !important;
border-style: solid !important;
border-width: 1px !important;
border-color: #A3CFBB !important;
}
.bg-danger-subtle{
background-color: #F8D7DA !important;
color: #672B31 !important;
border-style: solid !important;
border-width: 1px !important;
border-color: #F1AEB5 !important;
}
.bg-primary-subtle{
background-color: #CFE2FF !important;
color: #052C65 !important;
border-style: solid !important;
border-width: 1px !important;
border-color: #9EC5FE !important;
}
/* html attribute disabled and title don't work together, sets the pointer-event to auto in order to see the title toolip */
.btn:disabled {
+288
View File
@@ -0,0 +1,288 @@
/* css overrides for static cms content */
@import './default_cms_fixes.css';
/* colors */
:root {
--fhc-black-10: black;
--fhc-black-20: #1d2024;
--fhc-black-30: #212529;
--fhc-black-40: #292b2a;
--fhc-black-50: #2B3035;
--fhc-black-60: #2c3136;
--fhc-black-70: #343a40;
--fhc-white-10: #FFFFFF;
--fhc-white-20: #f6f6f6;
--fhc-white-30: #eceff1;
--fhc-white-40: #e9ecef;
--fhc-white-50: #dee2e6;
--fhc-white-60: #d5dae0;
--fhc-white-70: #ced4da;
--fhc-white-80: #c7cbcf;
--fhc-white-90: #aeb3b8;
--fhc-gray-10: #858f97;
--fhc-gray-20: #72777A;
--fhc-gray-30: #616568;
--fhc-gray-40: #495057;
--fhc-blue-10: #0a57ca;
--fhc-blue-20: #79b7d8;
/* --fhc-blue-20: #00649c;*/
--fhc-blue-30: #00598d;
/*--fhc-blue-40: #005585;*/
--fhc-blue-40: #61aed8;
--fhc-blue-50: #00507e;
--fhc-blue-primary: #006095;
--fhc-blue-primary-highlight: #0086CB;
--fhc-red-10: #842029;
--fhc-red-20: #800000;
--fhc-green-10: #008000;
--fhc-beige-10: rgba(245, 233, 215, 0.5);
--fhc-beige-20: rgba(172, 153, 125, 0.5);
}
:root.light {
/* fhc base colors */
--fhc-primary: var(--fhc-blue-primary);
--fhc-primary-highlight: var(--fhc-blue-primary-highlight);
--fhc-secondary: var(--fhc-white-20);
--fhc-secondary-highlight: var(--fhc-white-40);
--fhc-tertiary: var(--fhc-white-40);
--fhc-tertiary-highlight: var(--fhc-white-60);
--fhc-fourth: var(--fhc-white-60);
--fhc-fourth-highlight: var(--fhc-white-80);
--fhc-fifth: var(--fhc-white-80);
--fhc-fifth-highlight: var(--fhc-white-90);
/*background*/
--fhc-background: var(--fhc-white-10);
--fhc-background-highlight: var(--fhc-white-60);
/*text*/
--fhc-text: var(--fhc-black-30);
/* shadows and borders */
--fhc-shadow-20: rgba(0, 0, 0, 0.2);
--fhc-shadow-40: rgba(0, 0, 0, 0.4);
--fhc-shadow-60: rgba(0, 0, 0, 0.6);
--fhc-shadow-80: rgba(0, 0, 0, 0.8);
--fhc-box-shadow: var(--fhc-white-50);
--fhc-border: var(--fhc-white-50);
--fhc-border-highlight: var(--fhc-gray-40);
/* links */
--fhc-link: var(--fhc-blue-10);
/* disabled */
--fhc-disabled: var(--fhc-gray-10);
/* status colors */
--fhc-danger: var(--fhc-red-10);
--fhc-success: var(--fhc-green-10);
--fhc-dark: var(--fhc-black-30);
--fhc-light: var(--fhc-white-50);
--fhc-cis-menu-lvl-1-color: var(--fhc-white-50);
}
:root.dark {
/* fhc base colors */
--fhc-primary: var(--fhc-blue-primary);
--fhc-primary-highlight: var(--fhc-blue-primary-highlight);
--fhc-secondary: var(--fhc-black-50);
--fhc-secondary-highlight: var(--fhc-black-70);
--fhc-tertiary: var(--fhc-black-70);
--fhc-tertiary-highlight: var(--fhc-gray-40);
--fhc-danger: var(--fhc-red-10);
/*background*/
--fhc-background: var(--fhc-black-30);
--fhc-background-highlight: var(--fhc-black-20);
/*text*/
--fhc-text: var(--fhc-white-50);
/* shadows and borders */
--fhc-shadow-20: rgba(125, 125, 125, 0.2);
--fhc-shadow-40: rgba(125, 125, 125, 0.4);
--fhc-shadow-60: rgba(125, 125, 125, 0.6);
--fhc-shadow-80: rgba(125, 125, 125, 0.8);
--fhc-box-shadow: var(--fhc-gray-40);
--fhc-border: var(--fhc-gray-40);
--fhc-border-highlight: var(--fhc-white-90);
/* links */
--fhc-link: var(--fhc-white-70);
/* disabled */
--fhc-disabled: var(--fhc-gray-10);
/* status colors */
--fhc-danger: var(--fhc-red-10);
--fhc-success: var(--fhc-green-10);
--fhc-dark: var(--fhc-black-50);
--fhc-light: var(--fhc-white-70);
}
:root.purple {
--fhc-purple-primary: rgb(150, 120, 255);
--fhc-purple-primary-highlight: rgb(170, 140, 255);
--fhc-purple-5: rgb(245, 240, 255);
--fhc-purple-10: rgb(235, 225, 255);
--fhc-purple-20: rgb(220, 200, 255);
--fhc-purple-30: rgb(200, 170, 255);
--fhc-purple-40: rgb(180, 140, 240);
--fhc-purple-50: rgb(160, 110, 225);
--fhc-purple-60: rgb(140, 90, 210);
--fhc-purple-70: rgb(120, 70, 195);
--fhc-purple-80: rgb(100, 50, 180);
--fhc-purple-90: rgb(80, 30, 165);
/* fhc base colors */
--fhc-primary: var(--fhc-purple-primary-highlight);
--fhc-primary-highlight: var(--fhc-purple-primary);
--fhc-secondary: var(--fhc-purple-20);
--fhc-secondary-highlight: var(--fhc-purple-40);
--fhc-tertiary: var(--fhc-purple-30);
--fhc-tertiary-highlight: var(--fhc-purple-50);
/* background */
--fhc-background: var(--fhc-purple-5);
--fhc-background-highlight: var(--fhc-purple-10);
/* text */
--fhc-text: var(--fhc-black-20);
/* shadows and borders */
--fhc-shadow-20: rgba(150, 100, 200, 0.2);
--fhc-shadow-40: rgba(150, 100, 200, 0.4);
--fhc-shadow-60: rgba(150, 100, 200, 0.6);
--fhc-shadow-80: rgba(150, 100, 200, 0.8);
--fhc-box-shadow: var(--fhc-white-50);
--fhc-border: var(--fhc-white-50);
--fhc-border-highlight: var(--fhc-gray-40);
/* links */
--fhc-link: var(--fhc-black-50);
/* disabled */
--fhc-disabled: var(--fhc-purple-20);
/* status colors */
--fhc-danger: var(--fhc-red-10);
--fhc-success: var(--fhc-green-10);
--fhc-dark: var(--fhc-black-20);
--fhc-light: var(--fhc-white-70);
}
/* fhc primary*/
html.dark .bg-primary,
html.dark .btn-primary,
html.dark .btn-outline-primary {
background-color: var(--fhc-primary) !important;
border-color: var(--fhc-primary) !important;
color: white;
}
html.dark .btn-outline-primary:hover,
html.dark .btn-primary:hover {
filter: brightness(90%);
}
html.dark .text-primary {
color: var(--fhc-primary) !important;
}
html.dark .btn-outline-primary,
html.dark .border-primary {
border-color: var(--fhc-primary) !important;
}
/* fhc danger*/
html.dark .bg-danger,
html.dark .btn-danger,
html.dark .btn-outline-danger {
background-color: var(--fhc-danger) !important;
border-color: var(--fhc-danger) !important;
color: white;
}
html.dark .btn-outline-danger:hover,
html.dark .btn-danger:hover {
filter: brightness(90%);
}
html.dark .text-danger {
color: var(--fhc-danger) !important;
}
html.dark .border-danger {
border-color: var(--fhc-danger) !important;
}
html.dark .tabulator,
html.dark .tabulator .tabulator-header,
html.dark .tabulator .tabulator-header .tabulator-col,
html.dark .tabulator .tabulator-table,
html.dark .tabulator .tabulator-row {
background-color: var(--fhc-tertiary) !important;
color: var(--fhc-text) !important;
border-color: var(--fhc-border) !important;
}
html.dark .tabulator .tabulator-cell {
background-color: var(--fhc-tertiary) !important;
border-color: var(--fhc-border) !important;
}
html.dark .tabulator .tabulator-header .tabulator-col:hover {
background-color: var(--fhc-tertiary) !important;
}
.p-paginator{
background-color: var(--fhc-background) !important;
}
.p-paginator .p-paginator-pages .p-paginator-page.p-highlight,
.p-dropdown-panel .p-dropdown-items .p-dropdown-item.p-highlight {
background-color: var(--fhc-primary) !important;
border-color: var(--fhc-primary) !important;
color: var(--fhc-light) !important;
}
.p-paginator .p-paginator-pages .p-paginator-page,
.p-paginator .p-paginator-next,
.p-paginator .p-paginator-prev,
.p-paginator .p-paginator-first,
.p-paginator .p-paginator-last {
color: var(--fhc-primary) !important;
}
.p-link:focus {
box-shadow: 0 0 0 0.2rem rgba(var(--fhc-primary), 0.5) !important;
}
+14
View File
@@ -0,0 +1,14 @@
/* css overrides for dark mode */
html.dark #fhc-cms-content *[style*="color"] {
color: var(--fhc-text) !important;
}
/* fhc cms classes */
html.dark #fhc-cms-content a {
color: var(--fhc-link);
}
html.dark #fhc-cms-content img {
background-color: var(--fhc-text);
color: var(--fhc-background);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+2 -4
View File
@@ -95,10 +95,9 @@ var FHC_DialogLib = {
*/
_displayDialog: function(heading, message, icon, colorClass)
{
var html = "<p class='dialogmessage'><i class='glyphicon glyphicon-"+icon+"'></i>&nbsp;&nbsp;"+message+"</p>";
var html = "<p class='dialogmessage'>"+message+"</p>";
FHC_DialogLib.alertDefault(heading, html);
$(".ui-dialog-titlebar").addClass(colorClass+" text-center");
$(".glyphicon-"+icon).addClass(colorClass);
$(".ui-dialog-titlebar").addClass(colorClass+" alert text-center");
FHC_DialogLib._formatShortDialog();
},
@@ -113,7 +112,6 @@ var FHC_DialogLib = {
$(".ui-dialog-buttonpane.ui-widget-content").css("padding", ".3em .4em .5em .4em");
$(".ui-dialog .ui-dialog-content").css("padding", "0");
$(".ui-dialog-buttonset button").css("margin", "0");
$(".dialogmessage i.glyphicon").css("background-color", "transparent");
}
};
+6 -1
View File
@@ -6,5 +6,10 @@ export default {
{ }
);
},
getAuthInfo() {
return this.$fhcApi.get(
'/api/frontend/v1/AuthInfo/getAuthInfo',
{}
);
},
};
+6
View File
@@ -21,5 +21,11 @@ export default {
method: 'get',
url: '/api/frontend/v1/AuthInfo/getAuthUID'
};
},
getAuthInfo() {
return {
method: 'get',
url: '/api/frontend/v1/AuthInfo/getAuthInfo'
};
}
};
+25
View File
@@ -0,0 +1,25 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getAll() {
return {
method: 'get',
url: '/api/frontend/v1/language/get'
};
}
};
@@ -18,49 +18,79 @@
export default {
getRoomInfo(ort_kurzbz, start_date, end_date) {
return {
method: 'get',
url: '/api/frontend/v1/Stundenplan/getRoomplan',
method: 'post',
url: '/api/frontend/v1/LvPlan/getRoomplan',
params: { ort_kurzbz, start_date, end_date }
};
},
getStundenplan(start_date, end_date, lv_id) {
getLvPlan(start_date, end_date, lv_id) {
return {
method: 'get',
url: '/api/frontend/v1/Stundenplan/getStundenplan',
url: '/api/frontend/v1/LvPlan/getLvPlan',
params: { start_date, end_date, lv_id }
};
},
eventsPersonal(start_date, end_date) {
return {
method: 'post',
url: '/api/frontend/v1/lvPlan/eventsPersonal',
params: { start_date, end_date }
};
},
eventsLv(lv_id, start_date, end_date) {
return {
method: 'post',
url: '/api/frontend/v1/lvPlan/eventsLv',
params: { lv_id, start_date, end_date }
};
},
getStunden() {
// TODO(chris): seems to be called from nowhere?
return {
method: 'get',
url: '/api/frontend/v1/Stundenplan/Stunden'
url: '/api/frontend/v1/LvPlan/Stunden'
};
},
getOrtReservierungen(ort_kurzbz, start_date, end_date) {
return {
method: 'get',
url: `/api/frontend/v1/Stundenplan/Reservierungen/${ort_kurzbz}`,
method: 'post',
url: `/api/frontend/v1/LvPlan/getReservierungen/${ort_kurzbz}`,
params: { start_date, end_date }
};
},
getStundenplanReservierungen(start_date, end_date) {
getLvPlanReservierungen(start_date, end_date) {
return {
method: 'get',
url: '/api/frontend/v1/Stundenplan/Reservierungen',
method: 'post',
url: '/api/frontend/v1/LvPlan/getReservierungen',
params: { start_date, end_date }
};
},
getLehreinheitStudiensemester(lehreinheit_id) {
return {
method: 'get',
url: `/api/frontend/v1/Stundenplan/getLehreinheitStudiensemester/${lehreinheit_id}`
url: `/api/frontend/v1/LvPlan/getLehreinheitStudiensemester/${lehreinheit_id}`
};
},
studiensemesterDateInterval(date) {
return {
method: 'get',
url: `/api/frontend/v1/Stundenplan/studiensemesterDateInterval/${date}`
url: `/api/frontend/v1/LvPlan/studiensemesterDateInterval/${date}`
};
},
LvPlanEvents(start_date, end_date, lv_id) {
return {
method: 'post',
url: '/api/frontend/v1/LvPlan/LvPlanEvents',
params: {
start_date: start_date,
end_date: end_date,
lv_id: lv_id
}
};
},
getLv(lehrveranstaltung_id) {
return {
method: 'get',
url: '/api/frontend/v1/LvPlan/getLv/' + lehrveranstaltung_id
};
}
};
+8 -3
View File
@@ -16,10 +16,15 @@
*/
export default {
getView(uid) {
profilViewData(uid) {
let url = "/api/frontend/v1/Profil/profilViewData";
if(uid){
url += `/${uid}`;
}
return {
method: 'get',
url: `/api/frontend/v1/Profil/getView/${uid}`
url: url
};
},
fotoSperre(value) {
@@ -69,5 +74,5 @@ export default {
method: 'get',
url: '/api/frontend/v1/Profil/getAllNationen'
};
}
},
};
+7
View File
@@ -61,6 +61,13 @@ export default {
params: dms
};
},
updateProfilbild(dms) {
return {
method: 'post',
url: `/api/frontend/v1/ProfilUpdate/updateProfilbild`,
params: dms
};
},
getProfilRequestFiles(requestID) {
return {
method: 'get',
+12
View File
@@ -0,0 +1,12 @@
export default {
loadRenderers() {
return {
method: 'get',
url: '/api/frontend/v1/RendererLoader/GetRenderers',
params: {
}
};
},
}
+14
View File
@@ -22,5 +22,19 @@ export default {
url: '/api/frontend/v1/searchbar/search',
params
};
},
searchCis(params) {
return {
method: 'post',
url: '/api/frontend/v1/searchbar/searchCis',
params
};
},
searchStv(params) {
return {
method: 'post',
url: '/api/frontend/v1/searchbar/searchStv',
params
};
}
};
-1
View File
@@ -1,6 +1,5 @@
export default {
getArchiv(person_id) {
console.log(person_id);
return {
method: 'get',
url: 'api/frontend/v1/stv/archiv/getArchiv',
+10 -4
View File
@@ -16,16 +16,22 @@
*/
export default {
get(prestudent_id) {
get(prestudent_id, studiensemester_kurzbz) {
return {
method: 'post',
url: 'api/frontend/v1/stv/student/get/' + prestudent_id
url: 'api/frontend/v1/stv/student/get/'
+ encodeURIComponent(prestudent_id)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz)
};
},
save(prestudent_id, params) {
save(prestudent_id, studiensemester_kurzbz, params) {
return {
method: 'post',
url: 'api/frontend/v1/stv/student/save/' + prestudent_id,
url: 'api/frontend/v1/stv/student/save/'
+ encodeURIComponent(prestudent_id)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz),
params
};
},
+24 -11
View File
@@ -22,25 +22,34 @@ export default {
url: 'api/frontend/v1/stv/grades/list'
};
},
getCertificate(prestudent_id, all) {
all = all ? '/all' : '';
getCertificate(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/grades/getCertificate/' + encodeURIComponent(prestudent_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return {
method: 'get',
url: 'api/frontend/v1/stv/grades/getCertificate/' + prestudent_id + all
url: url
};
},
getTeacherProposal(prestudent_id, all) {
all = all ? '/all' : '';
getTeacherProposal(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/grades/getTeacherProposal/' + encodeURIComponent(prestudent_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return {
method: 'get',
url: 'api/frontend/v1/stv/grades/getTeacherProposal/' + prestudent_id + all
url: url
};
},
getRepeaterGrades(prestudent_id, all) {
all = all ? '/all' : '';
getRepeaterGrades(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/grades/getRepeaterGrades/' + encodeURIComponent(prestudent_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return {
method: 'get',
url: 'api/frontend/v1/stv/grades/getRepeaterGrades/' + prestudent_id + all
url: url
};
},
updateCertificate({lehrveranstaltung_id, student_uid, studiensemester_kurzbz, note, lehrveranstaltung_bezeichnung}) {
@@ -86,11 +95,15 @@ export default {
}
};
},
getGradeFromPoints(points, lehrveranstaltung_id) {
getGradeFromPoints(points, lehrveranstaltung_id, studiensemester_kurzbz) {
return {
method: 'post',
url: 'api/frontend/v1/stv/grades/getGradeFromPoints',
params: { points, lehrveranstaltung_id }
params: {
"points": points,
"lehrveranstaltung_id": lehrveranstaltung_id,
"studiensemester_kurzbz": studiensemester_kurzbz
}
};
}
};
+5 -2
View File
@@ -17,10 +17,13 @@
export default {
//------------- Prestudent.js------------------------------------------------------
get(prestudent_id) {
get(prestudent_id, studiensemester_kurzbz) {
return {
method: 'post',
url: 'api/frontend/v1/stv/prestudent/get/' + prestudent_id
url: 'api/frontend/v1/stv/prestudent/get/'
+ encodeURIComponent(prestudent_id)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz)
};
},
updatePrestudent(prestudent_id, params) {
+18 -6
View File
@@ -16,22 +16,34 @@
*/
export default {
uid(uid) {
uid(uid, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/students/'
+ encodeURIComponent(studiensemester_kurzbz)
+ '/uid/'
+ encodeURIComponent(uid);
return {
method: 'get',
url: 'api/frontend/v1/stv/students/uid/' + uid
url: url
};
},
prestudent(prestudent_id) {
prestudent(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/students/'
+ encodeURIComponent(studiensemester_kurzbz)
+ '/prestudent/'
+ encodeURIComponent(prestudent_id);
return {
method: 'get',
url: 'api/frontend/v1/stv/students/prestudent/' + prestudent_id
url: url
};
},
person(person_id) {
person(person_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/students/'
+ encodeURIComponent(studiensemester_kurzbz)
+ '/person/'
+ encodeURIComponent(person_id);
return {
method: 'get',
url: 'api/frontend/v1/stv/students/person/' + person_id
url: url
};
},
verband(relative_path) {
+32
View File
@@ -0,0 +1,32 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getVorlagen() {
return {
method: 'get',
url: 'api/frontend/v1/vorlagen/vorlagen/getVorlagen/'
};
},
getVorlagenByLoggedInUser() {
return {
method: 'get',
url: 'api/frontend/v1/vorlagen/vorlagen/getVorlagenByLoggedInUser/'
};
},
};
+6 -2
View File
@@ -22,7 +22,7 @@ import filter from "./filter.js";
import studstatus from "./studstatus.js";
import profil from "./profil.js";
import profilUpdate from "./profilUpdate.js";
import stundenplan from "./stundenplan.js";
import lvPlan from "./lvPlan.js";
import bookmark from "./bookmark.js";
import stv from "./stv.js";
import notiz from "./notiz.js";
@@ -39,6 +39,8 @@ import studiengang from "./studiengang.js";
import menu from "./menu.js";
import dashboard from "./dashboard.js";
import authinfo from "./authinfo.js";
import studium from "./studium.js";
import language from "./language.js";
export default {
search,
@@ -49,7 +51,7 @@ export default {
studstatus,
profil,
profilUpdate,
stundenplan,
lvPlan,
bookmark,
stv,
notiz,
@@ -66,4 +68,6 @@ export default {
studiengang,
menu,
authinfo,
studium,
language
};
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright (C) 2024 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getAll() {
return this.$fhcApi.get('/api/frontend/v1/language/get');
}
};
+75
View File
@@ -18,5 +18,80 @@ export default {
`/api/frontend/v1/Lehre/Pruefungen/${lehrveranstaltung_id}`
, {}
);
},
getStudentProjektarbeiten(uid) {
return this.$fhcApi.get(
`/api/frontend/v1/Lehre/getStudentProjektarbeiten/${uid}`
, {}
);
},
getStudentProjektabgaben(detail) {
return this.$fhcApi.get(
`/api/frontend/v1/Lehre/getStudentProjektabgaben`
, {
projektarbeit_id: detail.projektarbeit_id,
student_uid: detail.student_uid
}
);
},
postStudentProjektarbeitEndupload(formData) {
const url = '/api/frontend/v1/Lehre/postStudentProjektarbeitEndupload';
const headers = {Headers: { "Content-Type": "multipart/form-data" }}
return this.$fhcApi.post(url, formData, headers)
},
postStudentProjektarbeitZwischenabgabe(formData) {
const url = '/api/frontend/v1/Lehre/postStudentProjektarbeitZwischenabgabe';
const headers = {Headers: { "Content-Type": "multipart/form-data" }}
return this.$fhcApi.post(url, formData, headers)
},
getStudentProjektarbeitAbgabeFile(paabgabe_id, student_uid) {
const url = `/Cis/Abgabetool/getStudentProjektarbeitAbgabeFile?paabgabe_id=${paabgabe_id}&student_uid=${student_uid}`;
window.location = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url
},
getMitarbeiterProjektarbeiten(uid, all) {
return this.$fhcApi.get(
`/api/frontend/v1/Lehre/getMitarbeiterProjektarbeiten?showall=${all}`
, {}
);
},
postProjektarbeitAbgabe(termin) {
const payload = {
paabgabe_id: termin.paabgabe_id,
paabgabetyp_kurzbz: termin.bezeichnung.paabgabetyp_kurzbz,
datum: termin.datum,
fixtermin: termin.fixtermin,
insertvon: termin.insertvon,
kurzbz: termin.kurzbz,
projektarbeit_id: termin.projektarbeit_id
}
const url = '/api/frontend/v1/Lehre/postProjektarbeitAbgabe';
return this.$fhcApi.post(url, payload, null)
},
deleteProjektarbeitAbgabe(paabgabe_id) {
const payload = {
paabgabe_id
}
const url = '/api/frontend/v1/Lehre/deleteProjektarbeitAbgabe';
return this.$fhcApi.post(url, payload, null)
},
postSerientermin(datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, projektarbeit_ids) {
const payload = {
datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, projektarbeit_ids
}
const url = '/api/frontend/v1/Lehre/postSerientermin';
return this.$fhcApi.post(url, payload, null)
},
fetchDeadlines(person_id) {
const payload = {
person_id
}
const url = '/api/frontend/v1/Lehre/fetchDeadlines';
return this.$fhcApi.post(url, payload, null)
}
}
+14 -6
View File
@@ -7,16 +7,24 @@ export default {
url: '/api/frontend/v1/Lehrveranstaltung/loadByLV/' + encodeURIComponent(lehrveranstaltung_id)
};
},
getByStg(studiengang_kz, semester)
getByStg(studiensemester_kurzbz, studiengang_kz, semester = null)
{
return ("/api/frontend/v1/Lehrveranstaltung/loadByStudiengang/" + encodeURIComponent(studiengang_kz) + "/" + encodeURIComponent(semester));
},
let path = "/api/frontend/v1/Lehrveranstaltung/getByStg/" + encodeURIComponent(studiensemester_kurzbz) + "/" + encodeURIComponent(studiengang_kz);
getByEmpStg(mitarbeiter_uid, stg)
if (semester)
path += "/" + encodeURIComponent(semester);
return path;
},
getByEmp(studiensemester_kurzbz, mitarbeiter_uid, stg = null)
{
return ("/api/frontend/v1/Lehrveranstaltung/loadByEmployee/" + encodeURIComponent(mitarbeiter_uid) + "/" + encodeURIComponent(stg));
},
let path = "/api/frontend/v1/Lehrveranstaltung/getByEmp/" + encodeURIComponent(studiensemester_kurzbz) + "/" + encodeURIComponent(mitarbeiter_uid);
if (stg)
path += "/" + encodeURIComponent(stg);
return path;
},
getTable(url)
{
return {
+1 -1
View File
@@ -30,7 +30,7 @@ export default {
{
return {
method: 'post',
url: '/api/frontend/v1/lv/gruppe/deleteLVPlan/',
url: '/api/frontend/v1/lv/gruppe/deleteFromLVPlan/',
params: deleteData
};
},
+47
View File
@@ -0,0 +1,47 @@
export default {
getRoomInfo(ort_kurzbz, start_date, end_date) {
return this.$fhcApi.post(
'/api/frontend/v1/LvPlan/getRoomplan',
{ ort_kurzbz, start_date, end_date}
);
},
getStunden() {
return this.$fhcApi.get(
'/api/frontend/v1/LvPlan/Stunden',
{}
);
},
getOrtReservierungen(ort_kurzbz, start_date, end_date) {
return this.$fhcApi.post(
`/api/frontend/v1/LvPlan/getReservierungen/${ort_kurzbz}`,
{ start_date, end_date}
);
},
getLvPlanReservierungen(start_date, end_date) {
return this.$fhcApi.post(
'/api/frontend/v1/LvPlan/getReservierungen',
{ start_date, end_date }
);
},
getLehreinheitStudiensemester(lehreinheit_id) {
return this.$fhcApi.get(
`/api/frontend/v1/LvPlan/getLehreinheitStudiensemester/${lehreinheit_id}`,
{}
);
},
studiensemesterDateInterval(date) {
return this.$fhcApi.get(
`/api/frontend/v1/LvPlan/studiensemesterDateInterval/${date}`,
{}
);
},
LvPlanEvents(start_date, end_date, lv_id) {
return this.$fhcApi.get(
'/api/frontend/v1/LvPlan/LvPlanEvents',
{ start_date, end_date, lv_id }
);
},
};
+15
View File
@@ -7,4 +7,19 @@ export default {
{ ort_kurzbz: ort_kurbz }
);
},
getRooms(datum, von, bis, typ, personenanzahl = 0) {
return this.$fhcApi.get(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/api/frontend/v1/Ort/getRooms",
{ datum, von, bis, typ, personenanzahl }
);
},
getRoomTypes() {
return this.$fhcApi.get(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/api/frontend/v1/Ort/getTypes"
);
}
}
+9 -1
View File
@@ -16,8 +16,16 @@
*/
export default {
search(searchsettings) {
search(searchsettings, config) {
const url = '/api/frontend/v1/searchbar/search';
return this.$fhcApi.post(url, searchsettings, config);
},
searchAdvanced(searchsettings, config) {
const url = '/api/frontend/v1/searchbar/searchAdvanced';
return this.$fhcApi.post(url, searchsettings, config);
},
searchdummy(searchsettings) {
const url = 'public/js/apps/api/dummyapi.php/Search';
return this.$fhcApi.post(url, searchsettings);
}
};
+47
View File
@@ -0,0 +1,47 @@
export default {
getStudiensemester: function () {
return this.$fhcApi.get(
'/components/Cis/Mylv/Studiensemester',
{}
);
},
getAllStudienSemester: function (studiensemester=undefined, studiengang=undefined, semester=undefined, studienplan=undefined) {
return this.$fhcApi.get(
'/api/frontend/v1/Studium/getStudienAllSemester',
{studiensemester, studiengang, semester, studienplan}
);
},
getStudiengaengeForStudienSemester: function (studiensemester) {
return this.$fhcApi.get(
`/api/frontend/v1/Studium/getStudiengaengeForStudienSemester/${studiensemester}`,
{}
);
},
getStudienplaeneBySemester: function (studiengang, studiensemester) {
return this.$fhcApi.get(
`/api/frontend/v1/Studium/getStudienplaeneBySemester`,
{
studiengang,
studiensemester,
}
);
},
getLvPlanForStudiensemester: function (studiensemester, lvid) {
return this.$fhcApi.get(
`/api/frontend/v1/LvPlan/getLvPlanForStudiensemester/${studiensemester}/${lvid}`,
{
}
);
},
getLvEvaluierungInfo: function (studiensemester_kurzbz, lvid) {
return this.$fhcApi.get(
`/api/frontend/v1/Studium/getLvEvaluierungInfo/${studiensemester_kurzbz}/${lvid}`,
{
}
);
},
}
-45
View File
@@ -1,45 +0,0 @@
export default {
getRoomInfo(ort_kurzbz, start_date, end_date) {
return this.$fhcApi.get(
'/api/frontend/v1/Stundenplan/getRoomplan',
{ ort_kurzbz, start_date, end_date}
);
},
getStundenplan(start_date, end_date, lv_id) {
return this.$fhcApi.get(
'/api/frontend/v1/Stundenplan/getStundenplan',
{ start_date, end_date, lv_id }
);
},
getStunden() {
return this.$fhcApi.get(
'/api/frontend/v1/Stundenplan/Stunden',
{}
);
},
getOrtReservierungen(ort_kurzbz, start_date, end_date) {
return this.$fhcApi.get(
`/api/frontend/v1/Stundenplan/Reservierungen/${ort_kurzbz}`,
{ start_date, end_date}
);
},
getStundenplanReservierungen(start_date, end_date) {
return this.$fhcApi.get(
'/api/frontend/v1/Stundenplan/Reservierungen',
{ start_date, end_date }
);
},
getLehreinheitStudiensemester(lehreinheit_id) {
return this.$fhcApi.get(
`/api/frontend/v1/Stundenplan/getLehreinheitStudiensemester/${lehreinheit_id}`,
{}
);
},
studiensemesterDateInterval(date) {
return this.$fhcApi.get(
`/api/frontend/v1/Stundenplan/studiensemesterDateInterval/${date}`,
{}
);
},
};
+12 -6
View File
@@ -1,10 +1,16 @@
export default {
get(prestudent_id) {
return this.$fhcApi.post('api/frontend/v1/stv/student/get/' + prestudent_id);
get(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/student/get/'
+ encodeURIComponent(prestudent_id)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz);
return this.$fhcApi.post(url);
},
save(form, prestudent_id, data) {
return this.$fhcApi.post(form, 'api/frontend/v1/stv/student/save/' + prestudent_id,
data
);
save(form, prestudent_id, studiensemester_kurzbz, data) {
let url = 'api/frontend/v1/stv/student/save/'
+ encodeURIComponent(prestudent_id)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz);
return this.$fhcApi.post(form, url, data);
},
}
+27 -11
View File
@@ -2,17 +2,26 @@ export default {
list() {
return this.$fhcApi.get('api/frontend/v1/stv/grades/list');
},
getCertificate(prestudent_id, all) {
all = all ? '/all' : '';
return this.$fhcApi.get('api/frontend/v1/stv/grades/getCertificate/' + prestudent_id + all);
getCertificate(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/grades/getCertificate/' + encodeURIComponent(prestudent_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return this.$fhcApi.get(url);
},
getTeacherProposal(prestudent_id, all) {
all = all ? '/all' : '';
return this.$fhcApi.get('api/frontend/v1/stv/grades/getTeacherProposal/' + prestudent_id + all);
getTeacherProposal(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/grades/getTeacherProposal/' + encodeURIComponent(prestudent_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return this.$fhcApi.get(url);
},
getRepeaterGrades(prestudent_id, all) {
all = all ? '/all' : '';
return this.$fhcApi.get('api/frontend/v1/stv/grades/getRepeaterGrades/' + prestudent_id + all);
getRepeaterGrades(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/grades/getRepeaterGrades/' + encodeURIComponent(prestudent_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return this.$fhcApi.get(url);
},
updateCertificate({lehrveranstaltung_id, student_uid, studiensemester_kurzbz, note, lehrveranstaltung_bezeichnung}) {
return this.$fhcApi.post(
@@ -65,8 +74,15 @@ export default {
}
);
},
getGradeFromPoints(points, lehrveranstaltung_id, manualErrorHandling) {
getGradeFromPoints(points, lehrveranstaltung_id, studiensemester_kurzbz, manualErrorHandling) {
const config = manualErrorHandling ? {errorHandling: false} : {};
return this.$fhcApi.post('api/frontend/v1/stv/grades/getGradeFromPoints', {points, lehrveranstaltung_id}, config);
return this.$fhcApi.post('api/frontend/v1/stv/grades/getGradeFromPoints',
{
"points": points,
"lehrveranstaltung_id": lehrveranstaltung_id,
"studiensemester_kurzbz": studiensemester_kurzbz
},
config
);
}
}
+7 -2
View File
@@ -2,8 +2,13 @@ export default {
//------------- Prestudent.js------------------------------------------------------
get(prestudent_id){
return this.$fhcApi.post('api/frontend/v1/stv/prestudent/get/' + prestudent_id);
get(prestudent_id, studiensemester_kurzbz) {
return this.$fhcApi.post(
'api/frontend/v1/stv/prestudent/get/'
+ encodeURIComponent(prestudent_id)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz)
);
},
updatePrestudent(form, prestudent_id, data){
return this.$fhcApi.post(form, 'api/frontend/v1/stv/prestudent/updatePrestudent/' + prestudent_id,
+18 -6
View File
@@ -1,12 +1,24 @@
export default {
uid(uid) {
return this.$fhcApi.getUri('api/frontend/v1/stv/students/uid/' + uid);
uid(uid, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/students/'
+ encodeURIComponent(studiensemester_kurzbz)
+ '/uid/'
+ encodeURIComponent(uid);
return this.$fhcApi.getUri(url);
},
prestudent(prestudent_id) {
return this.$fhcApi.getUri('api/frontend/v1/stv/students/prestudent/' + prestudent_id);
prestudent(prestudent_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/students/'
+ encodeURIComponent(studiensemester_kurzbz)
+ '/prestudent/'
+ encodeURIComponent(prestudent_id);
return this.$fhcApi.getUri(url);
},
person(person_id) {
return this.$fhcApi.getUri('api/frontend/v1/stv/students/person/' + person_id);
person(person_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/stv/students/'
+ encodeURIComponent(studiensemester_kurzbz)
+ '/person/'
+ encodeURIComponent(person_id);
return this.$fhcApi.getUri(url);
},
verband(relative_path) {
return this.$fhcApi.getUri('api/frontend/v1/stv/students/' + relative_path);
+37 -9
View File
@@ -2,6 +2,7 @@ import FhcSearchbar from "../components/searchbar/searchbar.js";
import CisMenu from "../components/Cis/Menu.js";
import PluginsPhrasen from '../plugins/Phrasen.js';
import ApiSearchbar from '../api/factory/searchbar.js';
import Theme from "../plugins/Theme.js";
const app = Vue.createApp({
name: 'CisApp',
@@ -12,14 +13,17 @@ const app = Vue.createApp({
data: function() {
return {
searchbaroptions: {
origin: "cis",
cssclass: "",
calcheightonly: true,
types: [
"mitarbeiter",
"student",
"raum",
"organisationunit"
],
types: {
employee: Vue.computed(() => this.$p.t("search/type_employee")),
student: Vue.computed(() => this.$p.t("search/type_student")),
room: Vue.computed(() => this.$p.t("search/type_room")),
organisationunit: Vue.computed(() => this.$p.t("search/type_organisationunit")),
cms: Vue.computed(() => this.$p.t("search/type_cms")),
dms: Vue.computed(() => this.$p.t("search/type_dms"))
},
actions: {
employee: {
defaultaction: {
@@ -42,7 +46,7 @@ const app = Vue.createApp({
},
childactions: []
},
raum: {
room: {
defaultaction: {
type: "link",
renderif: function(data) {
@@ -104,14 +108,37 @@ const app = Vue.createApp({
}
},
childactions: []
}
},
cms: {
defaultaction: {
type: "link",
action: function (data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
return link;
}
},
childactions: []
},
dms: {
defaultaction: {
type: "link",
action: function (data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
'cms/dms.php?id=' + data.dms_id;
return link;
}
},
childactions: []
}
}
}
};
},
methods: {
searchfunction: function(searchsettings) {
return this.$api.call(ApiSearchbar.search(searchsettings));
return this.$api.call(ApiSearchbar.searchCis(searchsettings));
}
}
});
@@ -122,4 +149,5 @@ app.use(primevue.config.default, {
}
})
app.use(PluginsPhrasen);
app.use(Theme);
app.mount('#cis-header');
+114 -50
View File
@@ -1,20 +1,35 @@
import FhcDashboard from '../../components/Dashboard/Dashboard.js';
import PluginsPhrasen from '../../plugins/Phrasen.js';
import Theme from '../../plugins/Theme.js';
import contrast from '../../directives/contrast.js';
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js";
import Stundenplan, {DEFAULT_MODE_STUNDENPLAN} from "../../components/Cis/Stundenplan/Stundenplan.js";
import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js";
import MyLvPlan from "../../components/Cis/LvPlan/Personal.js";
import MylvStudent from "../../components/Cis/Mylv/Student.js";
import Profil from "../../components/Cis/Profil/Profil.js";
import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js";
import CmsNews from "../../components/Cis/Cms/News.js";
import CmsContent from "../../components/Cis/Cms/Content.js";
import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js";
import RoomInformation, {DEFAULT_MODE_RAUMINFO} from "../../components/Cis/Mylv/RoomInformation.js";
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js";
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js";
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js";
import Studium from "../../components/Cis/Studium/Studium.js";
import ApiRenderers from '../../api/factory/renderers.js';
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(`/${ciPath}`),
routes: [
{
path: `/Cis/Studium`,
name: 'Studium',
component: Studium,
props: true
},
{
path: `/Cis/Profil/View/:uid`,
name: 'ProfilView',
@@ -27,7 +42,30 @@ const router = VueRouter.createRouter({
component: Profil,
props: true
},
{
path: `/Cis/Abgabetool/Student/:student_uid_prop?`,
name: 'AbgabetoolStudent',
component: AbgabetoolStudent,
props: true
},
{
path: `/Cis/Abgabetool/Mitarbeiter`,
name: 'AbgabetoolMitarbeiter',
component: AbgabetoolMitarbeiter,
props: true
},
{
path: `/Cis/Abgabetool/Deadlines/:person_uid_prop?`,
name: 'DeadlineOverview',
component: DeadlineOverview,
props: true
},
{
path: `/Cis/Raumsuche`,
name: 'Raumsuche',
component: Raumsuche,
props: true
},
// Redirect old links to new format
{
path: "/CisVue/Cms/getRoomInformation/:ort_kurzbz",
@@ -113,59 +151,41 @@ const router = VueRouter.createRouter({
},
// Redirect old links to new format
{
// only trigger on first param being numeric to avoid paths like "Stundenplan/Month" entering here
path: "/Cis/Stundenplan/:lv_id(\\d+)",
name: "StundenplanOld",
component: Stundenplan,
redirect: (to) => {
return { // redirect to longer Stundenplan url and map params
name: "Stundenplan",
// only trigger on first param being numeric to avoid paths like "LvPlan/Month" entering here
path: "/Cis/LvPlan/:lv_id(\\d+)",
name: "LvPlanOld",
component: LvPlan,
redirect(to) {
const route = Vue.unref(router.currentRoute);
const { mode, focus_date } = route.params; // keep mode and focus_date if available
return { // redirect to longer LvPlan url and map params
name: "LvPlan",
params: {
mode,
focus_date,
lv_id: to.params.lv_id
},
};
},
},
{
path: `/Cis/Stundenplan/:mode?/:focus_date?/:lv_id?`,
name: 'Stundenplan',
component: Stundenplan,
props: (route) => { // validate and set mode/focus date if for some reason missing
const validModes = ["Month", "Week", "Day"];
// check mode string
const mode = route.params.mode &&
validModes.includes(route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase())
? route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()
: DEFAULT_MODE_STUNDENPLAN;
// default to today date if not provided or string forms invalid date
const d = new Date(route.params.focus_date)
const focus_date = !isNaN(d) ? route.params.focus_date : new Date().toISOString().split("T")[0];
// for consistency reasons format the props into one object but actually use a new name to we dont collide with
// existing viewData declaration written from codeigniter 3 into routerview tag
path: `/Cis/LvPlan/:mode?/:focus_date?/:lv_id?`,
name: 'LvPlan',
component: LvPlan,
props(route) {
return {
propsViewData: {
mode,
focus_date,
lv_id: route.params.lv_id
}
propsViewData: route.params
};
}
},
{
path: `/Cis/MyLvPlan/:mode?/:focus_date?`,
name: 'MyLvPlan',
component: MyLvPlan,
props(route) {
return {
propsViewData: route.params
};
},
beforeEnter: (to, from, next) => {
// missing mode or focus_date -> set defaults
if (!to.params.mode || !to.params.focus_date) {
next({
name: "Stundenplan",
params: {
mode: to.params.mode || DEFAULT_MODE_STUNDENPLAN,
focus_date: to.params.focus_date || new Date().toISOString().split("T")[0],
lv_id: to.params.lv_id
}
});
} else {
next();
}
}
},
{
@@ -200,7 +220,8 @@ const router = VueRouter.createRouter({
const app = Vue.createApp({
name: 'FhcApp',
data: () => ({
appSideMenuEntries: {}
appSideMenuEntries: {},
renderers: null,
}),
components: {},
computed: {
@@ -210,7 +231,8 @@ const app = Vue.createApp({
},
provide() {
return { // provide injectable & watchable language property
language: Vue.computed(() => this.$p.user_language)
language: Vue.computed(() => this.$p.user_language),
renderers: Vue.computed(() => this.renderers),
}
},
methods: {
@@ -220,7 +242,8 @@ const app = Vue.createApp({
},
handleClick(event) {
const target = event.target.closest('a');
if(target?.id == 'skiplink') return
if (target && this.isInternalRoute(target.href)) {
const url = new URL(target.href)
@@ -247,6 +270,45 @@ const app = Vue.createApp({
}
}
},
async created(){
await this.$api
.call(ApiRenderers.loadRenderers())
.then(res => res.data)
.then(data => {
for (let rendertype of Object.keys(data)) {
let modalTitle = null;
let modalContent = null;
let calendarEvent = null;
if (data[rendertype].modalTitle)
modalTitle = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].modalTitle)));
if (data[rendertype].modalContent)
modalContent = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].modalContent)));
if (data[rendertype].calendarEvent)
calendarEvent = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].calendarEvent)));
if (data[rendertype].calendarEventStyles){
var head = document.head;
if(!head.querySelector(`link[href="${data[rendertype].calendarEventStyles}"]`)){
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = data[rendertype].calendarEventStyles;
head.appendChild(link);
}
}
if(this.renderers === null) {
this.renderers = {};
}
if (!this.renderers[rendertype]) {
this.renderers[rendertype] = {}
}
this.renderers[rendertype].modalTitle = modalTitle;
this.renderers[rendertype].modalContent = modalContent;
this.renderers[rendertype].calendarEvent = calendarEvent;
}
});
},
mounted() {
document.addEventListener('click', this.handleClick);
},
@@ -264,6 +326,8 @@ app.use(primevue.config.default, {
tooltip: 8000
}
})
app.directive('tooltip', primevue.tooltip);
app.use(PluginsPhrasen);
app.use(Theme);
app.directive('contrast', contrast);
app.mount('#fhccontent');
app.mount('#fhccontent');
+2 -1
View File
@@ -1,5 +1,6 @@
import {CoreNavigationCmpt} from '../components/navigation/Navigation.js';
import DashboardAdmin from '../components/Dashboard/Admin.js';
import Phrases from "../plugin/Phrasen.js"
Vue.createApp({
name: 'DashboardAdminApp',
@@ -12,4 +13,4 @@ Vue.createApp({
},
mounted() {
}
}).mount('#main');
}).use(Phrases).mount('#main');
+61 -5
View File
@@ -13,8 +13,36 @@ const router = VueRouter.createRouter({
},
{
name: `byEmp`,
path: `/emp/:emp/:stg?`,
component: LVVerwaltung
path: `/emp/:studiensemester_kurzbz/:emp/:stg?/:semester?`,
component: LVVerwaltung,
props: route => {
let {emp, stg, semester, studiensemester_kurzbz} = route.params;
if (emp === '')
emp = undefined;
if (stg === '')
stg = undefined;
if (studiensemester_kurzbz === '')
studiensemester_kurzbz = undefined;
return {
studiensemester_kurzbz: studiensemester_kurzbz,
emp: emp,
stg: stg,
semester: semester
};
},
beforeEnter: (to, from, next) => {
const { studiensemester_kurzbz } = to.params;
const isSemester = /^(SS|WS)\d{4}$/.test(studiensemester_kurzbz);
if (!isSemester)
return next({ path: '/' });
else
next();
}
},
/*{
name: `byFachbereich`,
@@ -23,13 +51,41 @@ const router = VueRouter.createRouter({
},*/
{
name: `byStg`,
path: `/stg/:stg/:semester?`,
component: LVVerwaltung
path: '/stg/:studiensemester_kurzbz/:stg?/:semester?/',
component: LVVerwaltung,
props: route => {
let { studiensemester_kurzbz, stg, semester } = route.params;
if (semester === '')
semester = undefined;
if (studiensemester_kurzbz === '')
studiensemester_kurzbz = undefined;
if (stg === '')
stg = undefined;
return {
studiensemester_kurzbz: studiensemester_kurzbz,
stg: stg,
semester: semester,
};
},
beforeEnter: (to, from, next) => {
const studiensemester_kurzbz = to.params?.studiensemester_kurzbz
const isSemester = /^(SS|WS)\d{4}$/.test(studiensemester_kurzbz);
if (!isSemester)
return next({ path: '/' });
else
next();
}
},
{
path: '/:pathMatch(.*)*',
redirect: '/'
}
},
]
});
+139 -5
View File
@@ -26,14 +26,148 @@ const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes: [
{ path: `/${ciPath}/studentenverwaltung`, component: FhcStudentenverwaltung },
{ path: `/${ciPath}/studentenverwaltung/prestudent/:prestudent_id`, component: FhcStudentenverwaltung },
{ path: `/${ciPath}/studentenverwaltung/prestudent/:prestudent_id/:tab`, component: FhcStudentenverwaltung },
{ path: `/${ciPath}/studentenverwaltung/student/:id`, component: FhcStudentenverwaltung },
{ path: `/${ciPath}/studentenverwaltung/person/:person_id`, component: FhcStudentenverwaltung }
{
name: 'index',
path: `/${ciPath}/studentenverwaltung`,
component: FhcStudentenverwaltung
},
{
name: 'studiensemester',
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz`,
component: FhcStudentenverwaltung,
props: (route) => {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz
};
},
beforeEnter: (to, from, next) => {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
next();
}
},
{
name: 'studiengang',
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/:studiengang`,
component: FhcStudentenverwaltung,
props: (route) => {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
url_studiengang: route.params.studiengang,
};
},
beforeEnter: (to, from, next) => {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
const isStudiengang = /^[A-Z]{3}/.test(to.params.studiengang);
if (!isStudiengang) {
return next({
name: 'studiensemester',
params: {
studiensemester_kurzbz: to.params.studiensemester_kurzbz
}
});
}
next();
}
},
{
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/prestudent/:prestudent_id`,
component: FhcStudentenverwaltung,
props: (route) => {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
url_mode: 'prestudent',
url_prestudent_id: route.params.prestudent_id
};
},
beforeEnter: (to, from, next) => {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
next();
}
},
{
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/prestudent/:prestudent_id/:tab`,
component: FhcStudentenverwaltung,
props: (route) => {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
url_mode: 'prestudent',
url_prestudent_id: route.params.prestudent_id,
url_tab: route.params.tab
};
},
beforeEnter: (to, from, next) => {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
next();
}
},
{
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/student/:id`,
component: FhcStudentenverwaltung,
props: (route) => {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
url_mode: 'student',
url_student_id: route.params.id
};
},
beforeEnter: (to, from, next) => {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
next();
}
},
{
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/person/:person_id`,
component: FhcStudentenverwaltung,
props: (route) => {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
url_mode: 'person',
url_prestudent_id: route.params.person_id
};
},
beforeEnter: (to, from, next) => {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
next();
}
},
{
path: '/:pathMatch(.*)*',
redirect: {
name: 'index'
}
}
]
});
router.afterEach((to, from, failure) => {
let title = 'Studierendenverwaltung FH-Complete';
if (to.params.studiengang) {
title = to.params.studiengang + ' ' + title;
}
if (to.params.studiensemester_kurzbz) {
title = to.params.studiensemester_kurzbz + ' ' + title;
}
document.title = title;
});
const app = Vue.createApp({
name: 'StudentenverwaltungApp'
});
+1 -1
View File
@@ -16,7 +16,7 @@ export default {
:class="this.cModalClass"
:id="this.id"
:aria-labelledby="this.id + '_label'" aria-hidden="true" tabindex="-1">
<div class="modal-dialog" :class="this.cModalDialogClass">
<div class="modal-dialog modal-dialog-scrollable" :class="this.cModalDialogClass">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" :id="this.id + '_label'">
@@ -137,8 +137,6 @@ export default {
layout: 'fitColumns',
layoutColumnsOnNewData: false,
height: '550',
selectableRangeMode: 'click',
selectable: true,
persistenceID: 'core-betriebsmittel'
},
tabulatorEvents: [
@@ -292,12 +290,6 @@ export default {
this.formData.anmerkung = null;
this.formData.beschreibung = null;
this.statusNew = true;
},
//helper function: workaround to trigger validation if input is not a number
normalizeKaution() {
if (this.formData.kaution === null || this.formData.kaution === '') {
this.formData.kaution = 'xxx'
}
}
},
created() {
@@ -317,6 +309,7 @@ export default {
table-only
:side-menu="false"
reload
:reload-btn-infotext="this.$p.t('table', 'reload')"
new-btn-show
:new-btn-label="this.$p.t('ui', 'betriebsmittel')"
@click:new="actionNewBetriebsmittel"
@@ -407,10 +400,9 @@ export default {
<div class="row mb-3">
<form-input
type="number"
type="text"
:label="$p.t('infocenter/kaution')"
name="kaution"
@blur="normalizeKaution"
v-model="formData.kaution"
>
</form-input>
@@ -434,6 +426,7 @@ export default {
v-model="formData.ausgegebenam"
auto-apply
:enable-time-picker="false"
text-input
format="dd.MM.yyyy"
preview-format="dd.MM.yyyy"
:teleport="true"
@@ -449,6 +442,7 @@ export default {
v-model="formData.retouram"
auto-apply
:enable-time-picker="false"
text-input
format="dd.MM.yyyy"
preview-format="dd.MM.yyyy"
:teleport="true"
-17
View File
@@ -1,17 +0,0 @@
import CalendarHeader from './Header.js';
export default {
components: {
CalendarHeader
},
inject: [
'date',
'focusDate',
'size'
],
emits: [
'updateMode',
'change:range',
'input'
]
}
+311
View File
@@ -0,0 +1,311 @@
import BaseDraganddrop from './Base/DragAndDrop.js';
import BaseHeader from './Base/Header.js';
import BaseSlider from './Base/Slider.js';
import BsModal from '../Bootstrap/Modal.js';
import CalClick from '../../directives/Calendar/Click.js';
export default {
name: "CalendarBase",
components: {
BaseDraganddrop,
BaseHeader,
BaseSlider,
BsModal
},
directives: {
CalClick
},
provide() {
return {
locale: Vue.computed(() => this.locale),
timezone: Vue.computed(() => this.timezone),
timeGrid: Vue.computed(() => this.timeGrid),
draggableEvents: Vue.computed(() => {
if (!this.draggableEvents)
return () => false;
if (Array.isArray(this.draggableEvents))
return event => this.draggableEvents.includes(event.type);
if (this.draggableEvents instanceof Function)
return this.draggableEvents;
return () => true;
}),
dropableEvents: Vue.computed(() => {
if (!this.onDrop)
return () => false;
if (Array.isArray(this.dropableEvents))
return item => this.dropableEvents.includes(item.type);
if (this.dropableEvents instanceof Function)
return this.dropableEvents;
return () => true;
}),
hasDragoverFunc: Vue.computed(() => this.onDragover),
mode: Vue.computed(() => this.mode)
};
},
props: {
locale: {
type: String,
default: 'de'
},
timezone: {
type: String,
required: true
},
date: {
type: [Date, String, Number, luxon.DateTime],
default: props => luxon.DateTime.now().setZone(props.timezone).startOf('day')
},
modes: {
type: Object,
required: true,
default: {}
// TODO(chris): verfication functions
},
mode: String,
modeOptions: Object,
events: {
type: Array,
default: []
},
backgrounds: {
type: Array,
default: []
},
showBtns: Boolean,
btnMonth: {
type: Boolean,
default: undefined
},
btnWeek: {
type: Boolean,
default: undefined
},
btnDay: {
type: Boolean,
default: undefined
},
btnList: {
type: Boolean,
default: undefined
},
timeGrid: Array,
draggableEvents: [Boolean, Array, Function],
dropableEvents: [Boolean, Array, Function],
onDragover: Function,
onDrop: Function
},
emits: [
"click:next",
"click:prev",
"click:mode",
"click:event",
"click:day",
"click:week",
"update:date",
"update:mode",
"update:range",
"drop"
],
data() {
return {
internalView: null,
internalDate: null,
modalEvent: null
};
},
computed: {
convertedEvents() {
return this.events.map(orig => ({
id: orig.type + orig[orig.type + '_id'],
type: orig.type,
start: luxon.DateTime.fromISO(orig.isostart).setZone(this.timezone),
end: luxon.DateTime.fromISO(orig.isoend).setZone(this.timezone),
orig
}));
},
convertedBackgrounds() {
return this.backgrounds.map(bg => {
const res = { ...bg };
if (res.start) {
if (Number.isInteger(res.start))
res.start = luxon.DateTime.fromMillis(res.start, { zone: this.timezone, locale: this.locale });
else if (res.start instanceof Date)
res.start = luxon.DateTime.fromJSDate(res.start, { zone: this.timezone, locale: this.locale });
else if (typeof res.start ===
'string' || res.start instanceof String)
res.start = luxon.DateTime.fromISO(res.start, { zone: this.timezone, locale: this.locale });
}
if (res.end) {
if (Number.isInteger(res.end))
res.end = luxon.DateTime.fromMillis(res.end, { zone: this.timezone, locale: this.locale });
else if (res.end instanceof Date)
res.end = luxon.DateTime.fromJSDate(res.end, { zone: this.timezone, locale: this.locale });
else if (typeof res.end ===
'string' || res.end instanceof String)
res.end = luxon.DateTime.fromISO(res.end, { zone: this.timezone, locale: this.locale });
}
return res;
});
},
sDate() {
if (this.date instanceof luxon.DateTime)
return this.date;
return luxon.DateTime.fromJSDate(new Date(this.date)).setZone(this.timezone);
},
cDate: {
get() {
const date = this.internalDate ? this.internalDate : this.sDate;
return date.setLocale(this.locale);
},
set(value) {
this.internalDate = value;
this.$emit('update:date', value, this.cMode);
}
},
sMode() {
// choose default mode
let mode = this.mode;
if (mode)
mode = mode.toLowerCase();
if (!mode || !this.modes[mode])
mode = Object.keys(this.modes).find(Boolean); // start with first entry as active mode
return mode || '';
},
cMode: {
get() {
return this.internalView ? this.internalView : this.sMode;
},
set(value) {
this.internalView = value;
this.$emit('update:mode', value, this.cDate);
}
}
},
watch: {
sDate(n, o) {
if (this.sDate.isValid && !this.sDate.hasSame(this.internalDate, 'day'))
this.internalDate = this.sDate;
},
sMode() {
if (this.sMode)
this.internalView = this.sMode;
}
},
methods: {
clickPrev() {
const evt = new Event('click:prev', {cancelable: true});
this.$emit('click:prev', evt);
if (evt.defaultPrevented)
return;
// default: switch page
this.$refs.mode.prevPage();
},
clickNext() {
const evt = new Event('click:next', {cancelable: true});
this.$emit('click:next', evt);
if (evt.defaultPrevented)
return;
// default: switch page
this.$refs.mode.nextPage();
},
handleClickDefaults(evt) {
// TODO(chris): implement
switch (evt.detail.source) {
case 'day':
if (this.cMode != 'day' && this.modes['day']) {
evt.stopPropagation();
this.cDate = evt.detail.value;
this.cMode = 'day';
}
break;
case 'week':
if (this.cMode != 'week' && this.modes['week']) {
evt.stopPropagation();
this.cDate = luxon.DateTime.fromObject({
localWeekNumber: evt.detail.value.number,
localWeekYear: evt.detail.value.year
}, {
zone: this.cDate.zoneName,
locale: this.cDate.locale
});
this.cMode = 'week';
}
break;
}
},
onDropItem(evt, start, end) {
this.$emit('drop', evt, start, end);
},
showEventModal(eventObj) {
this.modalEvent = eventObj;
this.$refs.modal.show();
},
hideEventModal() {
if (this.modalEvent)
this.modalEvent.closeFn = undefined;
this.$refs.modal.hide();
this.modalEvent = null;
},
onModalHidden() {
if (this.modalEvent.closeFn)
this.modalEvent.closeFn();
}
},
beforeUnmount() {
this.hideEventModal();
},
template: /* html */`
<div class="fhc-calendar-base h-100">
<base-draganddrop
class="card h-100"
:events="convertedEvents"
:backgrounds="convertedBackgrounds"
@drop="onDropItem"
v-cal-click:container
@cal-click-default.capture="handleClickDefaults"
>
<base-header
class="card-header"
v-model:date="cDate"
v-model:mode="cMode"
@prev="clickPrev"
@next="clickNext"
@click:mode="$emit('click:mode', $event)"
:btn-day="!!modes['day'] && (btnDay || (showBtns && btnDay !== false))"
:btn-week="!!modes['week'] && (btnWeek || (showBtns && btnWeek !== false))"
:btn-month="!!modes['month'] && (btnMonth || (showBtns && btnMonth !== false))"
:btn-list="!!modes['list'] && (btnList || (showBtns && btnList !== false))"
:mode-options="modeOptions ? modeOptions[cMode] : undefined"
>
<slot name="actions" />
</base-header>
<component
:is="modes ? modes[cMode] : null || 'div'"
ref="mode"
v-model:current-date="cDate"
@update:range="$emit('update:range', $event)"
@request-modal-open="showEventModal"
@request-modal-close="hideEventModal"
v-bind="modeOptions ? modeOptions[cMode] : null || {}"
>
<template v-slot="slot"><slot v-bind="slot" /></template>
</component>
</base-draganddrop>
<bs-modal ref="modal" dialog-class="modal-lg" body-class="" @hidden-bs-modal="onModalHidden">
<template #title>
<slot v-if="modalEvent" v-bind="{mode: 'eventheader', event: modalEvent.event}" />
</template>
<template #default>
<slot v-if="modalEvent" v-bind="{mode: 'event', event: modalEvent.event}" />
</template>
</bs-modal>
</div>
`
}
@@ -0,0 +1,162 @@
import DragAndDrop from '../../../helpers/DragAndDrop.js';
import CalDnd from '../../../directives/Calendar/DragAndDrop.js';
/**
* TODO(chris): this needs serious rework!
*/
export default {
name: "CalendarDragAndDrop",
directives: {
CalDnd
},
provide() {
return {
events: Vue.computed(() => this.correctedEvents),
backgrounds: Vue.computed(() => this.backgrounds),
dropAllowed: Vue.computed(() => this.dragging && this.dropAllowed)
};
},
inject: {
mode: "mode",
dropableEvents: "dropableEvents"
},
props: {
events: Array,
backgrounds: Array
},
emits: [
"drop"
],
data() {
return {
dragging: false,
allowed: false,
draggedInternalEvent: null,
draggedExternalEvent: null,
targetTimestamp: 0,
targetGridEnds: null,
dropAllowed: false,
shadowPreview: false // TODO(chris): IMPLEMENT! (use background instead of event as preview)
};
},
computed: {
correctedEvents() {
if (this.dragging) {
if (this.draggedInternalEvent) {
const index = this.events.findIndex(e => e.id == this.draggedInternalEvent.id);
if (this.previewEvent && !this.shadowPreview)
return this.events.toSpliced(index, 1, this.previewEvent);
else
return this.events.toSpliced(index, 1);
}
if (this.previewEvent && !this.shadowPreview)
return [...this.events, this.previewEvent];
}
return this.events;
},
correctedBackgrounds() {
if (this.dragging) {
if (this.shadowPreview) {
// TODO(chris): how to get the length
return [...this.backgrounds, {
start: new Date(this.targetTimestamp),
class: 'shadow-preview'
}];
}
}
return this.backgrounds;
},
previewEvent() {
if (!this.dragging || !this.dropAllowed)
return null;
if (!this.targetTimestamp)
return null;
const event = this.draggedInternalEvent || this.draggedExternalEvent;
if (!event)
return null;
// TODO(chris): calculate length correctly from orig
let length = event.end - event.start;
if (this.targetGridEnds)
length = this.targetGridEnds.find(end => end >= this.targetTimestamp + length) - this.targetTimestamp;
return {
orig: event.orig,
start: this.targetTimestamp,
end: this.targetTimestamp + length
};
}
},
methods: {
onDragstart(evt) {
DragAndDrop.setTransferData(evt.detail.originalEvent, evt.detail.item.orig);
this.draggedInternalEvent = evt.detail.item;
},
onDragend() {
this.draggedInternalEvent = null;
this.dragging = false;
},
onDragenter(evt) {
this.dragging = true;
if (!this.draggedInternalEvent) {
const event = DragAndDrop.getValidTransferData(evt.detail.originalEvent);
if (event) {
this.draggedExternalEvent = {
id: event.id,
type: event.type,
start: event.isostart
? luxon.DateTime.fromISO(event.isostart).setZone(this.timezone)
: luxon.DateTime.local().setZone(this.timezone),
end: event.isoend
? luxon.DateTime.fromISO(event.isoend).setZone(this.timezone)
: luxon.DateTime.local().setZone(this.timezone),
orig: event
};
} else {
this.draggedExternalEvent = null;
}
this.dropAllowed = this.dropableEvents(event, this.mode);
} else {
this.dropAllowed = this.dropableEvents(this.draggedInternalEvent, this.mode);
}
},
onDragleave() {
this.dragging = false;
},
onDragchange(evt) {
this.targetTimestamp = evt.detail.timestamp;
this.targetGridEnds = evt.detail.ends || null;
},
onDrop(evt) {
if (!this.dragging || !this.dropAllowed)
return;
this.$emit('drop', evt, this.previewEvent.start, this.previewEvent.end);
this.dropAllowed = false;
this.dragging = false;
}
},
template: `
<div
class="fhc-calendar-base-draganddrop"
@calendar-dragstart="onDragstart"
@calendar-dragend="onDragend"
v-cal-dnd:dropcage
@calendar-dragenter="onDragenter"
@calendar-dragleave="onDragleave"
@calendar-dragchange="onDragchange"
@drop="onDrop"
>
<slot />
</div>
`
}
+435
View File
@@ -0,0 +1,435 @@
import GridLine from './Grid/Line.js';
import GridLineEvent from './Grid/Line/Event.js';
import CalDnd from '../../../directives/Calendar/DragAndDrop.js';
export default {
name: "CalendarGrid",
components: {
GridLine,
GridLineEvent
},
directives: {
CalDnd
},
inject: {
originalEvents: "events",
originalBackgrounds: "backgrounds",
dropAllowed: "dropAllowed"
},
provide() {
return {
flipAxis: Vue.computed(() => this.flipAxis),
axisRow: Vue.computed(() => this.axisRow)
};
},
props: {
axisMain: {
type: Array,
required: true,
validator(value) {
return value.every(item => item instanceof luxon.DateTime);
}
},
axisParts: {
type: Array,
required: true,
validator(value) {
return value.every(item =>
item instanceof luxon.Duration
|| Number.isInteger(item)
|| (
(
item.start instanceof luxon.Duration
|| Number.isInteger(item.start)
) && (
item.end instanceof luxon.Duration
|| Number.isInteger(item.end)
)
)
);
}
},
flipAxis: Boolean,
allDayEvents: Boolean,
axisMainCollapsible: Boolean,
snapToGrid: Boolean
},
data() {
return {
dragging: false,
resizeObserver: null,
mutationObserver: null,
userScroll: true
};
},
computed: {
axisRow() {
return this.flipAxis ? 'column' : 'row';
},
axisCol() {
return this.flipAxis ? 'row' : 'column';
},
axisPartsWithBreaks() {
return this.axisParts.reduce((res, tu, index) => {
const start = tu.start || tu;
const end = tu.end;
if (res.length) {
const lastTuEnd = res.pop();
if (Array.isArray(lastTuEnd)) {
res.push({
start: lastTuEnd[0],
end: start,
index: lastTuEnd[1]
});
} else if (lastTuEnd != start) {
// add pause
res.push({
start: lastTuEnd,
end: start
});
}
}
if (!end) {
res.push([start, index]);
} else {
res.push({
start,
end,
index
});
res.push(end);
}
return res;
}, []).slice(0, -1);
},
axisPartsSave() {
if (!this.axisParts[this.axisParts.length - 1].end)
return this.axisParts.slice(0, -1);
return this.axisParts;
},
start() {
return this.axisPartsWithBreaks[0].start;
},
end() {
return this.axisPartsWithBreaks[this.axisPartsWithBreaks.length - 1].end;
},
ends() {
const ends = [];
const partsEnds = this.axisPartsWithBreaks
.filter(p => p.index !== undefined)
.map(p => p.end);
for (var date of this.axisMain)
for (var part of partsEnds)
ends.push(date.plus(part));
return ends;
},
axisMainBorders() {
return this.axisMain.reduce(
(res, curr) => res.concat([curr.plus(this.start), curr.plus(this.end)]),
[]
);
},
eventsAllDay() {
if (!this.allDayEvents)
return [];
return this.mapIntoMainAxis(this.originalEvents.filter(event => event.orig.allDayEvent));
},
eventsNormal() {
if (!this.allDayEvents)
return this.events;
return this.mapIntoMainAxis(this.originalEvents.filter(event => !event.orig.allDayEvent));
},
events() {
return this.mapIntoMainAxis(this.originalEvents);
},
backgrounds() {
return this.mapIntoMainAxis(this.originalBackgrounds);
},
hasValidEvents() {
return this.events.find(e => e.length);
},
styleGridCols() {
let cols = 'repeat(' + this.axisMain.length + ', 1fr)';
if (this.axisMainCollapsible) {
if (this.hasValidEvents)
cols = this.events
.map(e => e.length
? '1fr'
: 'var(--fhc-calendar-axis-collapsible, .5fr)')
.join(' ');
}
return cols;
},
styleGridRows() {
const gridlines = {};
this.axisPartsWithBreaks.forEach(part => {
let ts = part.start.toMillis();
if (!gridlines[ts])
gridlines[ts] = ['t_' + ts];
if (part.index !== undefined)
gridlines[ts].push('ps_' + part.index);
ts = part.end.toMillis();
if (!gridlines[ts])
gridlines[ts] = ['t_' + ts];
if (part.index !== undefined)
gridlines[ts].push('pe_' + part.index);
});
this.eventsNormal.forEach((events, mainIndex) => {
let day = this.axisMain[mainIndex];
events.forEach(event => {
if (!event.startsHere && !event.endsHere)
return;
if (event.startsHere) {
let ts = event.start.diff(day).toMillis();
if (!gridlines[ts])
gridlines[ts] = ['t_' + ts, 'e_' + ts];
}
if (event.endsHere) {
let ts = event.end.diff(day).toMillis();
if (!gridlines[ts])
gridlines[ts] = ['t_' + ts, 'e_' + ts];
}
});
});
return Object.keys(gridlines).sort((a,b) => parseInt(a)-parseInt(b)).map((start, i, keys) => {
let end = keys[i + 1];
if (!end) {
gridlines[start].push('end');
return '[' + gridlines[start].join(' ') + ']';
}
return '[' + gridlines[start].join(' ') + '] ' + (end - start) + 'fr';
}).join(' ');
}
},
methods: {
mapIntoMainAxis(target) {
const result = Array.from({length: this.axisMain.length}, () => Array());
target.forEach(event => {
const start = event.start || this.axisMainBorders[0].plus(-1);
const end = event.end || this.axisMainBorders[this.axisMainBorders.length - 1].plus(1);
for (var i = 0; i < this.axisMain.length; i++) {
let laneStart = this.axisMainBorders[i * 2];
let laneEnd = this.axisMainBorders[i * 2 + 1];
if (event.orig?.allDayEvent) {
laneStart = laneStart.startOf('day');
laneEnd = laneEnd.endOf('day');
}
if (start < laneEnd && end > laneStart) {
const startsHere = start >= laneStart;
const endsHere = end <= laneEnd;
result[i].push({
...event,
startsHere,
endsHere
});
}
}
});
return result;
},
/* DRAG AND DROP */
getPageTop(el) {
let pageTop = el.offsetTop;
if (el.offsetParent)
pageTop += this.getPageTop(el.offsetParent);
return pageTop;
},
getPageLeft(el) {
let pageLeft = el.offsetLeft;
if (el.offsetParent)
pageLeft += this.getPageLeft(el.offsetParent);
return pageLeft;
},
getTimestampFromMouse(evt, dayTimestamp) {
let mouse, mouseFrac;
if (this.flipAxis) {
mouse = evt.pageX - this.getPageLeft(this.$refs.body) + this.$refs.main.scrollLeft;
mouseFrac = mouse / this.$refs.body.offsetWidth;
} else {
mouse = evt.pageY - this.getPageTop(this.$refs.body) + this.$refs.main.scrollTop;
mouseFrac = mouse / this.$refs.body.offsetHeight;
}
return dayTimestamp + this.start + Math.floor((this.end - this.start) * mouseFrac);
},
/* SCROLLING */
enableAutoScroll() {
if (!this.resizeObserver)
this.resizeObserver = new ResizeObserver(this.scrollToEarliestEvent);
this.resizeObserver.observe(this.$refs.body);
if (!this.mutationObserver)
this.mutationObserver = new MutationObserver(mutations => {
if (mutations.some(m => m.addedNodes.length && [].some.call(m.addedNodes, el => el.matches && el.matches('.fhc-calendar-base-grid-line-event'))))
this.scrollToEarliestEvent();
});
this.mutationObserver.observe(this.$refs.body, {
subtree: true,
childList: true
});
this.scrollToEarliestEvent();
},
disableAutoScroll() {
if (this.resizeObserver)
this.resizeObserver.disconnect();
this.resizeObserver = null;
if (this.mutationObserver)
this.mutationObserver.disconnect();
this.mutationObserver = null;
},
scrollToEarliestEvent() {
const eventElements = this.$refs.scroller.querySelectorAll('.fhc-calendar-base-grid-line-event');
let earliestEventOffset = [0, null];
for (var el of eventElements.values()) {
const top = el.offsetTop;
if (!earliestEventOffset[1] || top < earliestEventOffset[0])
earliestEventOffset = [top, el];
}
this.userScroll = false;
if (earliestEventOffset[1]) {
earliestEventOffset[1].scrollIntoView({ behavior: "smooth" });
} else {
this.$refs.scroller.scrollTo(0, 0);
}
}
},
beforeUnmount() {
this.disableAutoScroll();
},
template: /* html */`
<div
class="fhc-calendar-base-grid"
style="display:grid;width:100%;height:100%"
:style="'grid-template-' + axisRow + 's:auto' + (allDayEvents ? ' auto ' : ' ') + '1fr;grid-template-' + axisCol + 's:auto ' + styleGridCols"
>
<div
class="grid-header"
style="display:grid"
:style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'"
>
<div
v-for="(date, index) in axisMain"
:key="index"
class="main-header"
:class="{'collapsed-header': axisMainCollapsible && hasValidEvents && !events[index].length}"
:style="'grid-' + axisCol + ':' + (2+index)"
>
<slot name="main-header" v-bind="{ index, date }" />
</div>
</div>
<div
v-if="allDayEvents"
class="grid-allday"
style="display:grid"
:style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'"
>
<div
v-for="(events, index) in eventsAllDay"
:key="index"
class="all-day-events"
:style="'grid-' + axisCol + ':' + (2+index)"
>
<grid-line-event
v-for="(event, i) in events"
:key="i"
:event="event"
>
<template v-slot="slot">
<slot name="event" v-bind="slot" />
</template>
</grid-line-event>
</div>
</div>
<div
ref="scroller"
@scrollend="userScroll ? disableAutoScroll() : userScroll = true"
style="display:grid;overflow:auto"
:style="'grid-' + axisCol + ':1/-1;grid-template-' + axisCol + 's:subgrid'"
>
<div
ref="main"
class="grid-main"
style="position:relative;grid-column:1/-1;grid-row:1/-1;display:grid"
:style="'grid-template-' + axisCol + 's:subgrid;grid-template-' + axisRow + 's:' + styleGridRows"
>
<div
v-for="(part, index) in axisPartsSave"
:key="index"
class="part-header"
:style="'grid-' + axisCol + ':1;grid-' + axisRow + ': ps_' + index + '/pe_' + index"
>
<slot name="part-header" v-bind="{ index, part }" />
</div>
<div
ref="body"
class="grid-body"
style="display:grid;grid-template-rows:subgrid;grid-template-columns:subgrid"
:style="'grid-' + axisCol + ':2/-1;grid-' + axisRow + ':1/-1'"
v-cal-dnd:dropcage
@calendar-dragenter="dragging = true"
@calendar-dragleave="dragging = false"
@dragover="dropAllowed ? $event.preventDefault() : null"
>
<template
v-for="(date, index) in axisMain"
:key="index"
>
<div
v-for="(part, i) in axisPartsSave"
:key="i"
class="part-body"
style="position:relative"
:style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i"
>
<slot name="part-body" v-bind="{ index, part }" />
<div
v-if="snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1"
v-cal-dnd:dropzone.once="{date: date.plus(part.start || part), ends: ends.slice(ends.findIndex(end => end > date))}"
></div>
</div>
<grid-line
:start="date.plus(start)"
:end="date.plus(end)"
:date="date"
:events="eventsNormal[index]"
:backgrounds="backgrounds[index]"
style="position:relative"
:style="'grid-' + axisRow + ':1/-1;grid-' + axisCol + ':' + (1+index)"
>
<template #event="slot">
<slot name="event" v-bind="slot" />
</template>
<template #dropzone>
<div
v-if="!snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1"
v-cal-dnd:dropzone="evt => getTimestampFromMouse(evt, date)"
></div>
</template>
</grid-line>
</template>
</div>
</div>
</div>
</div>
`
}
@@ -0,0 +1,85 @@
import LineEvent from './Line/Event.js';
import LineBackground from './Line/Background.js';
/**
* TODO(chris):
* Event overflow for Month mode (more-button)
*/
export default {
name: "GridLine",
components: {
LineEvent,
LineBackground
},
inject: {
axisRow: "axisRow"
},
props: {
date: {
type: luxon.DateTime,
required: true
},
start: {
type: luxon.DateTime,
required: true
},
end: {
type: luxon.DateTime,
required: true
},
events: {
type: Array,
default: []
},
backgrounds: {
type: Array,
default: []
}
},
computed: {
eventsWithRowInfo() {
const events = [];
this.events.forEach(event => {
const rows = [1, -1];
if (event.startsHere) {
rows[0] = 't_' + event.start.diff(this.date).toMillis();
}
if (event.endsHere) {
rows[1] = 't_' + event.end.diff(this.date).toMillis();
}
events.push({
...event,
rows
});
});
return events;
}
},
template: /* html */`
<div
class="fhc-calendar-base-grid-line"
style="position:relative;display:grid;grid-auto-flow:dense"
:style="'grid-template-' + axisRow + 's:subgrid'"
>
<line-background
v-for="bg in backgrounds"
:start="start"
:end="end"
:background="bg"
></line-background>
<line-event
v-for="(event, i) in eventsWithRowInfo"
:key="i"
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
:event="event"
>
<template v-slot="slot">
<slot name="event" v-bind="slot" />
</template>
</line-event>
<slot name="dropzone" />
</div>
`
}
@@ -0,0 +1,72 @@
export default {
name: "GridLineBackground",
inject: {
flipAxis: "flipAxis"
},
props: {
start: {
type: luxon.DateTime,
required: true
},
end: {
type: luxon.DateTime,
required: true
},
background: {
type: Object,
required: true,
validator(value) {
if (!value.start && !value.end)
return false;
if (value.start && !(value.start instanceof luxon.DateTime))
return false;
if (value.end && !(value.end instanceof luxon.DateTime))
return false;
return true;
}
}
},
computed: {
styles() {
if (!this.background.endsHere && !this.background.startsHere)
return this.background.style;
const perc = (this.end.ts - this.start.ts) / 100;
let border = {};
if (this.background.startsHere)
border[this.flipAxis ? 'left' : 'top'] = (this.background.start.diff(this.start)) / perc + '%';
if (this.background.endsHere)
border[this.flipAxis ? 'right' : 'bottom'] = (this.end.diff(this.background.end)) / perc + '%';
if (!this.background.style)
return border;
return [this.background.style, border];
},
classes() {
if (!this.background.endsHere && !this.background.startsHere)
return this.background.class;
const result = [];
if (this.background.class)
result.push(this.background.class);
if (this.background.startsHere)
result.push('bg-begin');
if (this.background.endsHere)
result.push('bg-end');
return result;
}
},
template: /* html */`
<div
class="fhc-calendar-base-grid-line-background"
:class="classes"
style="position:absolute;inset:0;z-index:0"
:style="styles"
:title="background.title"
>
<span v-if="background.label">{{ background.label }}</span>
</div>
`
}
@@ -0,0 +1,57 @@
import CalDnd from '../../../../../directives/Calendar/DragAndDrop.js';
import CalClick from '../../../../../directives/Calendar/Click.js';
export default {
name: "GridLineEvent",
directives: {
CalDnd,
CalClick
},
inject: {
draggableEvents: "draggableEvents",
mode: "mode"
},
props: {
event: {
type: Object,
required: true,
validator(value) {
return (value.start && value.end && value.orig);
}
}
},
computed: {
isHeaderOrFooter() {
return ['header', 'footer'].includes(this.event.orig);
},
draggable() {
return !this.isHeaderOrFooter && this.draggableEvents(this.event.orig, this.mode);
},
classes() {
const classes = [];
if (this.isHeaderOrFooter) {
classes.push('event-' + this.event.orig);
} else {
if (this.event.startsHere)
classes.push('event-begin');
if (this.event.endsHere)
classes.push('event-end');
}
return classes
}
},
template: /* html */`
<div
class="fhc-calendar-base-grid-line-event event"
:class="classes"
style="z-index: 1"
:draggable="draggable"
v-cal-dnd:draggable="event"
v-cal-click:event="isHeaderOrFooter ? event : event.orig"
>
<slot :event="isHeaderOrFooter ? event : event.orig">
{{ event.orig }}
</slot>
</div>
`
}
@@ -0,0 +1,123 @@
/**
* TODO(chris): use click-directive
*/
import DatePicker from './Header/Datepicker.js';
export default {
name: "CalendarHeader",
components: {
DatePicker
},
props: {
date: {
type: luxon.DateTime,
required: true
},
mode: {
type: String,
required: true
},
modeOptions: {
type: Object,
default: {}
},
btnMonth: Boolean,
btnWeek: Boolean,
btnDay: Boolean,
btnList: Boolean
},
emits: [
"next",
"prev",
"click:mode",
"update:date",
"update:mode"
],
data() {
return {
open: false
};
},
methods: {
clickMode(evt, mode) {
this.$emit('click:mode', evt);
if (!evt.defaultPrevented)
this.$emit('update:mode', mode);
}
},
template: /* html */`
<div class="fhc-calendar-base-header">
<div class="header-actions">
<div class="header-userdefined">
<slot />
</div>
<div class="header-modes">
<div class="d-flex gap-1 justify-content-end" role="group">
<button
v-if="btnMonth"
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'month'}"
@click="clickMode($event, 'month')"
>
<i class="fa fa-calendar-days"></i>
</button>
<button
v-if="btnWeek"
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'week'}"
@click="clickMode($event, 'week')"
>
<i class="fa fa-calendar-week"></i>
</button>
<button
v-if="btnDay"
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'day'}"
@click="clickMode($event, 'day')"
>
<i class="fa fa-calendar-day"></i>
</button>
<button
v-if="btnList"
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'list'}"
@click="clickMode($event, 'list')"
>
<i class="fa fa-table-list"></i>
</button>
</div>
</div>
</div>
<div class="header-picker">
<div class="btn-group" role="group">
<button
class="btn btn-outline-secondary border-0"
@click="$emit('prev')"
:disabled="open"
>
<i class="fa fa-chevron-left"></i>
</button>
<date-picker
:mode="mode"
:date="date"
@update:date="$emit('update:date', $event)"
@open="open = true"
@closed="open = false"
:list-length="modeOptions.length"
/>
<button
class="btn btn-outline-secondary border-0"
@click="$emit('next')"
:disabled="open"
>
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
</div>
`
}
@@ -0,0 +1,116 @@
// TODO(chris): translate aria-labels
export default {
name: "CalendarHeaderDatepicker",
components: {
VueDatePicker
},
inject: [
"locale",
"timezone"
],
props: {
date: {
type: luxon.DateTime,
required: true
},
mode: {
type: String,
required: true
},
listLength: {
type: Number,
default: 7
}
},
emits: [
"update:date"
],
computed: {
convertedDate() {
// convert to target TZ then strip TZ Information
// so the datepicker can work with local times
return this.date.setZone(this.timezone).setZone('local', { keepLocalTime: true });
},
current() {
switch (this.mode) {
case "month":
return {month: this.convertedDate.month-1, year: this.convertedDate.year};
case "list":
return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.listLength }).ts - 1];
case "week":
return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts];
case "day":
return this.convertedDate;
default:
return null;
}
},
title() {
switch (this.mode) {
case "month":
return this.date.toLocaleString({ month: 'long', year: 'numeric' });
case "week":
var year = this.date.localWeekYear;
var week = this.date.toFormat('nn');
return this.$p.t('calendar/year_kw', { year, week });
case "list":
return this.date.toLocaleString(luxon.DateTime.DATE_FULL) + '-' + this.date.plus({ days: this.listLength - 1 }).toLocaleString(luxon.DateTime.DATE_FULL);
case "day":
return this.date.toLocaleString(luxon.DateTime.DATE_FULL);
default:
return 'View not Supported';
}
},
weekStart() {
return luxon.Info.getStartOfWeek(this.date)%7;
}
},
methods: {
update(value) {
let date;
switch (this.mode) {
case "month":
value.month++;
date = luxon.DateTime.fromObject(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break;
case "list":
case "week":
date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break;
case "day":
date = luxon.DateTime.fromJSDate(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break;
default:
return; // Don't update if the value is invalid!
}
this.$emit("update:date", date);
},
weekNumbers(date) {
return luxon.DateTime.fromJSDate(date, { locale: this.locale }).localWeekNumber;
}
},
template: /* html */`
<vue-date-picker
:model-value="current"
@update:model-value="update"
:format="() => title"
:month-picker="mode == 'month'"
:week-picker="mode == 'week'"
:range="mode == 'list' ? { autoRange: listLength - 1 } : false"
:text-input="mode == 'day'"
:week-start="weekStart"
:week-numbers="{ type: weekNumbers }"
:clearable="false"
:enable-time-picker="false"
:config="{ keepActionRow: mode != 'month' }"
:action-row="{ showSelect: false, showCancel: false, showNow: mode != 'month', showPreview: false }"
auto-apply
six-weeks
teleport
:locale="locale"
:now-button-label="$p.t('calendar/today')"
:week-num-name="$p.t('calendar/kw')"
/>
`
}
@@ -0,0 +1,39 @@
import CalClick from '../../../../directives/Calendar/Click.js';
export default {
name: "LabelDay",
directives: {
CalClick
},
props: {
date: {
type: luxon.DateTime,
required: true
}
},
computed: {
titleFull() {
return this.date.toLocaleString({day: 'numeric', month: 'long', year: 'numeric'});
},
titleLong() {
return this.date.toLocaleString({day: '2-digit', month: '2-digit', year: 'numeric'});
},
titleShort() {
return this.date.toLocaleString({day: 'numeric', month: 'numeric'});
},
titleNarrow() {
return this.date.toLocaleString({day: 'numeric'});
}
},
template: /* html */`
<div
class="fhc-calendar-base-label-day"
v-cal-click:day="date"
>
<span class="full">{{ titleFull }}</span>
<span class="long">{{ titleLong }}</span>
<span class="short">{{ titleShort }}</span>
<span class="narrow">{{ titleNarrow }}</span>
</div>
`
}
@@ -0,0 +1,35 @@
import CalClick from '../../../../directives/Calendar/Click.js';
export default {
name: "LabelDow",
directives: {
CalClick
},
props: {
date: {
type: luxon.DateTime,
required: true
}
},
computed: {
titleLong() {
return this.date.toLocaleString({weekday: 'long'});
},
titleShort() {
return this.date.toLocaleString({weekday: 'short'});
},
titleNarrow() {
return this.date.toLocaleString({weekday: 'narrow'});
}
},
template: /* html */`
<div
class="fhc-calendar-base-label-dow"
v-cal-click:dow="date"
>
<b class="long">{{ titleLong }}</b>
<b class="short">{{ titleShort }}</b>
<b class="narrow">{{ titleNarrow }}</b>
</div>
`
}
@@ -0,0 +1,58 @@
export default {
name: "LabelTime",
props: {
part: {
type: [luxon.Duration, Number, Object],
required: true,
validator(value) {
if (value instanceof Object) {
if (value instanceof luxon.Duration)
return true;
let start_ok = true;
let end_ok = true;
if (value.start) {
start_ok = (
value.start instanceof luxon.Duration
|| Number.isInteger(value.start)
);
}
if (value.end) {
end_ok = (
value.end instanceof luxon.Duration
|| Number.isInteger(value.end)
);
}
return start_ok && end_ok;
}
return true;
}
}
},
computed: {
sanitizedTimestamps() {
return this.part.start || this.part.end ? this.part : { start: this.part };
},
start() {
if (!this.sanitizedTimestamps.start)
return null;
return this.formatTime(this.sanitizedTimestamps.start);
},
end() {
if (!this.sanitizedTimestamps.end)
return null;
return this.formatTime(this.sanitizedTimestamps.end);
}
},
methods: {
formatTime(date) {
return date.toISOTime({ suppressSeconds: true });
}
},
template: `
<div class="fhc-calendar-base-label-time">
<span v-if="start">{{ start }}</span>
<span v-if="end">-</span>
<span v-if="end">{{ end }}</span>
</div>
`
}
@@ -0,0 +1,38 @@
import CalClick from '../../../../directives/Calendar/Click.js';
export default {
name: "LabelWeek",
directives: {
CalClick
},
props: {
date: {
type: luxon.DateTime,
required: true
}
},
computed: {
weeks() {
const firstDay = this.date.startOf('week', { useLocaleWeeks: true });
const lastDay = this.date.endOf('week', { useLocaleWeeks: true });
const weeks = [
{ number: firstDay.localWeekNumber, year: firstDay.localWeekYear },
{ number: lastDay.localWeekNumber, year: lastDay.localWeekYear }
];
if (weeks[0].number == weeks[1].number)
weeks.pop();
return weeks;
}
},
template: `
<div class="fhc-calendar-base-label-week">
<span
v-for="week in weeks"
v-cal-click:week="week"
>
{{ week.number }}
</span>
</div>
`
}
@@ -0,0 +1,129 @@
export default {
name: 'CalendarSlider',
inject: {
time: {
from: "sliderTime",
default: ".3s"
}
},
emits: [
'slid'
],
data() {
return {
target: 0,
extrasAfter: 0,
extrasBefore: 0,
running: false,
promiseResolve: null
}
},
computed: {
itemsAfter() {
return [...Array(this.extrasAfter)].map((i, k) => 1+k);
},
itemsBefore() {
return [...Array(this.extrasBefore)].map((i, k) => k-this.extrasBefore);
},
styleSlider() {
const style = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%'
};
if (this.running) {
style.left = (-this.target * 100) + '%';
style.transition = 'left ' + this.time + ' ease-in-out';
}
return style;
},
styleBefore() {
return {
position: 'absolute',
top: 0,
height: '100%',
display: 'flex',
right: '100%',
width: (this.extrasBefore * 100) + '%'
};
},
styleAfter() {
return {
position: 'absolute',
top: 0,
height: '100%',
display: 'flex',
left: '100%',
width: (this.extrasAfter * 100) + '%'
};
}
},
methods: {
prevPage() {
return this.slidePages(-1);
},
nextPage() {
return this.slidePages(1);
},
slidePages(dir) {
return new Promise(resolve => {
this.promiseResolve = resolve;
this.running = true;
const newTarget = this.target + dir;
if (newTarget > 0) {
if (this.extrasAfter < newTarget)
this.extrasAfter = newTarget;
} else if (newTarget < 0) {
if (-this.extrasBefore > newTarget)
this.extrasBefore = -newTarget;
}
this.target = newTarget;
});
},
endSlide() {
if (this.promiseResolve) {
this.promiseResolve(this.target);
this.promiseResolve = null;
}
this.$emit('slid', this.target);
this.running = false;
this.target = 0;
this.extrasAfter = this.extrasBefore = 0;
}
},
template: /* html */`
<div
class="fhc-calendar-base-slider h-100"
style="position:relative;overflow:hidden"
>
<div
:style="styleSlider"
@transitionend="endSlide"
>
<div :style="styleBefore">
<div
v-for="i in itemsBefore"
:key="i"
style="height:100%;width:100%"
>
<slot :offset="i" />
</div>
</div>
<div :style="styleAfter">
<div
v-for="i in itemsAfter"
:key="i"
style="height:100%;width:100%"
>
<slot :offset="i" />
</div>
</div>
<div style="height:100%;width:100%">
<slot :offset="0" />
</div>
</div>
</div>
`
}
-269
View File
@@ -1,269 +0,0 @@
import CalendarMonth from './Month.js';
import CalendarMonths from './Months.js';
import CalendarYears from './Years.js';
import CalendarWeek from './Week.js';
import CalendarWeeks from './Weeks.js';
import CalendarDay from './Day.js';
import CalendarMinimized from './Minimized.js';
import CalendarDate from '../../composables/CalendarDate.js';
import CalendarDates from '../../composables/CalendarDates.js';
const todayDate = new Date(new Date().setHours(0, 0, 0, 0));
const today = todayDate.getTime()
export default {
components: {
CalendarMonth,
CalendarMonths,
CalendarYears,
CalendarWeek,
CalendarWeeks,
CalendarDay,
CalendarMinimized,
},
provide() {
return {
today,
todayDate,
date: this.date,
focusDate: this.focusDate,
size: Vue.computed({ get: () => this.size }),
containerHeight: Vue.computed({ get: () => this.containerHeight }),
containerWidth: Vue.computed({ get: () => this.containerWidth }),
events: Vue.computed(() => this.eventsPerDay),
filteredEvents: Vue.computed(() => this.filteredEvents),
minimized: Vue.computed({ get: () => this.minimized, set: v => this.$emit('update:minimized', v) }),
showWeeks: this.showWeeks,
noMonthView: this.noMonthView,
noWeekView: this.noWeekView,
eventsAreNull: Vue.computed(() => this.events === null),
mode: Vue.computed(()=>this.mode),
selectedEvent: Vue.computed(() => this.selectedEvent),
setSelectedEvent: (event)=>{this.selectedEvent = event;},
};
},
props: {
events: Array,
initialDate: {
type: [Date, String],
default: new Date()
},
showWeeks: {
type: Boolean,
default: true
},
initialMode: {
type: String,
default: 'month'
},
minimized: Boolean,
noWeekView: Boolean,
noMonthView: Boolean
},
watch:{
mode(newVal) {
this.$emit('change:mode', newVal)
},
selectedEvent:{
handler(newSelectedEvent) {
this.$emit('selectedEvent', newSelectedEvent);
},
immediate: true,
},
// scroll to the first event if the html element was found
scrollTime({focusDate,scrollTime}){
// return early if the scrollTime is not set
if(!scrollTime) return;
// scroll the Stundenplan to the closest event
Vue.nextTick(()=>{
let previousScrollAnchor = document.getElementById('scroll' + (scrollTime - 1) + this.focusDate.d + this.focusDate.w)
let scrollAnchor = document.getElementById('scroll' + scrollTime + this.focusDate.d + this.focusDate.w);
if (previousScrollAnchor) {
previousScrollAnchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
else {
if (scrollAnchor) {
scrollAnchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
});
},
},
emits: [
'select:day',
'select:event',
'change:range',
'update:minimized',
'selectedEvent',
'change:offset'
],
data() {
return {
header: '',
prevMode: null,
currMode: null,
date: new CalendarDate(),
focusDate: new CalendarDate(),
size: 0,
containerWidth: 0,
containerHeight: 0,
selectedEvent:null,
}
},
computed: {
sizeClass() {
// mainly determines calendar font-size
return 'fhc-calendar-' + ['xs', 'sm', 'md', 'lg'][this.size];
},
mode: {
get() { return this.minimized ? 'minimized' : this.currMode; },
set(v) {
if (!v && this.prevMode) {
this.currMode = this.prevMode;
this.prevMode = null;
} else {
this.prevMode = this.currMode;
this.currMode = v;
}
}
},
eventsPerDay() {
if (!this.events)
return {};
return this.events.reduce((result, event) => {
let days = Math.ceil((event.end - event.start) / 86400000) || 1;
while (days-- > 0) {
let day = (new Date(event.start.getFullYear(), event.start.getMonth(), event.start.getDate() + days)).toDateString();
if (!result[day])
result[day] = [];
result[day].push(event);
}
return result;
}, {});
},
// returns the hour of the earliest event, used to scroll to the events in the calendar (week / day view)
scrollTime() {
// return the first beginning time of the filtered events
if(this.filteredEvents && Array.isArray(this.filteredEvents) && this.filteredEvents.length > 0)
{
let scrollTime = parseInt(this.filteredEvents.sort((a, b) => parseInt(a.beginn) - parseInt(b.beginn))[0].beginn);
// to ensure that the scrollTime watcher triggers even if the scrollTime doesn't change, it returns both the scrollTime and the focusDate
return { focusDate: this.focusDate, scrollTime };
}
// there is no event that matches the current view mode constraints
else
{
return { focusDate: this.focusDate, scrollTime: null };
}
},
// filters the events based on the current calendar view mode
// week view - filter events based on their week
// day view - filter events based on their day and week
// month view - does not filter the events
filteredEvents: function(){
if (this.events && Array.isArray(this.events) && this.events.length > 0) {
let filteredEvents = this.events.filter(event => {
let eventDate = new CalendarDate(new Date(event.datum));
if (this.mode == 'week' || this.mode == 'Week')
{
// week view filters the elements only for the same week
return this.focusDate.w == eventDate.w;
}
else if (this.mode == 'day' || this.mode == 'Day')
{
// day view filters the elements for the same day and the same week
return this.focusDate.d == eventDate.d && this.focusDate.w == eventDate.w;
}
else
{
// returns all the events, does not filter the events
return true;
}
})
return filteredEvents;
}
else
{
return null;
}
},
},
methods: {
setMode(mode) {
this.mode = mode
},
handleInput(day) {
this.$emit(day[0], day[1]);
},
},
created() {
const initMode = this.initialMode.toLowerCase()
const allowedInitialModes = ['day'];
if (!this.noWeekView)
allowedInitialModes.push('week');
if (!this.noMonthView)
allowedInitialModes.push('month');
this.mode = allowedInitialModes[allowedInitialModes.indexOf(initMode)] || allowedInitialModes.pop();
this.date.set(new Date(this.initialDate));
this.focusDate.set(this.date);
},
mounted() {
if (this.$refs.container) {
new ResizeObserver(entries => {
for (const entry of entries) {
const w = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width;
const h = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
// https://getbootstrap.com/docs/5.0/layout/breakpoints/
// bootstrap breakpoints watch window size and this function monitors container size of calendar itself.
// calendar is using bootstrap breakpoints which influence layout, which retriggers this function
// -> some width constellations will loop so we dont use values around bs5 breakpoints
// ['xs', 'sm', 'md', 'lg'][this.size]
if (w >= 600)
this.size = 3;
else if (w >= 350)
this.size = 2;
else if (w >= 250)
this.size = 1;
else
this.size = 0;
this.containerWidth = w
this.containerHeight = h
}
}).observe(this.$refs.container);
}
},
unmounted(){
CalendarDates.cleanup();
},
template: /*html*/`
<div ref="container" class="fhc-calendar card h-100" :class="sizeClass">
<component :is="'calendar-' + mode" @updateMode="mode = $event" @change:range="$emit('change:range',$event)"
@input="handleInput" @change:offset="$emit('change:offset', $event)">
<template #calendarDownloads>
<slot name="calendarDownloads" ></slot>
</template>
<template #monthPage="{event,day}">
<slot name="monthPage" :event="event" :day="day" ></slot>
</template>
<template #weekPage="{event,day}">
<slot name="weekPage" :event="event" :day="day" ></slot>
</template>
<template #dayPage="{event,day,mobile}">
<slot name="dayPage" :event="event" :day="day" :mobile="mobile"></slot>
</template>
<template #pageMobilContent="{lvMenu}">
<slot name="pageMobilContent" :lvMenu="lvMenu"></slot>
</template>
<template #pageMobilContentEmpty>
<slot name="pageMobilContentEmpty" ></slot>
</template>
<template #minimizedPage="{event,day}">
<slot name="minimizedPage" :event="event" :day="day"></slot>
</template>
</component>
</div>`
}
-64
View File
@@ -1,64 +0,0 @@
import CalendarAbstract from './Abstract.js';
import CalendarPane from './Pane.js';
import CalendarDayPage from './Day/Page.js';
import CalendarDate from '../../composables/CalendarDate.js';
export default {
mixins: [
CalendarAbstract
],
components: {
CalendarDayPage,
CalendarPane
},
computed: {
title() {
return this.focusDate.wYear + ' KW ' + this.focusDate.w;
}
},
methods: {
paneChanged(dir) {
let previousDate = new CalendarDate(this.focusDate);
this.focusDate.d += dir;
this.emitRangeChanged(previousDate);
},
emitRangeChanged(previousDate) {
this.$emit('change:range', { start: previousDate, end:this.focusDate });
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: 0, m: 0, d: -1 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 0, m: 0, d: 1 });
},
selectEvent(event) {
this.$emit('input', ['select:event', event]);
}
},
created() {
this.emitRangeChanged();
},
template: /*html*/`
<div class="fhc-calendar-day">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'week')">
<template #calendarDownloads>
<slot name="calendarDownloads"></slot>
</template>
</calendar-header>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-day-page :active="slot.active" :year="focusDate.y" :week="focusDate.w+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" >
<template #dayPage="{event,day,mobile}">
<slot name="dayPage" :event="event" :day="day" :mobile="mobile" ></slot>
</template>
<template #pageMobilContent="{lvMenu}">
<slot name="pageMobilContent" :lvMenu="lvMenu" ></slot>
</template>
<template #pageMobilContentEmpty>
<slot name="pageMobilContentEmpty" ></slot>
</template>
</calendar-day-page>
</calendar-pane>
</div>`
}
-434
View File
@@ -1,434 +0,0 @@
import CalendarDate from '../../../composables/CalendarDate.js';
import LvModal from "../../../components/Cis/Mylv/LvModal.js";
import ApiStundenplan from '../../../api/factory/stundenplan.js';
import ApiAddons from '../../../api/factory/addons.js';
function ggt(m, n) {
return n == 0 ? m : ggt(n, m % n);
}
function kgv(m, n) {
return (m * n) / ggt(m, n);
}
export default {
name: 'DayPage',
components: {
LvModal,
},
data() {
return {
hourPosition: null,
curHourPosition: null,
hourPositionTime: null,
lvMenu: null,
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
'events',
'noMonthView',
'filteredEvents',
'isSliding',
'calendarScrollTop',
'calendarClientHeight',
'setSelectedEvent',
'selectedEvent',
'rowMinHeight'
],
props: {
year: Number,
week: Number,
active: Boolean,
},
emits: [
'updateMode',
'page:back',
'page:forward',
'input'
],
watch: {
//TODO: on first render non of the day-page components are active and the watcher on selectedEvent does not fetch the lvMenu
//TODO: workaround is to watch the active state and refetch in case the lvMenu is empty
active: {
handler(value) {
if (value) {
if (!this.lvMenu) {
this.fetchLvMenu(this.selectedEvent);
}
}
},
immediate: true,
},
eventsPerDayAndHour: {
handler(newEvents) {
// if no event is selected, select the first event of the day
if (this.selectedEvent == null && newEvents[this.day.toDateString()]?.events.length > 0) {
let events = newEvents[this.day.toDateString()]?.events;
if (Array.isArray(events) && events.length > 0) {
this.setSelectedEvent(events[0].orig);
}
}
},
immediate: true
},
selectedEvent: {
handler(event) {
// return early if the day-page component is not the active carousel item
if (!this.active) {
return;
}
this.lvMenu = null;
this.fetchLvMenu(event);
},
immediate: true,
},
isSliding: {
handler(value) {
if (value) {
this.setSelectedEvent(null);
}
}
}
},
computed: {
overlayStyle() {
return {
'background-color': '#F5E9D7',
'position': 'absolute',
'pointer-events': 'none',
'z-index': 2,
height: this.getDayTimePercent + '%',
opacity: 0.5,
overflow: 'hidden'
}
},
pageHeaderStyle() {
return {
'z-index': 4,
'grid-template-columns': 'repeat(' + this.day.length + ', 1fr)',
'grid-template-rows': 1,
position: 'sticky',
top: 0,
}
},
dayText(){
if(!this.day)return {};
return {
heading: this.day.toLocaleString(this.$p.user_locale.value, { dateStyle: 'short' }),
tag: this.day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 2 ? 'narrow' : (this.size < 3 ? 'short' : 'long') }),
datum: this.day.toLocaleString(this.$p.user_locale.value, [{ day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { dateStyle: 'short' }][this.size]),
}
},
noLvStyle() {
return {
top: (this.calendarScrollTop + 100) + 'px',
position: 'absolute',
left: 0,
'text-align': 'center',
width: '100%',
'z-index': 1,
}
},
indicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '3.5rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.hourPosition + 'px',
left: 0,
right: 0,
}
},
curTime() {
const now = new Date();
return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
},
curIndicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '7rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.getDayTimePercent + '%',
left: 0,
right: 0,
}
},
noEventsCondition() {
return !this.isSliding && (this.filteredEvents?.length === 0 || !this.filteredEvents);
},
hours() {
// returns an array with elements starting at 7 and ending at 24
return [...Array(24).keys()].filter(hour => hour >= 7 && hour <= 24);
},
day() {
return new Date(this.focusDate.y, this.focusDate.m, this.focusDate.d);
},
eventsPerDayAndHour() {
// return early if the calendar pane is sliding
if (this.isSliding) return {};
const res = {};
let key = this.day.toDateString();
let nextDay = new Date(this.day);
nextDay.setDate(nextDay.getDate() + 1);
nextDay.setMilliseconds(nextDay.getMilliseconds() - 1);
let d = {events: [], lanes: 1};
if (this.events[key]) {
this.events[key].forEach(evt => {
let event = {
orig: evt,
lane: 1,
maxLane: 1,
start: evt.start < this.day ? this.day : evt.start,
end: evt.end > nextDay ? nextDay : evt.end,
shared: [],
setSharedMaxRecursive(doneItems) {
this.maxLane = Math.max(doneItems[0].maxLane, this.maxLane);
doneItems.push(this);
this.shared.filter(other => !doneItems.includes(other)).forEach(i => i.setSharedMaxRecursive(doneItems));
}
};
event.shared = d.events.filter(other => other.start < event.end && other.end > event.start);
event.shared.forEach(other => other.shared.push(event));
let occupiedLanes = event.shared.map(other => other.lane);
while (occupiedLanes.includes(event.lane))
event.lane++;
event.maxLane = Math.max(...[event.lane], ...occupiedLanes);
if (event.maxLane > 1) {
event.setSharedMaxRecursive([event]);
}
d.events.push(event);
});
d.lanes = d.events.map(e => e.maxLane).reduce((res, i) => kgv(res, i), 1);
}
res[key] = d;
return res;
},
smallestTimeFrame() {
return [30, 15, 10, 5][this.size];
},
lookingAtToday() {
return this.date.compare(this.todayDate)
},
getDayTimePercent() {
const now = new Date(Date.now())
const currentMinutes = now.getMinutes() + now.getHours() * 60
let timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
return timePercentage
}
},
methods: {
dayGridStyle(day) {
const styleObj = {
'grid-template-columns': '1 1fr',
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
}
if(this.date.compare(this.todayDate)) {
styleObj['backgroundImage'] = 'linear-gradient(to bottom, #F5E9D7 '+this.getDayTimePercent+'%, #FFFFFF '+this.getDayTimePercent+'%)'
styleObj['border-color'] = '#E8E8E8';
// styleObj.opacity = 0.5; // would opaque the whole column
}
return styleObj
},
fetchLvMenu(event) {
if (event && event.type == 'lehreinheit') {
this.$api
.call(ApiStundenplan.getLehreinheitStudiensemester(event.lehreinheit_id[0]))
.then(res => res.data)
.then(studiensemester_kurzbz => this.$api.call(
ApiAddons.getLvMenu(
event.lehreinheit_id,
studiensemester_kurzbz
)
))
.then(res => {
if (res.data) {
this.lvMenu = res.data;
}
});
}
},
hourGridIdentifier(hour) {
// this is the id attribute that is responsible to scroll the calender to the first event
return 'scroll' + hour + this.focusDate.d + this.week;
},
hourGridStyle(hour) {
return {
'pointer-events': 'none',
top: this.getAbsolutePositionForHour(hour),
left: 0,
right: 0,
'z-index': 0,
}
},
eventGridStyle(day, event) {
return {
'z-index': 1,
'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane,
'grid-column-end': 1 + event.lane * day.lanes / event.maxLane,
'grid-row-start': this.dateToMinutesOfDay(event.start),
'grid-row-end': this.dateToMinutesOfDay(event.end),
'background-color': event.orig.color,
'--test': this.dateToMinutesOfDay(event.end),
}
},
eventClick(evt) {
let event = evt.orig;
this.setSelectedEvent(event);
this.$emit('input', event);
},
calcHourPosition(event) {
let height = this.$refs.eventcontainer.getBoundingClientRect().height;
let top = this.$refs.eventcontainer.getBoundingClientRect().top;
let position = event.clientY - top;
// position percentage of total height
let timePercentage = (position / height) * 100;
// minute percentage of total minutes
let result = (this.hours.length * 60) * (timePercentage / 100);
// calculate time in float
let currentMinutes = ((result + (this.hours[0] * 60)) / 60);
// get hour part of time
let currentHour = Math.floor(currentMinutes);
// get float part of time
let minutePercentage = currentMinutes % currentHour;
// calculate minutes from float part of time
let minute = Math.round(60 * minutePercentage);
// convert minutes to 5 minute interval
if (minute % 5 != 0) {
minute = Math.round(minute / 5) * 5;
}
// in case the rounding made the minutes 60, increase the hour and reset the minutes
if (minute == 60) {
currentHour++;
minute = 0;
}
// ## after rounding the time to the nearest 5 Minute interval, we have to convert the time back to the relative position
// convert current time in minutes
currentMinutes = currentHour * 60 + minute;
// calculate the minutes percentage of the total minutes
timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
// calculate the relative position of the time percentage
position = height * (timePercentage / 100);
this.hourPosition = position;
// add padding to minutes that consist of only one digit
minute.toString().length == 1 ? minute = "0" + minute : minute;
this.hourPositionTime = currentHour + ":" + minute;
},
getAbsolutePositionForHour(hour) {
// used for the absolute positioning of the gutters of hours
return (100 / this.hours.length) * (hour - (24 - this.hours.length)) + '%';
},
changeToMonth(day) {
if (!this.noMonthView) {
this.date.set(day);
this.focusDate.set(day);
this.$emit('updateMode', 'month');
}
},
dateToMinutesOfDay(day) {
return Math.floor(((day.getHours() - 7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
}
},
template: /*html*/`
<div class="fhc-calendar-day-page h-100">
<div class="row m-0 h-100">
<div class="col-12 col-xl-6 p-0 h-100">
<div class="d-flex flex-column h-100">
<div ref="header" class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle">
<div type="button" class="flex-grow-1" :title="dayText.heading" @click.prevent="changeToMonth(day)">
<div class="fw-bold">{{dayText.tag}}</div>
<a href="#" class="small text-secondary text-decoration-none" >{{dayText.datum}}</a>
</div>
</div>
<div id="scroll g-0" style="height: 100%; overflow-y: scroll;">
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="hourPosition = null" >
<div :id="hourGridIdentifier(hour)" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border-top" :style="hourGridStyle(hour)"></div>
<Transition>
<div v-if="hourPosition && !noEventsCondition" class="position-absolute border-top small" :style="indicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
</div>
</Transition>
<Transition>
<div v-if="lookingAtToday && !noEventsCondition" class="position-absolute border-top small" :style="curIndicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{curTime}}</span>
</div>
</Transition>
<div>
<h1 v-if="noEventsCondition" class="m-0 text-secondary" ref="noEventsText" :style="noLvStyle">{{ $p.t('lehre/noLvFound') }}</h1>
<div :class="{'fhc-calendar-no-events-overlay':noEventsCondition, 'events':true}">
<div class="hours">
<div v-for="hour in hours" :style="'min-height:' + rowMinHeight " :key="hour" class="text-muted text-end small" :ref="'hour' + hour">{{hour}}:00</div>
</div>
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
<div v-if="lookingAtToday && !noEventsCondition" :style="overlayStyle"></div>
<div v-for="event in day.events" :key="event" :style="eventGridStyle(day,event)" v-contrast
:selected="event.orig == selectedEvent" class="fhc-entry mx-2 small rounded overflow-hidden" >
<!-- desktop version of the page template, parent receives slotProp mobile = false -->
<div class="d-none d-xl-block h-100 " @click.prevent="eventClick(event)">
<slot name="dayPage" :event="event" :day="day" :mobile="false">
<p>this is a slot placeholder</p>
</slot>
</div>
<!-- mobile version of the page template, parent receives slotProp mobile = true -->
<div class="d-block d-xl-none h-100" @click.prevent="eventClick(event)">
<slot name="dayPage" :event="event" :day="day" :mobile="true">
<p>this is a slot placeholder</p>
</slot>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-xl-block col-xl-6 p-4 d-none" style="max-height: 100%">
<div style="z-index:0; max-height: 100%" class="sticky-top d-flex justify-content-center align-items-center flex-column">
<div style="max-height: 100%; overflow-y:auto;" class="w-100">
<template v-if="selectedEvent && lvMenu">
<slot name="pageMobilContent" :lvMenu="lvMenu" >
<p>this is a slot placeholder</p>
</slot>
</template>
<template v-else-if="noEventsCondition">
<slot name="pageMobilContentEmpty" >
<h3>This is an slot placeholder</h3>
</slot>
</template>
<template v-else>
<div class="p-4 d-flex w-100 justify-content-center align-items-center">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
`
}
-73
View File
@@ -1,73 +0,0 @@
export default {
data(){
return{
selected: this.mode,
modes:{
day: { mode_bezeichnung: "day", icon: "fa-calendar-day" , condition:true},
week: { mode_bezeichnung: "week", icon: "fa-calendar-week", condition: !this.noWeekView },
month: { mode_bezeichnung: "month", icon: "fa-calendar-days", condition: !this.noMonthView },
},
headerPadding:null,
}
},
inject: [
'eventsAreNull',
'size',
'mode',
'noWeekView',
'noMonthView',
'containerWidth'
],
props: {
title: String
},
emits: [
'updateMode',
'prev',
'next',
'click'
],
computed: {
getHeaderClassSide() {
return this.containerWidth > 780 ? 'col-3' : 'col-12'
},
getHeaderClassMiddle() {
return this.containerWidth > 780 ? 'col-6' : 'col-12'
}
},
template: /*html*/`
<div class="calendar-header card-header w-100">
<div class="row align-items-center ">
<div :class="getHeaderClassSide" class="d-flex justify-content-center justify-content-md-start align-items-center">
<slot name="calendarDownloads"></slot>
</div>
<div :class="getHeaderClassMiddle" :style="{'padding-left':headerPadding}">
<div class="row align-items-center justify-content-center">
<div class="col-auto p-2">
<button class="btn btn-outline-secondary border-0" :class="{'btn-sm':!this.size}" @click="$emit('prev')"><i class="fa fa-chevron-left"></i></button>
</div>
<div class="justify-content-center text-center col-auto">
<div class="d-flex justify-content-center align-items-center">
<button class="btn btn-link link-secondary text-decoration-none" :class="{'btn-sm': !this.size}" @click="$emit('click')">
{{ title }}
<i v-if="eventsAreNull" class="fa fa-spinner fa-pulse"></i>
</button>
</div>
</div>
<div class="col-auto p-2">
<button class="btn btn-outline-secondary border-0" :class="{'btn-sm': !this.size}" @click="$emit('next')"><i class="fa fa-chevron-right"></i></button>
</div>
</div>
</div>
<div ref="viewButtons" v-if="!noWeekView && !noMonthView" :class="getHeaderClassSide" class="d-flex justify-content-center justify-content-md-end align-items-center" style="pointer-events: none;">
<div style="pointer-events: all;">
<div role="group" aria-label="Kalender Modus">
<button type="button" :class="{'active':mode_kurzbz.toLowerCase() === mode.toLowerCase()}" style="margin-right: 4px;" @click.prevent="$emit('updateMode',mode_kurzbz)" class="btn btn-outline-secondary" v-for="({mode_bezeichnung,icon,condition},mode_kurzbz) in modes">
<i v-if="condition" class="fa" :class="icon" ></i>
</button>
</div>
</div>
</div>
</div>
</div>`
}
+164
View File
@@ -0,0 +1,164 @@
import FhcCalendar from "./Base.js";
import ApiLvPlan from '../../api/factory/lvPlan.js';
import { useEventLoader } from '../../composables/EventLoader.js';
import ModeDay from './Mode/Day.js';
import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js';
export default {
name: "CalendarLvPlan",
components: {
FhcCalendar
},
inject: [
"renderers"
],
props: {
timezone: {
type: String,
required: true
},
date: {
type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local()
},
mode: {
type: String,
default: 'Week'
},
getPromiseFunc: {
type: Function,
required: true
}
},
emits: [
"update:date",
"update:mode",
"update:range"
],
data() {
return {
modes: {
day: Vue.markRaw(ModeDay),
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth)
},
modeOptions: {
day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound'))
},
week: {
collapseEmptyDays: false
}
},
teachingunits: null
};
},
computed: {
backgrounds() {
let now = luxon.DateTime.now().setZone(this.timezone);
if (this.mode == 'Month')
return [
{
class: 'background-past',
end: now.startOf('day')
}
];
return [
{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}
];
}
},
methods: {
eventStyle(event) {
if (!event.farbe)
return undefined;
return '--event-bg:#' + event.farbe;
},
updateRange(rangeInterval) {
this.rangeInterval = rangeInterval;
this.$emit('update:range', rangeInterval);
}
},
setup(props, context) {
const rangeInterval = Vue.ref(null);
const { events, lv } = useEventLoader(rangeInterval, props.getPromiseFunc);
Vue.watch(lv, newValue => {
context.emit('update:lv', newValue);
});
return {
rangeInterval,
events,
lv
};
},
created() {
this.$api
.call(ApiLvPlan.getStunden())
.then(res => {
return this.teachingunits = res.data.map(el => ({
id: el.stunde,
start: el.beginn,
end: el.ende
}));
});
},
template: /* html */`
<fhc-calendar
ref="calendar"
class="fhc-calendar-lvplan"
:date="date"
:modes="modes"
:mode-options="modeOptions"
:mode="mode"
:timezone="timezone"
:locale="$p.user_locale.value"
:events="events || []"
:backgrounds="backgrounds"
:time-grid="teachingunits"
show-btns
@update:date="(newDate, newMode) => $emit('update:date', newDate, newMode)"
@update:mode="(newMode, newDate) => $emit('update:mode', newMode, newDate)"
@update:range="updateRange"
>
<template v-slot="{ event, mode }">
<div
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
:type="mode == 'day' ? 'button' : undefined"
:style="eventStyle(event)"
>
<component
v-if="mode == 'event'"
:is="renderers[event.type]?.modalContent"
:event="event"
></component>
<component
v-else-if="mode == 'eventheader'"
:is="renderers[event.type]?.modalTitle"
:event="event"
></component>
<component
v-else
:is="renderers[event.type]?.calendarEvent"
:event="event"
></component>
</div>
</template>
<template #actions>
<slot />
</template>
</fhc-calendar>`
}
@@ -1,26 +0,0 @@
import CalendarAbstract from './Abstract.js';
export default {
mixins: [
CalendarAbstract
],
inject: [
'size',
'minimized',
'date',
],
data() {
return {
start: 0
}
},
methods: {
maximize() {
this.minimized = false;
}
},
template: `
<div class="fhc-calendar-minimized h-100 d-flex flex-column">
<slot name="minimizedPage"></slot>
</div>`
}
+101
View File
@@ -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: `
<div
class="fhc-calendar-mode-day flex-grow-1 position-relative"
>
<base-slider ref="slider" v-slot="slot">
<day-view
ref="view"
v-bind="viewAttrs(slot.offset)"
@request-modal-open="$emit('requestModalOpen', $event)"
@request-modal-close="$emit('requestModalClose', $event)"
>
<template v-slot="slot"><slot v-bind="slot" /></template>
</day-view>
</base-slider>
</div>
`
}
@@ -0,0 +1,175 @@
import CalendarGrid from '../../Base/Grid.js';
import LabelDay from '../../Base/Label/Day.js';
import LabelDow from '../../Base/Label/Dow.js';
import LabelTime from '../../Base/Label/Time.js';
import { useResizeObserver } from '../../../../composables/Responsive.js';
export default {
name: "DayView",
components: {
CalendarGrid,
LabelDay,
LabelDow,
LabelTime
},
inject: {
timeGrid: "timeGrid",
originalEvents: "events",
timezone: "timezone"
},
props: {
day: {
type: luxon.DateTime,
required: true
},
emptyMessage: String,
emptyMessageDetails: String
},
emits: [
"requestModalOpen",
"requestModalClose"
],
data() {
return {
chosenEvent: null,
gridMainRef: null
};
},
computed: {
axisMain() {
return [this.day.startOf('day')];
},
axisParts() {
if (this.timeGrid) {
// create {start, end} array
return this.timeGrid.map(tu => {
return {
start: luxon.Duration.fromISOTime(tu.start),
end: luxon.Duration.fromISOTime(tu.end)
};
});
} else {
// create 07:00-23:00
return Array.from({ length: 17 }, (e, i) => luxon.Duration.fromObject({ hours: i + 7 }));
}
},
events() {
return this.originalEvents
.filter(event => event.start < this.day.plus({ days: 1 }) && event.end > this.day)
.sort((a, b) => a.start.ts - b.start.ts)
.map(evt => evt.orig);
},
currentEvent() {
if (this.chosenEvent) {
if (this.events.find(e => e == this.chosenEvent))
return this.chosenEvent;
}
let first = null;
if (this.events)
first = this.events.find(Boolean); // undefined => none found
if (first && first.type == 'loading')
return null; // null => loading
return first;
},
isToday() {
return this.day.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
}
},
watch: {
compact() {
if (this.compact) {
if (this.chosenEvent) {
this.$emit('requestModalOpen', {
event: this.chosenEvent,
closeFn: () => { this.chosenEvent = null; }
});
}
} else {
this.$emit('requestModalClose');
}
}
},
methods: {
handleClickDefaults(evt) {
if (evt.detail.source == 'event') {
this.chosenEvent = evt.detail.value;
if (this.compact) {
this.$emit('requestModalOpen', {
event: this.chosenEvent,
closeFn: () => { this.chosenEvent = null; }
});
}
}
}
},
setup() {
const container = Vue.ref(null); // use useTemplateRef when updating to Vue 3.5
const { compact } = useResizeObserver(container, 750);
return {
container, // must be exposed or it won't be set in Vue < 3.5
compact
};
},
mounted() {
this.gridMainRef = this.$refs.grid.$refs.main;
},
template: /* html */`
<div
ref="container"
class="fhc-calendar-mode-day-view d-flex h-100"
@cal-click-default.capture="handleClickDefaults"
>
<calendar-grid
ref="grid"
:axis-main="axisMain"
:axis-parts="axisParts"
:snap-to-grid="!!timeGrid"
all-day-events
>
<template #main-header="{ date }">
<div :class="{ today: isToday }">
<label-dow
@cal-click="evt => evt.detail.source = 'day'"
v-bind="{ date }"
/>
<label-day
v-bind="{ date }"
/>
</div>
</template>
<template #part-header="{ part }">
<label-time v-bind="{ part }" />
</template>
<template #event="slot">
<div v-if="slot.event.type == 'loading'" class="placeholder-glow h-100 opacity-50">
<span class="placeholder w-100 h-100" />
</div>
<slot v-else v-bind="slot" mode="day" />
</template>
</calendar-grid>
<Teleport :disabled="!gridMainRef" :to="gridMainRef">
<div
v-if="emptyMessage && currentEvent !== null && !currentEvent"
class="fhc-calendar-no-events-overlay"
style="position:absolute;inset:0"
>
{{ emptyMessage }}
</div>
</Teleport>
<div class="event-details" v-if="!compact">
<div
v-if="currentEvent === null"
class="p-4 d-flex w-100 justify-content-center align-items-center"
>
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<h3 v-else-if="!currentEvent">{{ emptyMessageDetails }}</h3>
<slot v-else :event="currentEvent" mode="event" />
</div>
</div>
`
}
+103
View File
@@ -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: `
<div
class="fhc-calendar-mode-list flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults"
>
<base-slider ref="slider" v-slot="slot">
<list-view v-bind="viewAttrs(slot.offset)">
<template v-slot="slot"><slot v-bind="slot" /></template>
</list-view>
</base-slider>
</div>
`
}
@@ -0,0 +1,98 @@
import LabelDay from '../../Base/Label/Day.js';
import LabelDow from '../../Base/Label/Dow.js';
import CalDnd from '../../../../directives/Calendar/DragAndDrop.js';
import CalClick from '../../../../directives/Calendar/Click.js';
// TODO(chris): drag and drop
export default {
name: "ListView",
components: {
LabelDay,
LabelDow
},
directives: {
CalDnd,
CalClick
},
inject: {
draggableEvents: "draggableEvents",
events: "events",
timezone: "timezone"
},
props: {
day: {
type: luxon.DateTime,
required: true
},
length: {
type: Number,
required: true
}
},
data() {
return {
chosenEvent: null
};
},
computed: {
days() {
return Array.from({ length: this.length }, (e, days) => this.day.plus({ days }));
},
eventsPerDay() {
const eventsPerDay = this.days.map(day => {
return {
day,
events: this.events
.filter(event => event.start < day.plus({ days: 1 }) && event.end > day)
.sort((a, b) => a.start.ts - b.start.ts)
};
});
return eventsPerDay.filter(day => day.events.length);
}
},
methods: {
draggable(event) {
return this.draggableEvents(event.orig, 'list');
},
isToday(date) {
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
}
},
template: /* html */`
<div class="fhc-calendar-mode-list-view h-100 overflow-auto">
<div v-if="!eventsPerDay.length" class="h-100">
<slot :event="undefined" mode="list" />
</div>
<div v-for="{ day, events } in eventsPerDay">
<div
class="text-center"
:class="{ today: isToday(day) }"
>
<label-dow
:date="day"
class="d-inline"
@cal-click="evt => evt.detail.source = 'day'"
/>
,
<label-day :date="day" class="d-inline" />
</div>
<div v-for="event in events">
<div v-if="event.type == 'loading'" class="placeholder-glow opacity-50">
<span class="placeholder w-100" />
</div>
<div
v-else
class="event"
:draggable="draggable(event)"
v-cal-dnd:draggable="event"
v-cal-click:event="event.orig"
>
<slot :event="event.orig" mode="list" />
</div>
</div>
</div>
</div>
`
}
+126
View File
@@ -0,0 +1,126 @@
import BaseSlider from '../Base/Slider.js';
import MonthView from './Month/View.js';
export default {
name: "ModeMonth",
components: {
BaseSlider,
MonthView
},
props: {
currentDate: {
type: luxon.DateTime,
required: true
}
},
emits: [
"update:currentDate",
"update:range",
"click",
"requestModalOpen"
],
data() {
return {
focusDate: this.currentDate,
rangeOffset: 0
};
},
computed: {
range() {
let first = this.focusDate.startOf('month').startOf('week', { useLocaleWeeks: true });
let last = first.plus({ days: 41 }).endOf('day'); // NOTE(chris): 6 weeks minus 1 day
if (this.rangeOffset != 0) {
const nextFocusDate = this.focusDate.plus({ months: this.rangeOffset});
const nextRangeStart = nextFocusDate.startOf('month').startOf('week', { useLocaleWeeks: true });
if (this.rangeOffset < 0) {
first = nextRangeStart;
} else {
last = nextRangeStart.plus({ days: 41 }).endOf('day');
}
}
return luxon.Interval.fromDateTimes(first, last);
}
},
watch: {
currentDate() {
if (this.currentDate.locale != this.focusDate.locale) {
this.focusDate = this.currentDate;
this.$emit('update:range', this.range);
} else {
this.rangeOffset = this.currentDate.startOf('month').diff(this.focusDate.startOf('month'), 'months').months;
if (this.rangeOffset) {
this.$emit('update:range', this.range);
this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
}
}
}
},
methods: {
prevPage() {
this.rangeOffset = this.$refs.slider.target - 1;
this.$emit('update:range', this.range);
this.$refs.slider.prevPage().then(this.updatePage);
},
nextPage() {
this.rangeOffset = this.$refs.slider.target + 1;
this.$emit('update:range', this.range);
this.$refs.slider.nextPage().then(this.updatePage);
},
updatePage(months) {
const newFocusDate = this.focusDate.plus({ months });
this.focusDate = newFocusDate;
this.rangeOffset = 0;
this.$emit('update:currentDate', this.focusDate);
this.$emit('update:range', this.range);
},
viewAttrs(months) {
const day = this.focusDate.plus({ months });
return { day };
},
handleClickDefaults(evt) {
switch (evt.detail.source) {
case 'week':
// default: Move to week if not in month
let dayInWeek = luxon.DateTime.fromObject({
localWeekNumber: evt.detail.value.number,
localWeekYear: evt.detail.value.year
}, {
zone: this.currentDate.zoneName,
locale: this.currentDate.locale
});
if (!this.focusDate.hasSame(dayInWeek.startOf('week', { useLocaleWeeks: true }), 'month')) {
this.$emit('update:currentDate', dayInWeek.startOf('week', { useLocaleWeeks: true }));
} else if (!this.focusDate.hasSame(dayInWeek.endOf('week', { useLocaleWeeks: true }), 'month')) {
this.$emit('update:currentDate', dayInWeek.endOf('week', { useLocaleWeeks: true }));
}
break;
case 'day':
// default: Set current-date
this.$emit('update:currentDate', evt.detail.value);
break;
case 'event':
// default: Request Modal
this.$emit('requestModalOpen', { event: evt.detail.value });
break;
}
}
},
mounted() {
this.$emit('update:range', this.range);
},
template: `
<div
class="fhc-calendar-mode-month flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults"
>
<base-slider ref="slider" v-slot="slot">
<month-view v-bind="viewAttrs(slot.offset)">
<template v-slot="slot"><slot v-bind="slot" mode="month" /></template>
</month-view>
</base-slider>
</div>
`
}
@@ -0,0 +1,95 @@
import CalendarGrid from '../../Base/Grid.js';
import LabelWeek from '../../Base/Label/Week.js';
import LabelDow from '../../Base/Label/Dow.js';
import LabelDay from '../../Base/Label/Day.js';
export default {
name: "MonthView",
components: {
CalendarGrid,
LabelWeek,
LabelDow,
LabelDay
},
provide() {
return {
// NOTE(chris): snap events to day
events: Vue.computed(() => {
//const events = [];
const events = this.events.map(event => {
const start = event.start.startOf('day');
const end = event.end.plus({ days: 1 }).startOf('day');
return {
...event,
start,
end
};
});
for (var w = 5; w > -1; w--) {
for (var d = 6; d > -1; d--) {
const startdate = this.axisMain[w].plus(this.axisParts[d]);
events.unshift({
start: startdate,
end: startdate.plus({ days: 1 }),
orig: 'header'
});
}
}
return events;
})
};
},
inject: {
events: "events",
timezone: "timezone"
},
props: {
day: {
type: luxon.DateTime,
required: true
}
},
computed: {
axisMain() {
const start = this.day.startOf('month').startOf('week', { useLocaleWeeks: true });
return Array.from({ length: 6 }, (e, i) => start.plus({ weeks: i }));
},
axisParts() {
return Array.from({ length: 8 }, (e, i) => luxon.Duration.fromObject({ days: i }));
}
},
methods: {
isToday(date) {
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
}
},
template: /* html */`
<div class="fhc-calendar-mode-month-view h-100">
<calendar-grid
flip-axis
:axis-main="axisMain"
:axis-parts="axisParts"
snap-to-grid
>
<template #main-header="{ date }">
<label-week v-bind="{ date }" />
</template>
<template #part-header="{ part }">
<label-dow :date="axisMain[0].plus(part)" class="text-center" />
</template>
<template #event="slot">
<label-day
v-if="slot.event.orig == 'header'"
:date="slot.event.start"
:class="{ disabled: !day.hasSame(slot.event.start, 'month'), today: isToday(slot.event.start) }"
/>
<div v-else-if="slot.event.type == 'loading'" class="placeholder-glow opacity-50">
<span class="placeholder w-100 fs-1" />
</div>
<slot v-else v-bind="slot" />
</template>
</calendar-grid>
</div>
`
}
+113
View File
@@ -0,0 +1,113 @@
import BaseSlider from '../Base/Slider.js';
import WeekView from './Week/View.js';
export default {
name: "ModeWeek",
components: {
BaseSlider,
WeekView
},
props: {
currentDate: {
type: luxon.DateTime,
required: true
}
},
emits: [
"update:currentDate",
"update:range",
"click",
"requestModalOpen"
],
data() {
return {
focusDate: this.currentDate,
rangeOffset: 0
};
},
computed: {
range() {
let first = this.focusDate.startOf('week', { useLocaleWeeks: true });
let last = this.focusDate.endOf('week', { useLocaleWeeks: true });
if (this.rangeOffset != 0) {
if (this.rangeOffset < 0) {
first = first.plus({ weeks: this.rangeOffset });
} else {
last = last.plus({ weeks: this.rangeOffset });
}
}
return luxon.Interval.fromDateTimes(first, last);
}
},
watch: {
currentDate() {
if (this.currentDate.locale != this.focusDate.locale) {
this.focusDate = this.currentDate;
this.$emit('update:range', this.range);
} else {
this.rangeOffset = this.currentDate.startOf('week', { useLocaleWeeks: true }).diff(this.focusDate.startOf('week', { useLocaleWeeks: true }), 'weeks').weeks;
if (this.rangeOffset) {
this.$refs.view.$refs.grid.disableAutoScroll();
this.$emit('update:range', this.range);
this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
}
}
}
},
methods: {
prevPage() {
this.rangeOffset = this.$refs.slider.target - 1;
this.$refs.view.$refs.grid.disableAutoScroll();
this.$emit('update:range', this.range);
this.$refs.slider.prevPage().then(this.updatePage);
},
nextPage() {
this.rangeOffset = this.$refs.slider.target + 1;
this.$refs.view.$refs.grid.disableAutoScroll();
this.$emit('update:range', this.range);
this.$refs.slider.nextPage().then(this.updatePage);
},
updatePage(weeks) {
const newFocusDate = this.focusDate.plus({ weeks });
this.focusDate = newFocusDate;
this.rangeOffset = 0;
this.$emit('update:currentDate', this.focusDate);
this.$emit('update:range', this.range);
this.$refs.view.$refs.grid.enableAutoScroll();
},
viewAttrs(weeks) {
const day = this.focusDate.plus({ weeks });
return { ...this.$attrs, day };
},
handleClickDefaults(evt) {
switch (evt.detail.source) {
case 'day':
// default: Set current-date
this.$emit('update:currentDate', evt.detail.value);
break;
case 'event':
// default: Request Modal
this.$emit('requestModalOpen', { event: evt.detail.value });
break;
}
}
},
mounted() {
this.$emit('update:range', this.range);
this.$refs.view.$refs.grid.enableAutoScroll();
},
template: `
<div
class="fhc-calendar-mode-week flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults"
>
<base-slider ref="slider" v-slot="slot">
<week-view ref="view" v-bind="viewAttrs(slot.offset)">
<template v-slot="slot"><slot v-bind="slot" mode="week" /></template>
</week-view>
</base-slider>
</div>
`
}
@@ -0,0 +1,85 @@
import CalendarGrid from '../../Base/Grid.js';
import LabelDay from '../../Base/Label/Day.js';
import LabelDow from '../../Base/Label/Dow.js';
import LabelTime from '../../Base/Label/Time.js';
export default {
name: "WeekView",
components: {
CalendarGrid,
LabelDay,
LabelDow,
LabelTime
},
inject: {
timeGrid: "timeGrid",
timezone: "timezone"
},
props: {
day: {
type: luxon.DateTime,
required: true
},
collapseEmptyDays: Boolean
},
computed: {
start() {
return this.day.startOf('week', { useLocaleWeeks: true });
},
axisMain() {
return Array.from({ length: 7 }, (e, i) => this.start.plus({ days: i }));
},
axisParts() {
if (this.timeGrid) {
// create {start, end} array
return this.timeGrid.map(tu => {
return {
start: luxon.Duration.fromISOTime(tu.start),
end: luxon.Duration.fromISOTime(tu.end)
};
});
} else {
// create 07:00-23:00
return Array.from({ length: 17 }, (e, i) => luxon.Duration.fromObject({ hours: i + 7 }));
}
}
},
methods: {
isToday(date) {
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
}
},
template: /* html */`
<div class="fhc-calendar-mode-week-view h-100">
<calendar-grid
ref="grid"
:axis-main="axisMain"
:axis-parts="axisParts"
:axis-main-collapsible="collapseEmptyDays"
:snap-to-grid="!!timeGrid"
all-day-events
>
<template #main-header="{ date }">
<div :class="{ today: isToday(date) }">
<label-dow
v-bind="{ date }"
@cal-click="evt => evt.detail.source = 'day'"
/>
<label-day
v-bind="{ date }"
/>
</div>
</template>
<template #part-header="{ part }">
<label-time v-bind="{ part }" />
</template>
<template #event="slot">
<div v-if="slot.event.type == 'loading'" class="placeholder-glow h-100 opacity-50">
<span class="placeholder w-100 h-100" />
</div>
<slot v-else v-bind="slot" />
</template>
</calendar-grid>
</div>
`
}
-79
View File
@@ -1,79 +0,0 @@
import CalendarAbstract from './Abstract.js';
import CalendarPane from './Pane.js';
import CalendarMonthPage from './Month/Page.js';
export default {
mixins: [
CalendarAbstract
],
components: {
CalendarMonthPage,
CalendarPane
},
data() {
return {
syncOnNextChange: false
}
},
computed: {
title() {
return this.focusDate.format({month: ['short','long','long','long'][this.size], year: 'numeric'}, this.$p.user_locale.value);
}
},
methods: {
paneChanged(dir) {
if (this.syncOnNextChange) {
this.syncOnNextChange = false;
this.focusDate.set(this.date);
} else {
this.focusDate.moveMonthInDirection(dir)
}
this.$emit('change:range', {
start: new Date(this.focusDate.y, this.focusDate.m, 1),
end: new Date(this.focusDate.y, this.focusDate.m+1, 0)
});
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: 0, m: -1, d: 0 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 0, m: 1, d: 0 });
},
selectDay(day) {
let m = day.getMonth();
if (this.focusDate.m != m) {
this.syncOnNextChange = true;
if (this.focusDate.m-1 == m || (m == 11 && !this.focusDate.m))
this.$refs.pane.prev();
else
this.$refs.pane.next();
} else {
this.focusDate.set(this.date);
}
this.$emit('input', ['select:day',day])
}
},
created() {
this.$emit('change:range', {
start: new Date(this.focusDate.y, this.focusDate.m, 1),
end: new Date(this.focusDate.y, this.focusDate.m+1, 0)
});
},
template: `
<div class="fhc-calendar-month">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'months')" >
<template #calendarDownloads>
<slot name="calendarDownloads"></slot>
</template>
</calendar-header>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-month-page :year="focusDate.y" :month="focusDate.m+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectDay" >
<template #monthPage="{event,day}">
<slot name="monthPage" :event="event" :day="day" ></slot>
</template>
</calendar-month-page>
</calendar-pane>
</div>`
}
-166
View File
@@ -1,166 +0,0 @@
import CalendarDate from '../../../composables/CalendarDate.js';
export default {
name: 'MonthPage',
data(){
return{
highlightedWeek: null,
highlightedDay: null,
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
'events',
'showWeeks',
'noWeekView',
'selectedEvent',
'setSelectedEvent'
],
props: {
year: Number,
month: Number
},
emits: [
'updateMode',
'page:back',
'page:forward',
'input'
],
computed: {
dayText(){
if (!this.size || !this.weeks[0]?.days) return {};
let dayTextMap ={};
this.weeks[0].days.forEach((day)=>{
dayTextMap[day] = day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 1 ? 'narrow' : (this.size < 3 ? 'short' : 'long') });
});
return dayTextMap;
},
weeks() {
let firstDayOfMonth = new CalendarDate(this.year, this.month, 1);
let startDay = firstDayOfMonth.firstDayOfCalendarMonth;
let endDay = firstDayOfMonth.lastDayOfCalendarMonth;
let res = [];
let week = {no:0,y:0,days:[]};
while (startDay <= endDay) {
week.days.push(new Date(startDay));
if (week.days.length == 7) {
let d = new CalendarDate(week.days[5]);
week.no = d.w;
week.y = d.y;
res.push(week);
week = {no:0,y:0,days:[]};
}
startDay.setDate(startDay.getDate() + 1);
}
return res;
},
},
methods: {
getDayClass(week, day) {
let classstring = 'fhc-calendar-month-page-day text-decoration-none overflow-hidden'
const isHighlightedWeek = this.isHighlightedWeek(week)
const isHighlightedDay = this.isHighlightedDay(day)
const isThisDate = this.date.compare(day)
const isNotThisMonth = day.getMonth() != this.month
const isInThePast = day.getTime() < this.today // this.date is just the focusDate but not the initial Date
if(isHighlightedWeek) classstring += ' fhc-highlight-week'
if(isHighlightedDay) classstring += ' fhc-highlight-day'
if(isNotThisMonth) classstring += ' opacity-25'
if(isInThePast) classstring += ' fhc-calendar-past'
return classstring
},
selectDay(day) {
this.date.set(day);
this.$emit('input', day);
},
changeToWeek(week) {
if (!this.noWeekView) {
if (!this.focusDate.isInWeek(week.no, week.y))
this.focusDate.set(week.days[0]);
this.$emit('updateMode', 'week');
}
},
highlight(week, day){
this.highlightedWeek = week.no;
this.highlightedDay = day;
},
isHighlightedDay(day) {
return day == this.highlightedDay
},
isHighlightedWeek(week) {
return week.no == this.highlightedWeek
},
clickEvent(day,week) {
if(!this.noWeekView)
{
this.focusDate.set(day);
this.$emit('updateMode', 'day');
}
this.selectDay(day);
},
getNumberStyle(day) {
const styleObj = {}
styleObj.display = 'inline-block';
styleObj.height = '32px';
styleObj['line-height'] = '32px';
styleObj['text-align'] = 'center';
styleObj['font-weight'] = 'bold';
styleObj['font-size'] = '14px';
if(day.getDate() === this.todayDate.getDate()
&& day.getMonth() === this.todayDate.getMonth()
&& day.getFullYear() === this.todayDate.getFullYear()) {
styleObj['background-color'] = '#00649c'; // fh blau
styleObj.color = 'white';
}
return styleObj
}
},
mounted() {
const container = document.getElementById("calendarContainer")
if(container) container.style['overflow-y'] = 'auto'
},
template: /*html*/`
<div class="fhc-calendar-month-page" :class="{'show-weeks': showWeeks}">
<div v-if="showWeeks" class=" bg-light fw-bold border-top border-bottom text-center"></div>
<div v-for="day in weeks[0].days" :key="day" class="bg-light fw-bold border-top border-bottom text-center">
{{dayText[day]}}
</div>
<template v-for="week in weeks"
:key="week.no">
<a href="#" v-if="showWeeks" class="fhc-calendar-month-page-weekday text-decoration-none text-end opacity-25"
@click.prevent="changeToWeek(week)">{{week.no}}</a>
<a href="#"
@click="clickEvent(day,week)"
@mouseover="highlight(week,day)"
@mouseleave="highlightedWeek = null; highlightedDay = null"
v-for="day in week.days"
:key="day"
:class="getDayClass(week, day)"
>
<span @click="clickEvent(day,week)" class="no" :style="getNumberStyle(day)">{{day.getDate()}}</span>
<span v-if="events[day.toDateString()] && events[day.toDateString()].length" class="events">
<div v-for="event in events[day.toDateString()]" :key="event.id"
:style="{'background-color': event.color}" class="fhc-entry" :selected="event == selectedEvent" v-contrast >
<slot name="monthPage" :event="event" :day="day" >
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
</slot>
</div>
</span>
</a>
</template>
</div>
`
}
-34
View File
@@ -1,34 +0,0 @@
import CalendarAbstract from './Abstract.js';
export default {
mixins: [
CalendarAbstract
],
inject: [
'size'
],
data() {
return {
monthIndices: [...Array(12).keys()]
}
},
computed: {
title() {
return this.focusDate.format({year: 'numeric'});
},
months() {
return this.monthIndices.map(i => (new Date(0, i, 1)).toLocaleString(this.$p.user_locale.value, {month: this.size < 2 ? 'short' : 'long'}));
}
},
template: `
<div class="fhc-calendar-months">
<calendar-header :title="title" @prev="focusDate.y--" @next="focusDate.y++" @click="$emit('updateMode', 'years')" @updateMode="$emit('updateMode', $event)" />
<div class="d-flex flex-wrap">
<div v-for="(month, key) in months" :key="key" class="d-grid col-4">
<button @click="focusDate.m = key; $emit('updateMode', 'month')" class="btn btn-outline-secondary" :class="{'border-0': key != focusDate.m}">
{{month}}
</button>
</div>
</div>
</div>`
}
-91
View File
@@ -1,91 +0,0 @@
export default {
name: 'Pane',
emits: [
'slid'
],
data() {
return {
carousel: null,
queue: 0,
offset: 0,
slideAnimation:false,
scrollTop:null,
clientHeight:null,
carouselItems:null,
}
},
provide() {
return {
isSliding: Vue.computed(() => this.slideAnimation),
calendarScrollTop: Vue.computed(() =>this.scrollTop),
calendarClientHeight: Vue.computed(() => this.clientHeight),
}
},
computed: {
offsets() {
return [...Array(3).keys()].map(i => (3+i-this.offset)%3-1);
},
activeCarouselItemIndex() {
if (Array.isArray(this.carouselItems) && this.carouselItems.length > 0) {
for(let index=0; index < this.carouselItems.length; index++){
if (this.carouselItems[index] == true){
return index;
}
}
}
return -1;
}
},
methods: {
scrollCalendar(event){
this.scrollTop = this.$refs.calendarContainer.scrollTop;
this.clientHeight = this.$refs.calendarContainer.clientHeight;
},
prev() {
if (!this.queue--)
this.carousel.prev();
},
next() {
if (!this.queue++)
this.carousel.next();
},
slid(evt) {
let dir = evt.direction == 'left' ? 1 : -1;
this.queue -= dir;
this.$emit('slid', dir);
this.offset = (3+this.offset+dir)%3;
if (this.queue) {
if (this.queue > 0)
this.carousel.next();
else
this.carousel.prev();
}
this.carouselItems = this.$refs.carouselItems.map((item) => { return item.classList.contains('active') });
this.slideAnimation = false;
},
slide(evt) {
this.slideAnimation = true;
}
},
mounted() {
if (this.$refs.carousel) {
this.$refs.carousel.children[0].children[1].classList.add('active');
this.carousel = new window.bootstrap.Carousel(this.$refs.carousel, {
interval: false
});
}
this.carouselItems = this.$refs.carouselItems.map((item)=>{return item.classList.contains('active')});
this.scrollTop = this.$refs.calendarContainer.scrollTop;
this.clientHeight = this.$refs.calendarContainer.clientHeight;
},
template: `
<div ref="carousel" class="calendar-pane carousel slide" @[\`slide.bs.carousel\`]="slide" @[\`slid.bs.carousel\`]="slid" :data-queue="queue">
<div id="calendarContainer" @scroll="scrollCalendar" ref="calendarContainer" class="carousel-inner " style="height:var(--fhc-calendar-pane-height);">
<div ref="carouselItems" v-for="i in [...Array(3).keys()]" :key="i" class="carousel-item" style="height:var(--fhc-calendar-pane-height);">
<slot :active="i == activeCarouselItemIndex" :index="i" :offset="offsets[i]" />
</div>
</div>
</div>`
}
-58
View File
@@ -1,58 +0,0 @@
import CalendarAbstract from './Abstract.js';
import CalendarPane from './Pane.js';
import CalendarWeekPage from './Week/Page.js';
export default {
mixins: [
CalendarAbstract
],
components: {
CalendarWeekPage,
CalendarPane
},
computed: {
title() {
return this.focusDate.wYear + ' KW ' + this.focusDate.w;
}
},
methods: {
paneChanged(dir) {
this.focusDate.d += dir * 7;
this.emitRangeChanged();
},
emitRangeChanged() {
let start = this.focusDate.firstDayOfWeek;
let end = this.focusDate.lastDayOfWeek;
this.$emit('change:range', { start, end });
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: 0, m: 0, d: -7 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 0, m: 0, d: 7 });
},
selectEvent(event) {
this.$emit('input', ['select:event',event]);
}
},
created() {
this.emitRangeChanged();
},
template: /*html*/`
<div class="fhc-calendar-week">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'weeks')">
<template #calendarDownloads>
<slot name="calendarDownloads"></slot>
</template>
</calendar-header>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-week-page :year="focusDate.wYear" :week="focusDate.w+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" >
<template #weekPage="{event,day}">
<slot name="weekPage" :event="event" :day="day" ></slot>
</template>
</calendar-week-page>
</calendar-pane>
</div>`
}
-368
View File
@@ -1,368 +0,0 @@
import CalendarDate from '../../../composables/CalendarDate.js';
function ggt(m,n) { return n==0 ? m : ggt(n, m%n); }
function kgv(m,n) { return (m*n) / ggt(m,n); }
export default {
name: 'WeekPage',
data(){
return{
hourPosition:null,
hourPositionTime:null,
resizeObserver: null,
width: 0
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
'events',
'noMonthView',
'isSliding',
'selectedEvent',
'setSelectedEvent',
'rowMinHeight'
],
props: {
year: Number,
week: Number
},
emits: [
'updateMode',
'page:back',
'page:forward',
'input',
],
computed: {
getGridStyle() {
return {
'min-height': this.rowMinHeight,
// this.size is the magic number anyway which directs font-size,
// which in turn influences a lot of layout
width: '42px'
}
},
laneWidth() {
return (this.width - 42) / this.days.length
},
curTime() {
const now = new Date();
return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
},
pageHeaderStyle(){
return {
'z-index': 4,
'grid-template-columns': 'repeat(' + this.days.length + ', 1fr)',
'grid-template-rows': 1,
position: 'sticky',
top: 0,
}
},
overlayStyle() {
return {
'background-color': '#F5E9D7',
'position': 'absolute',
'pointer-events': 'none',
'z-index': 2,
height: this.getDayTimePercent + '%',
width: this.laneWidth + 'px',
opacity: 0.5
}
},
indicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '3.5rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.hourPosition + 'px',
left: 0,
right: 0,
}
},
hours(){
// returns an array with elements starting at 7 and ending at 24
return [...Array(24).keys()].filter(hour => hour >= 7 && hour <= 24);
},
dayText() {
if (!this.size || !this.days) return {};
let dayTextMap ={};
this.days.forEach((day)=>{
dayTextMap[day] = {
heading: day.toLocaleString(this.$p.user_locale.value, { dateStyle: 'short' }),
tag: day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 2 ? 'narrow' : (this.size < 3 ? 'short' : 'long') }),
datum: day.toLocaleString(this.$p.user_locale.value, [{ day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { dateStyle: 'short' }][this.size]),
};
});
return dayTextMap;
},
days() {
let tmpDate = new CalendarDate(this.year,1,1); // NOTE(chris): somewhere in the middle of the year
tmpDate.w = this.week;
let startDay = tmpDate.firstDayOfWeek;
let result = [];
for (let i = 0; i < 7; i++) {
result.push(new Date(startDay.getFullYear(), startDay.getMonth(), startDay.getDate() + i));
}
return result;
},
eventsPerDayAndHour() {
// return early if the calendar pane is sliding
if (this.isSliding) return {};
const res = {};
this.days.forEach(day => {
let key = day.toDateString();
let nextDay = new Date(day);
nextDay.setDate(nextDay.getDate()+1);
nextDay.setMilliseconds(nextDay.getMilliseconds()-1);
let d = {events:[],lanes:1, isPast: false};
d.isPast = nextDay.getTime() < this.today
d.isToday = nextDay.getFullYear() === this.todayDate.getFullYear() && nextDay.getMonth() === this.todayDate.getMonth() && nextDay.getDate() === this.todayDate.getDate()
if (this.events[key]) {
this.events[key].forEach(evt => {
let event = {orig:evt,lane:1,maxLane:1,start: evt.start < day ? day : evt.start, end: evt.end > nextDay ? nextDay : evt.end,shared:[],setSharedMaxRecursive(doneItems) {
this.maxLane = Math.max(doneItems[0].maxLane, this.maxLane);
doneItems.push(this);
this.shared.filter(other => !doneItems.includes(other)).forEach(i => i.setSharedMaxRecursive(doneItems));
}};
event.shared = d.events.filter(other => other.start < event.end && other.end > event.start);
event.shared.forEach(other => other.shared.push(event));
let occupiedLanes = event.shared.map(other => other.lane);
while (occupiedLanes.includes(event.lane))
event.lane++;
event.maxLane = Math.max(...[event.lane], ...occupiedLanes);
if (event.maxLane > 1) {
event.setSharedMaxRecursive([event]);
}
d.events.push(event);
});
d.lanes = d.events.map(e => e.maxLane).reduce((res, i) => kgv(res, i), 1);
}
res[key] = d;
});
return res;
},
smallestTimeFrame() {
return [30,15,10,5][this.size];
},
lookingAtToday() {
return this.days.some(d =>
d.getFullYear() === this.todayDate.getFullYear() &&
d.getMonth() === this.todayDate.getMonth() &&
d.getDate() === this.todayDate.getDate()
)
},
curIndicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '1rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.getDayTimePercent + '%',
width: this.laneWidth + 'px'
}
},
getDayTimePercent() {
const now = new Date(Date.now())
const currentMinutes = now.getMinutes() + now.getHours() * 60
let timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
return timePercentage
}
},
methods: {
hourGridIdentifier(hour) {
// this is the id attribute that is responsible to scroll the calender to the first event
return 'scroll' + hour + this.focusDate.d + this.week;
},
hourGridStyle(hour) {
return {
'pointer-events': 'none',
top: this.getAbsolutePositionForHour(hour),
left: 0,
right: 0,
'z-index': 0
}
},
dayGridStyle(day) {
const styleObj = {
'grid-template-columns': 'repeat(' + day.lanes + ', 1fr)',
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
}
if(day.isPast) {
styleObj['background-color'] = '#F5E9D7'
styleObj['border-color'] = '#E8E8E8';
styleObj.opacity = 0.5;
} else if (day.isToday) {
// styleObj['backgroundImage'] = 'linear-gradient(to bottom, #F5E9D7 '+this.getDayTimePercent+'%, #FFFFFF '+this.getDayTimePercent+'%)'
// styleObj['border-color'] = '#E8E8E8';
// styleObj.opacity = 0.5;
}
return styleObj
},
eventGridStyle(day, event) {
return {
'z-index': 1,
'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane,
'grid-column-end': 1 + event.lane * day.lanes / event.maxLane,
'grid-row-start': this.dateToMinutesOfDay(event.start),
'grid-row-end': this.dateToMinutesOfDay(event.end),
'background-color': event.orig.color,
'max-height': '75px'
}
},
calcHourPosition(event) {
let height = this.$refs.eventcontainer.getBoundingClientRect().height;
let top = this.$refs.eventcontainer.getBoundingClientRect().top;
let position = event.clientY - top;
// position percentage of total height
let timePercentage = (position / height) * 100;
// minute percentage of total minutes
let result = (this.hours.length * 60) * (timePercentage / 100);
// calculate time in float
let currentMinutes = ((result + (this.hours[0] * 60)) / 60);
// get hour part of time
let currentHour = Math.floor(currentMinutes);
// get float part of time
let minutePercentage = currentMinutes % currentHour;
// calculate minutes from float part of time
let minute = Math.round(60 * minutePercentage);
// convert minutes to 5 minute interval
if (minute % 5 != 0) {
minute = Math.round(minute / 5) * 5;
}
// in case the rounding made the minutes 60, increase the hour and reset the minutes
if (minute == 60) {
currentHour++;
minute = 0;
}
// ## after rounding the time to the nearest 5 Minute interval, we have to convert the time back to the relative position
// convert current time in minutes
currentMinutes = currentHour * 60 + minute;
// calculate the minutes percentage of the total minutes
timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
// calculate the relative position of the time percentage
position = height * (timePercentage / 100);
this.hourPosition = position;
// add padding to minutes that consist of only one digit
minute.toString().length == 1 ? minute = "0" + minute : minute;
this.hourPositionTime = currentHour + ":" + minute;
},
getAbsolutePositionForHour(hour){
// used for the absolute positioning of the gutters of hours
return (100 / this.hours.length) * (hour - (24-this.hours.length)) + '%';
},
changeToMonth(day) {
if (!this.noMonthView) {
this.date.set(day);
this.focusDate.set(day);
this.$emit('updateMode', 'month');
}
},
changeToDay(day) {
this.date.set(day);
this.focusDate.set(day);
this.$emit('updateMode', 'day');
},
dateToMinutesOfDay(day) {
// subtract 7 from the total hours because the hours range from 7 to 24
return Math.floor(((day.getHours()-7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
},
weekPageClick(event, day) {
this.setSelectedEvent(event);
this.focusDate.set(new CalendarDate(new Date(event.datum)));
this.$emit('input', event)
},
initResizeObserver() {
const events = this.$refs['eventsRef'+this.week];
if (!events) return;
this.resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
if(width > 0) this.width = width
}
});
this.resizeObserver.observe(events);
},
destroyResizeObserver() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
},
},
mounted() {
setTimeout(() => this.$refs.eventcontainer.scrollTop = this.$refs.eventcontainer.scrollHeight / 3 + 1, 0);
const container = document.getElementById("calendarContainer")
if(container) {
container.style['overflow-y'] = 'scroll'
container.style['overflow-x'] = 'auto'
}
this.initResizeObserver();
},
beforeUnmount() {
this.destroyResizeObserver();
},
template: /*html*/`
<div ref="page" class="fhc-calendar-week-page" style="min-width: 700px;">
<div class="d-flex flex-column">
<div class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle" >
<div type="button" v-for="day in days" :key="day" class="flex-grow-1" :title="dayText[day]?.heading" @click.prevent="changeToDay(day)">
<div class="fw-bold">{{dayText[day]?.tag}}</div>
<a href="#" class="small text-secondary text-decoration-none" >{{dayText[day]?.datum}}</a>
</div>
</div>
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="hourPosition = null" >
<div :id="hourGridIdentifier(hour)" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border-top" :style="hourGridStyle(hour)"></div>
<Transition>
<div v-if="hourPosition" class="position-absolute border-top small" :style="indicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
</div>
</Transition>
<div class="events" :ref="'eventsRef'+week">
<div class="hours">
<div v-for="hour in hours" :style="getGridStyle" :key="hour" class="text-muted text-end small" :ref="'hour' + hour">{{hour}}:00</div>
</div>
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
<Transition>
<div v-if="day.isToday" class="position-absolute border-top small" :style="curIndicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{curTime}}</span>
</div>
</Transition>
<div v-if="day.isToday" :style="overlayStyle"></div>
<div v-for="event in day.events" :key="event" @click.prevent="weekPageClick(event.orig, day)"
:selected="event.orig == selectedEvent"
:style="eventGridStyle(day,event)"
class="mx-2 small rounded overflow-hidden fhc-entry "
v-contrast >
<slot name="weekPage" :event="event" :day="day">
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
</slot>
</div>
</div>
</div>
</div>
</div>
</div>`
}
-45
View File
@@ -1,45 +0,0 @@
import CalendarAbstract from './Abstract.js';
export default {
mixins: [
CalendarAbstract
],
inject: [
'size',
'focusDate'
],
computed: {
weeks(){
return [...Array(this.focusDate.numWeeks).keys()].map(i => i + 1);
},
title() {
return this.focusDate.format({year: 'numeric'});
}
},
methods: {
setWeek(week) {
// TODO(chris): test is there a week jump on year select? => yes there is if the same month/day are in different weeks ... should we prevent that?
this.focusDate.w = week;
this.$emit('updateMode', 'week');
},
prev(){
this.focusDate.y--;
this.focusDate._clean();
},
next() {
this.focusDate.y++;
this.focusDate._clean();
},
},
template: `
<div class="fhc-calendar-weeks h-100">
<calendar-header :title="title" @prev="prev" @next="next" @click="$emit('updateMode', 'years')" @updateMode="$emit('updateMode', $event)" />
<div class="d-flex flex-wrap">
<div v-for="(week, key) in weeks" :key="key" class="d-grid col-2">
<button @click="setWeek(week)" class="btn btn-outline-secondary" :class="{'border-0': week != focusDate.w}">
{{week}}
</button>
</div>
</div>
</div>`
}

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