mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-10-12 06:54:04 +00:00
Merge branch 'master' into feature-75891/Electronic_Onboarding_Infocenter_Tool
This commit is contained in:
+28
-35
@@ -147,6 +147,8 @@ html {
|
||||
--fhc-cis-menu-lvl-5-color-hover: var(--fhc-text);
|
||||
--fhc-cis-grade-positive: var(--fhc-success);
|
||||
--fhc-cis-grade-negative: var(--fhc-danger);
|
||||
|
||||
--fhc-offcanvas-zindex: 1045;
|
||||
}
|
||||
|
||||
#themeSwitch i{
|
||||
@@ -375,7 +377,7 @@ html {
|
||||
/* searchbar */
|
||||
#nav-search {
|
||||
background-color: var(--fhc-primary);
|
||||
z-index: 1;
|
||||
z-index: calc(var(--fhc-offcanvas-zindex) + 1) !important;
|
||||
}
|
||||
#nav-search.me-3 {
|
||||
margin: 0 !important;
|
||||
@@ -386,12 +388,6 @@ html {
|
||||
#nav-search > .input-group > * {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
#nav-search .searchbar_results {
|
||||
top: 100% !important;
|
||||
left: 0;
|
||||
right: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* frame */
|
||||
.in-frame {
|
||||
@@ -413,10 +409,18 @@ html {
|
||||
color: var(--fhc-link) !important;
|
||||
}
|
||||
|
||||
#nav-main {
|
||||
z-index: var(--fhc-offcanvas-zindex);
|
||||
}
|
||||
|
||||
#nav-main-sticky {
|
||||
max-height: calc(100vh - var(--fhc-cis-header-height));
|
||||
}
|
||||
|
||||
#nav-user-menu {
|
||||
z-index: calc(var(--fhc-offcanvas-zindex) + 1);
|
||||
}
|
||||
|
||||
#nav-user-menu img {
|
||||
object-fit: cover;
|
||||
height: calc( 3 * var(--fhc-cis-header-py));
|
||||
@@ -457,7 +461,14 @@ html {
|
||||
/* overflow: visible !important; */
|
||||
}
|
||||
#cis-header {
|
||||
z-index: 10;
|
||||
z-index: 10;
|
||||
}
|
||||
#cis-header-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
height: var(--fhc-cis-header-height);
|
||||
width: 100%;
|
||||
background-color: var(--fhc-primary);
|
||||
}
|
||||
#cis-header nav {
|
||||
position: initial;
|
||||
@@ -473,12 +484,7 @@ html {
|
||||
display: none;
|
||||
}
|
||||
#nav-logo {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: var(--fhc-cis-header-height);
|
||||
width: var(--fhc-cis-menu-width);
|
||||
background-color: var(--fhc-primary);
|
||||
padding: var(--fhc-cis-header-py) var(--fhc-cis-header-px);
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -493,37 +499,27 @@ html {
|
||||
top: var(--fhc-cis-header-height);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#nav-main-sticky > :not(#nav-main-toggle) {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
#nav-main-toggle {
|
||||
width: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
#nav-main-toggle .btn,
|
||||
#nav-main-toggle .fa-arrow-circle-left {
|
||||
#nav-main-toggle:hover {
|
||||
background-color: var(--fhc-secondary-highlight);
|
||||
}
|
||||
#nav-main-toggle .div,
|
||||
#nav-main-toggle .fa-chevron-left {
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
#nav-main-toggle .collapsed.btn {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
#nav-main-toggle .collapsed .fa-arrow-circle-left {
|
||||
#nav-main-toggle .collapsed .fa-chevron-left {
|
||||
transform: scaleX(-1);
|
||||
color: var(--fhc-black-40);
|
||||
}
|
||||
#nav-search {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: var(--fhc-cis-menu-width);
|
||||
height: var(--fhc-cis-header-height);
|
||||
right: calc(var(--fhc-cis-header-height) + 2 * var(--fhc-cis-header-px) - 2 * var(--fhc-cis-header-py));
|
||||
width: auto !important;
|
||||
}
|
||||
#nav-user {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
}
|
||||
#nav-user-btn {
|
||||
border-width: 0;
|
||||
@@ -558,7 +554,7 @@ html {
|
||||
|
||||
#nav-main-menu {
|
||||
height: 100%;
|
||||
background-color: var(--fhc-cis-menu-bg);
|
||||
background-color: var(--fhc-primary);
|
||||
}
|
||||
#nav-main-menu > div {
|
||||
width: var(--fhc-cis-menu-width);
|
||||
@@ -600,9 +596,6 @@ html {
|
||||
}
|
||||
#nav-user{
|
||||
position: relative;
|
||||
}
|
||||
#nav-user-btn {
|
||||
|
||||
}
|
||||
#nav-user-btn img {
|
||||
object-fit: cover;
|
||||
|
||||
@@ -38,3 +38,9 @@ textarea[name="anmerkung"] {
|
||||
{
|
||||
border-color: black;
|
||||
}
|
||||
|
||||
.node-verband {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@import './SvgIcons.css';
|
||||
@import './components/searchbar/searchbar.css';
|
||||
@import './components/verticalsplit.css';
|
||||
@import './components/horizontalsplit.css';
|
||||
@import './components/FilterComponent.css';
|
||||
@import './components/Tabs.css';
|
||||
@import './components/Notiz.css';
|
||||
@@ -95,6 +96,13 @@ html.fs_huge {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* to enable scrolling for many student entries*/
|
||||
.stv-details-abschlusspruefung-student-names {
|
||||
max-height: 80px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.p-dropdown-item{
|
||||
z-index: 1201 !important;
|
||||
}
|
||||
@@ -197,10 +205,6 @@ html.fs_huge {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.tiny-90 div.tox.tox-tinymce {
|
||||
height: 90% !important;
|
||||
}
|
||||
|
||||
/* slim begin */
|
||||
.stv .form-label {
|
||||
margin-bottom: .15rem;
|
||||
@@ -281,3 +285,7 @@ html.fs_huge {
|
||||
}
|
||||
*/
|
||||
/* slim ende */
|
||||
|
||||
.fhc-xxl-modal {
|
||||
min-width: 80vw;
|
||||
}
|
||||
@@ -62,3 +62,16 @@
|
||||
top:13%;"
|
||||
z-index: 104; font-size: 1rem; width: 2.5rem; height: 2.5rem; opacity:0; transition: opacity 0.2s; top:13%;"
|
||||
}*/
|
||||
|
||||
.tag-hover:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.green {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.btn-hover {
|
||||
--bs-btn-hover-bg: #e9ecef; /* Neue Hintergrundfarbe beim Hover */
|
||||
--bs-btn-hover-border-color: #e9ecef; /* Neue Rahmenfarbe beim Hover */
|
||||
}
|
||||
|
||||
@@ -70,6 +70,10 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.filter-table-dataset .tabulator-tableholder {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.tabulator {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@@ -305,4 +305,104 @@
|
||||
/* If you use hover rows, you need to ensure the sticky cell matches the hover color */
|
||||
#abgabetable .tabulator-row:hover .tabulator-cell.sticky-col {
|
||||
background-color: #ccc; /* Match your existing hover color */
|
||||
}
|
||||
}
|
||||
|
||||
.tabulator-cell {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.tabulator-col-title {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@container (max-width: 100px) {
|
||||
.full-text {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.short-text {
|
||||
display: inline-block !important;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/*conditional tooltips fix*/
|
||||
.p-tooltip.custom-tooltip {
|
||||
z-index: 8001 !important;
|
||||
}
|
||||
|
||||
/* Shrinks font and table rows for desktop users who have zoomed in their browser (150%+).
|
||||
Does not affect mobile/touchscreen devices, which use touch input instead of a mouse. */
|
||||
@media (pointer: fine) and (min-resolution: 1.5dppx) {
|
||||
|
||||
html.abgabetool {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
.abgabetool .tabulator-cell,
|
||||
.abgabetool .tabulator-row {
|
||||
height: 20px;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*confetti celebration on endupload - impossible to miss*/
|
||||
#confetti-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.confetti-piece {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
will-change: top, transform, opacity;
|
||||
}
|
||||
|
||||
/* Background Rain */
|
||||
@keyframes fallAndSpin {
|
||||
0% {
|
||||
top: var(--start-y);
|
||||
transform: translate3d(0, 0, 0) rotateX(0deg) rotateY(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 105vh;
|
||||
transform: translate3d(var(--drift), 0, 0) rotateX(720deg) rotateY(360deg);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
/* Corner Cannons*/
|
||||
@keyframes cannonBlast {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0) scale(0.3) rotate(0deg);
|
||||
opacity: 1;
|
||||
animation-timing-function: cubic-bezier(0.1, 0.8, 0.2, 1);
|
||||
}
|
||||
30% {
|
||||
transform: translate3d(var(--blast-x), var(--blast-y), 0) scale(1.2) rotate(270deg);
|
||||
opacity: 1;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(calc(var(--blast-x) * 1.4), 15vh, 0) scale(0.4) rotate(630deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.p-timeline-event-opposite {
|
||||
flex: 0 0 90px;
|
||||
max-width: 90px;
|
||||
}
|
||||
|
||||
.p-timeline-event-content {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@import './dashboard/news.css';
|
||||
@import './dashboard/LvPlan.css';
|
||||
|
||||
:root{
|
||||
:root {
|
||||
--fhc-dashboard-danger: var(--fhc-danger, #842029);
|
||||
--fhc-dashboard-grid-size: 4;
|
||||
--fhc-dashboard-link: var(--fhc-link, #0a57ca);
|
||||
@@ -17,22 +17,16 @@
|
||||
--fhc-dashboard-section-info-color-hover: var(--fhc-primary-highlight, #005585);
|
||||
}
|
||||
|
||||
@media(max-width: 577px) {
|
||||
:root {
|
||||
--fhc-dashboard-grid-size: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.core-dashboard a{
|
||||
.core-dashboard a {
|
||||
color: var(--fhc-dashboard-link);
|
||||
}
|
||||
|
||||
@media (max-width: 576px){
|
||||
@media (max-width: 576px) {
|
||||
.widget-icon {
|
||||
max-height: 250px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.widget-icon-container{
|
||||
.widget-icon-container {
|
||||
max-width: 250px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@@ -46,27 +40,36 @@
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
cursor:pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dashboard-section > .newGridRow{
|
||||
position:absolute;
|
||||
width:20px;
|
||||
height:20px;
|
||||
padding:0;
|
||||
bottom:0;
|
||||
left:50%;
|
||||
transform:translate(-50%, 50%);
|
||||
.dashboard-section.edit-active {
|
||||
/**
|
||||
* replaces margin for extra row
|
||||
* 10% equals 0.1 of 100%
|
||||
* 1rem equals the padding of pb-3 that is overwritten here
|
||||
*/
|
||||
padding-bottom: calc(10% / var(--fhc-dashboard-grid-size) + 1rem) !important;
|
||||
}
|
||||
|
||||
.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;
|
||||
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>');
|
||||
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 {
|
||||
@@ -74,16 +77,6 @@
|
||||
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;
|
||||
@@ -105,6 +98,7 @@
|
||||
@media(max-width: 577px) {
|
||||
:root {
|
||||
--fhc-dashboard-grid-size: 1;
|
||||
--fhc-dg-item-py: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,50 +126,64 @@
|
||||
cursor: move !important;
|
||||
}
|
||||
|
||||
.draggedItem {
|
||||
.drop-grid-item-resize > .dashboard-item,
|
||||
.drop-grid-item-move > .dashboard-item {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--fhc-dashboard-draggeditem-background);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dashboard-item-overlay{
|
||||
.drop-grid-item-resize > .dashboard-item > *,
|
||||
.drop-grid-item-move > .dashboard-item > * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.drop-grid-item-sizechanged > .dashboard-item,
|
||||
.drop-grid-item-move > .dashboard-item {
|
||||
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;
|
||||
.drop-grid-item-sizechanged > .dashboard-item::before,
|
||||
.drop-grid-item-move > .dashboard-item::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: .25rem;
|
||||
left: .25rem;
|
||||
right: .25rem;
|
||||
bottom: .25rem;
|
||||
border: 4px dashed var(--fhc-dashboard-item-overly-border-color);
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
#deleteBookmark i{
|
||||
.drop-grid-item-oversized > .dashboard-item {
|
||||
/* Bootstrap: border-danger */
|
||||
--bs-border-opacity: 1;
|
||||
border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important;
|
||||
}
|
||||
|
||||
#deleteBookmark i {
|
||||
color: var(--fhc-dashboard-danger);
|
||||
}
|
||||
|
||||
.pin:hover{
|
||||
.pin:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pin[pinned]:hover{
|
||||
.pin[pinned]:hover {
|
||||
color: var(--fhc-dashboard-pin-pinned-hover-color);
|
||||
}
|
||||
|
||||
.section-info{
|
||||
.section-info {
|
||||
color: var(--fhc-dashboard-section-info-color);
|
||||
cursor:pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.section-info:hover {
|
||||
color: var(--fhc-dashboard-section-info-color-hover);
|
||||
}
|
||||
|
||||
.denied-dragging-animation {
|
||||
.drop-grid-item-blocker [pinned='true'] {
|
||||
animation: wiggle 0.5s linear;
|
||||
color: var(--fhc-dashboard-denied-dragging-animation-color) !important;
|
||||
}
|
||||
@@ -204,13 +212,13 @@
|
||||
|
||||
}
|
||||
|
||||
.hiddenWidget{
|
||||
.hidden-widget {
|
||||
background: var(--fhc-disabled-background);
|
||||
opacity: 40%;
|
||||
}
|
||||
|
||||
.hiddenWidget .card,
|
||||
.hiddenWidget .card-body,
|
||||
.hiddenWidget .card-body *{
|
||||
.hidden-widget .card,
|
||||
.hidden-widget .card-body,
|
||||
.hidden-widget .card-body * {
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
:root {
|
||||
--fhc-horizontalsplit-hsplitter-bg-color: var(--fhc-background, #eee);
|
||||
--fhc-horizontalsplit-hsplitter-border-color: var(--fhc-border, #eee);
|
||||
--fhc-horizontalsplit-hsplitter-splitactions-color: var(--fhc-dark, #000);
|
||||
}
|
||||
|
||||
.horizontalsplit-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
max-height: 100%;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.horizontalsplitted {
|
||||
overflow: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.horizontalsplitter {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.horizontalsplitter.left {
|
||||
border-left: solid 3px var(--fhc-horizontalsplit-hsplitter-border-color);
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.horizontalsplitter.right {
|
||||
border-right: solid 3px var(--fhc-horizontalsplit-hsplitter-border-color);
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.splitactions.horizontal {
|
||||
background-color: var(--fhc-horizontalsplit-hsplitter-bg-color);
|
||||
color: var(--fhc-horizontalsplit-hsplitter-splitactions-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 5px 0 5px 0;
|
||||
}
|
||||
|
||||
.splitactions.horizontal.left {
|
||||
border-radius: 0 40% 40% 0;
|
||||
}
|
||||
|
||||
.splitactions.horizontal.right {
|
||||
border-radius: 40% 0 0 40%;
|
||||
}
|
||||
|
||||
.splitactions.horizontal .splitaction {
|
||||
width: 14px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.splitactions.horizontal .splitaction.resize {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
#content > div:first-child {
|
||||
margin-top: 30px;
|
||||
}
|
||||
+40
-2
@@ -46,7 +46,6 @@
|
||||
background-color: #ffA500ff;
|
||||
}
|
||||
|
||||
|
||||
.tag_braun {
|
||||
background-color: #6d4c41;
|
||||
}
|
||||
@@ -69,12 +68,46 @@
|
||||
|
||||
.tag_limette {
|
||||
background-color: #D3FFCE;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.tag_turquoise {
|
||||
background-color: #89cccf;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.tag_bordeaux {
|
||||
background-color: #7B1B38;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tag_olive {
|
||||
background-color: #868d07;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tag_beige {
|
||||
background-color: #E4D5B4;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.tag_skyblue {
|
||||
background-color: #90D5FF;
|
||||
color: black;
|
||||
}
|
||||
.tag_blush {
|
||||
background-color: #A83E3E;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tag_done {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.tag_auto {
|
||||
border: 1px dashed white;
|
||||
}
|
||||
|
||||
.display_all {
|
||||
background-color: darkgrey !important;
|
||||
border: none;
|
||||
@@ -111,4 +144,9 @@
|
||||
.copy-btn {
|
||||
float: right;
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-hover {
|
||||
--bs-btn-hover-bg: #e9ecef;
|
||||
--bs-btn-hover-border-color: #e9ecef;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,13 @@ export default {
|
||||
}
|
||||
};
|
||||
},
|
||||
patchProjektarbeitAbgabeMultiple(payload) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/Abgabe/patchProjektarbeitAbgabeMultiple',
|
||||
params: payload
|
||||
};
|
||||
},
|
||||
deleteProjektarbeitAbgabe(paabgabe_id) {
|
||||
return {
|
||||
method: 'post',
|
||||
@@ -84,6 +91,13 @@ export default {
|
||||
params: { paabgabe_id }
|
||||
};
|
||||
},
|
||||
deleteProjektarbeitAbgabeMultiple(paabgabe_ids) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/Abgabe/deleteProjektarbeitAbgabeMultiple',
|
||||
params: { paabgabe_ids }
|
||||
};
|
||||
},
|
||||
postSerientermin(datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, upload_allowed, projektarbeit_ids, fixtermin) {
|
||||
return {
|
||||
method: 'post',
|
||||
@@ -139,6 +153,30 @@ export default {
|
||||
url: '/api/frontend/v1/Abgabe/getSignaturStatusForProjektarbeitAbgaben',
|
||||
params: {paabgabe_ids, student_uid},
|
||||
|
||||
};
|
||||
},
|
||||
postStudentProjektarbeitTitel(projektarbeit_id, titel) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/Abgabe/postStudentProjektarbeitTitel',
|
||||
params: {projektarbeit_id, titel},
|
||||
|
||||
};
|
||||
},
|
||||
sendZweitbetreuerTokenMail(projektarbeit_id, betreuer_person_id, student_uid) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/Abgabe/sendZweitbetreuerTokenMail',
|
||||
params: {projektarbeit_id, bperson_id: betreuer_person_id, student_uid},
|
||||
|
||||
};
|
||||
},
|
||||
fetchProjektarbeitenHistory(student_uid) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/Abgabe/fetchProjektarbeitenHistory',
|
||||
params: {student_uid},
|
||||
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
getViewData() {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/Cis4FhcApi/getViewData'
|
||||
url: '/api/frontend/v1/Cis4FhcApi/dashboardViewData'
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (C) 2026 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 {
|
||||
getAllStudienSemester(studiensemester, studiengang, semester, studienplan) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: 'api/frontend/v1/Studium/getStudienAllSemester/',
|
||||
params: {studiensemester, studiengang, semester, studienplan}
|
||||
};
|
||||
},
|
||||
}
|
||||
@@ -34,4 +34,10 @@ export default {
|
||||
url: 'api/frontend/v1/detailheader/detailheader/getLeitungOrg/' + oekurzbz,
|
||||
};
|
||||
},
|
||||
getSemesterStati(prestudent_id){
|
||||
return {
|
||||
method: 'get',
|
||||
url: 'api/frontend/v1/detailheader/detailheader/getSemesterStati/' + prestudent_id,
|
||||
};
|
||||
},
|
||||
}
|
||||
@@ -35,5 +35,11 @@ export default {
|
||||
method: 'get',
|
||||
url: `/api/frontend/v1/Lehre/Pruefungen/${lehrveranstaltung_id}`
|
||||
};
|
||||
},
|
||||
getSemesterAverageGrade(semester) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: `/api/frontend/v1/Lehre/semesterAverageGrade/${semester}`
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -16,6 +16,12 @@
|
||||
*/
|
||||
|
||||
export default {
|
||||
getMyLvPlanViewData() {
|
||||
return {
|
||||
method: 'get',
|
||||
url: `/api/frontend/v1/LvPlan/myLvPlanViewData`,
|
||||
};
|
||||
},
|
||||
getRoomInfo(ort_kurzbz, start_date, end_date) {
|
||||
return {
|
||||
method: 'post',
|
||||
@@ -30,11 +36,11 @@ export default {
|
||||
params: { start_date, end_date, lv_id }
|
||||
};
|
||||
},
|
||||
eventsPersonal(start_date, end_date) {
|
||||
eventsPersonal(start_date, end_date, uid = null) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/lvPlan/eventsPersonal',
|
||||
params: { start_date, end_date }
|
||||
params: { start_date, end_date, uid }
|
||||
};
|
||||
},
|
||||
eventsLv(lv_id, start_date, end_date) {
|
||||
@@ -57,11 +63,11 @@ export default {
|
||||
params: { start_date, end_date }
|
||||
};
|
||||
},
|
||||
getLvPlanReservierungen(start_date, end_date) {
|
||||
getLvPlanReservierungen(start_date, end_date, uid = null) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/LvPlan/getReservierungen',
|
||||
params: { start_date, end_date }
|
||||
params: { start_date, end_date, uid }
|
||||
};
|
||||
},
|
||||
getLehreinheitStudiensemester(lehreinheit_id) {
|
||||
@@ -92,5 +98,42 @@ export default {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/LvPlan/getLv/' + lehrveranstaltung_id
|
||||
};
|
||||
}
|
||||
},
|
||||
eventsStgOrg(start_date, end_date, stg_kz, sem, verband, gruppe) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/lvPlan/eventsStgOrg',
|
||||
params: { start_date, end_date, stg_kz, sem, verband, gruppe }
|
||||
};
|
||||
},
|
||||
getStudiengaenge(){
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/lvPlan/getStudiengaenge'
|
||||
}
|
||||
},
|
||||
getLehrverband(stg_kz, sem){
|
||||
return {
|
||||
method: 'get',
|
||||
url: `/api/frontend/v1/lvPlan/getLehrverband/${stg_kz}/${sem}`
|
||||
}
|
||||
},
|
||||
getGruppe(stg_kz, sem, verband){
|
||||
return {
|
||||
method: 'get',
|
||||
url: `/api/frontend/v1/lvPlan/getLehrverband/${stg_kz}/${sem}/${verband}`
|
||||
}
|
||||
},
|
||||
checkPermissionOtherLvPlan(){
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/lvPlan/permissionOtherLvPlan',
|
||||
}
|
||||
},
|
||||
getCompactibleEventTypes(){
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/lvPlan/compactibleEventTypes',
|
||||
}
|
||||
},
|
||||
};
|
||||
+2
-20
@@ -16,28 +16,10 @@
|
||||
*/
|
||||
|
||||
export default {
|
||||
get(path) {
|
||||
let url = 'api/frontend/v1/lv/StgTree';
|
||||
if (path)
|
||||
url += '/' + path;
|
||||
getOtherLvPlanViewData(uid) {
|
||||
return {
|
||||
method: 'get',
|
||||
url
|
||||
url: `/api/frontend/v1/OtherLvPlan/otherLvPlanViewData/${uid}`,
|
||||
};
|
||||
},
|
||||
favorites: {
|
||||
get() {
|
||||
return {
|
||||
method: 'get',
|
||||
url: 'api/frontend/v1/lv/favorites'
|
||||
};
|
||||
},
|
||||
set(favorites) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: 'api/frontend/v1/lv/favorites/set',
|
||||
params: { favorites }
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -41,5 +41,24 @@ export default {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/phrasen/getAllLanguages'
|
||||
};
|
||||
}
|
||||
},
|
||||
getPhrases(phrasesGroupedByCategory, languages = []) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/phrasen/getPhrases',
|
||||
params: {
|
||||
phrasesGroupedByCategory,
|
||||
languages,
|
||||
},
|
||||
};
|
||||
},
|
||||
getTabulatorPhrases(languages = []) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/phrasen/getTabulatorPhrases',
|
||||
params: {
|
||||
languages: JSON.stringify(languages),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
export default {
|
||||
|
||||
profilViewData(uid) {
|
||||
getProfilViewData(uid = null) {
|
||||
let url = "/api/frontend/v1/Profil/profilViewData";
|
||||
if(uid){
|
||||
url += `/${uid}`;
|
||||
|
||||
@@ -16,28 +16,10 @@
|
||||
*/
|
||||
|
||||
export default {
|
||||
get(path) {
|
||||
let url = 'api/frontend/v1/stv/verband';
|
||||
if (path)
|
||||
url += '/' + path;
|
||||
getStgOrgLvPlanViewData(uid) {
|
||||
return {
|
||||
method: 'get',
|
||||
url
|
||||
url: `/api/frontend/v1/StgOrgLvPlan/stgOrgLvPlanViewData`,
|
||||
};
|
||||
},
|
||||
favorites: {
|
||||
get() {
|
||||
return {
|
||||
method: 'get',
|
||||
url: 'api/frontend/v1/stv/favorites'
|
||||
};
|
||||
},
|
||||
set(favorites) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: 'api/frontend/v1/stv/favorites/set',
|
||||
params: { favorites }
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -29,5 +29,20 @@ export default {
|
||||
url: 'api/frontend/v1/organisation/studiensemester/getAll',
|
||||
params: { order, start }
|
||||
};
|
||||
},
|
||||
current() {
|
||||
return {
|
||||
method: 'get',
|
||||
url: 'api/frontend/v1/organisation/studiensemester/current'
|
||||
};
|
||||
},
|
||||
set(studiensemester_kurzbz) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: 'api/frontend/v1/organisation/studiensemester/set',
|
||||
params: {
|
||||
studiensemester_kurzbz
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
import app from './stv/app.js';
|
||||
import lists from './stv/lists.js';
|
||||
import verband from './stv/verband.js';
|
||||
import students from './stv/students.js';
|
||||
import filter from './stv/filter.js';
|
||||
import konto from './stv/konto.js';
|
||||
@@ -34,7 +33,6 @@ import admissionDates from './stv/admissionDates.js';
|
||||
export default {
|
||||
app,
|
||||
lists,
|
||||
verband,
|
||||
students,
|
||||
filter,
|
||||
konto,
|
||||
|
||||
@@ -16,16 +16,20 @@
|
||||
*/
|
||||
|
||||
export default {
|
||||
getCourselist(params) {
|
||||
getCourselist(student_uid, start_date, end_date, stundenplan) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: 'api/frontend/v1/stv/LvTermine/getStundenplan/' + params.student_uid + '/'
|
||||
+ params.start_date + '/'
|
||||
+ params.end_date + '/'
|
||||
+ params.group_consecutiveHours + '/'
|
||||
+ params.dbStundenplanTable
|
||||
url: 'api/frontend/v1/stv/LvTermine/getStundenplan/' + encodeURIComponent(student_uid) + '/'
|
||||
+ encodeURIComponent(start_date) + '/'
|
||||
+ encodeURIComponent(end_date) + '/'
|
||||
+ encodeURIComponent(stundenplan) + '/'
|
||||
+ encodeURIComponent(true)
|
||||
};
|
||||
},
|
||||
exportCalendar(student_uid, stundenplan)
|
||||
{
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + 'content/statistik/termine.xls.php?student_uid=' + encodeURIComponent(student_uid) + '&db_stpl_table='+encodeURIComponent(stundenplan);
|
||||
},
|
||||
getStudiensemester(){
|
||||
return {
|
||||
method: 'get',
|
||||
|
||||
@@ -38,6 +38,10 @@ export default {
|
||||
};
|
||||
},
|
||||
insert(params) {
|
||||
if(params.betrag)
|
||||
{
|
||||
params.betrag = params.betrag.replace(',', '.');
|
||||
}
|
||||
return {
|
||||
method: 'post',
|
||||
url: 'api/frontend/v1/stv/konto/insert',
|
||||
@@ -52,6 +56,10 @@ export default {
|
||||
};
|
||||
},
|
||||
edit(params) {
|
||||
if(params.betrag)
|
||||
{
|
||||
params.betrag = params.betrag.replace(',', '.');
|
||||
}
|
||||
return {
|
||||
method: 'post',
|
||||
url: 'api/frontend/v1/stv/konto/update',
|
||||
@@ -65,10 +73,14 @@ export default {
|
||||
params: { buchungsnr }
|
||||
};
|
||||
},
|
||||
getBuchungstypen() {
|
||||
getBuchungstypen(studiensemester_kurzbz) {
|
||||
let url = 'api/frontend/v1/stv/konto/getBuchungstypen'
|
||||
if (!!studiensemester_kurzbz)
|
||||
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
|
||||
|
||||
return {
|
||||
method: 'get',
|
||||
url: 'api/frontend/v1/stv/konto/getBuchungstypen'
|
||||
url: url
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -17,9 +17,9 @@
|
||||
|
||||
export default {
|
||||
uid(uid, studiensemester_kurzbz) {
|
||||
let url = 'api/frontend/v1/stv/students/'
|
||||
let url = 'api/frontend/v1/treemenudata/stv/stdsem/'
|
||||
+ encodeURIComponent(studiensemester_kurzbz)
|
||||
+ '/uid/'
|
||||
+ '/student/'
|
||||
+ encodeURIComponent(uid);
|
||||
return {
|
||||
method: 'get',
|
||||
@@ -27,7 +27,7 @@ export default {
|
||||
};
|
||||
},
|
||||
prestudent(prestudent_id, studiensemester_kurzbz) {
|
||||
let url = 'api/frontend/v1/stv/students/'
|
||||
let url = 'api/frontend/v1/treemenudata/stv/stdsem/'
|
||||
+ encodeURIComponent(studiensemester_kurzbz)
|
||||
+ '/prestudent/'
|
||||
+ encodeURIComponent(prestudent_id);
|
||||
@@ -37,7 +37,7 @@ export default {
|
||||
};
|
||||
},
|
||||
person(person_id, studiensemester_kurzbz) {
|
||||
let url = 'api/frontend/v1/stv/students/'
|
||||
let url = 'api/frontend/v1/treemenudata/stv/stdsem/'
|
||||
+ encodeURIComponent(studiensemester_kurzbz)
|
||||
+ '/person/'
|
||||
+ encodeURIComponent(person_id);
|
||||
|
||||
@@ -51,4 +51,20 @@ export default {
|
||||
params: data
|
||||
};
|
||||
},
|
||||
|
||||
getAllTagsPrestudent(prestudent_id){
|
||||
return {
|
||||
method: 'get',
|
||||
url: 'api/frontend/v1/stv/Tags/getAllTags',
|
||||
params: prestudent_id
|
||||
};
|
||||
},
|
||||
|
||||
rebuildTagsforTypeId(data){
|
||||
return {
|
||||
method: 'post',
|
||||
url: 'api/frontend/v1/stv/Tags/rebuildTagsForTypeId/',
|
||||
params: data
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 {
|
||||
getTabulatorPresets(tableName) {
|
||||
return {
|
||||
method: "get",
|
||||
url:
|
||||
"/api/frontend/v1/TabulatorPresets/getTabulatorPresets?tableName=" +
|
||||
tableName,
|
||||
};
|
||||
},
|
||||
createTabulatorPreset(tableName, presetName, preset) {
|
||||
return {
|
||||
method: "post",
|
||||
url: "/api/frontend/v1/TabulatorPresets/createTabulatorPreset",
|
||||
params: {
|
||||
tableName,
|
||||
presetName,
|
||||
preset,
|
||||
},
|
||||
};
|
||||
},
|
||||
updateTabulatorPreset(presetId, preset) {
|
||||
return {
|
||||
method: "post",
|
||||
url: "/api/frontend/v1/TabulatorPresets/updateTabulatorPreset",
|
||||
params: {
|
||||
presetId,
|
||||
preset,
|
||||
},
|
||||
};
|
||||
},
|
||||
deleteTabulatorPreset(tabulatorPresetId) {
|
||||
return {
|
||||
method: "post",
|
||||
url:
|
||||
"/api/frontend/v1/TabulatorPresets/deleteTabulatorPreset",
|
||||
params: {
|
||||
presetId: tabulatorPresetId,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (C) 2026 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 {
|
||||
get(config, path = '') {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/treemenu/' + config + '/' + path
|
||||
};
|
||||
},
|
||||
data(config, path = '') {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/treemenudata/' + config + '/' + path
|
||||
};
|
||||
},
|
||||
favorites: {
|
||||
get(config) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: 'api/frontend/v1/favorites/get/' + config
|
||||
};
|
||||
},
|
||||
set(config, favorites) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: 'api/frontend/v1/favorites/set/' + config,
|
||||
params: { favorites }
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
export default {
|
||||
getBookmarks() {
|
||||
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/Bookmark/getBookmarks'
|
||||
@@ -28,18 +29,24 @@ export default {
|
||||
url: `/api/frontend/v1/Bookmark/delete/${bookmark_id}`
|
||||
};
|
||||
},
|
||||
update({ bookmark_id, url, title, tag=null }) {
|
||||
update({ bookmark_id, url, title, tag }) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: `/api/frontend/v1/Bookmark/update/${bookmark_id}`,
|
||||
params: { url, title }
|
||||
params: { url, title, tag }
|
||||
};
|
||||
},
|
||||
insert({ url, title, tag }) {
|
||||
insert({ url, title, tag, sort }) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: `/api/frontend/v1/Bookmark/insert`,
|
||||
params: { url, title, tag }
|
||||
params: { url, title, tag, sort }
|
||||
};
|
||||
},
|
||||
changeOrder(bookmark_id1, bookmark_id2) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: `/api/frontend/v1/Bookmark/changeOrder/${bookmark_id1}/${bookmark_id2}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
get() {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/lv/config/get'
|
||||
};
|
||||
},
|
||||
set(params) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: 'api/frontend/v1/lv/config/set',
|
||||
params
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -18,6 +18,14 @@ export default {
|
||||
params: newData
|
||||
};
|
||||
},
|
||||
getGruppe(gruppe)
|
||||
{
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/lv/gruppe/getGruppe/',
|
||||
params: gruppe
|
||||
};
|
||||
},
|
||||
getByLehreinheit(lehreinheit_id)
|
||||
{
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
getCourselist(le_id, start_date, end_date, stundenplan)
|
||||
{
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/lvPlan/getLeEvents/' + encodeURIComponent(le_id) + "/" + encodeURIComponent(start_date) + "/" + encodeURIComponent(end_date) + "/" + encodeURIComponent(stundenplan),
|
||||
};
|
||||
},
|
||||
exportCalendar(le_id, stundenplan)
|
||||
{
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + 'content/statistik/termine.xls.php?lehreinheit_id=' + encodeURIComponent(le_id) + '&db_stpl_table='+encodeURIComponent(stundenplan);
|
||||
},
|
||||
getStudiensemester(){
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/lv/setup/getStudiensemester/'
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
getCourselist(lv_id, start_date, end_date, stundenplan)
|
||||
{
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/lvPlan/getLvEvents/' + encodeURIComponent(lv_id) + "/" + encodeURIComponent(start_date) + "/" + encodeURIComponent(end_date) + "/" + encodeURIComponent(stundenplan),
|
||||
};
|
||||
},
|
||||
exportCalendar(lv_id, stundenplan)
|
||||
{
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + 'content/statistik/termine.xls.php?lehrveranstaltung_id=' + encodeURIComponent(lv_id) + '&db_stpl_table='+encodeURIComponent(stundenplan);
|
||||
},
|
||||
getStudiensemester(){
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/lv/setup/getStudiensemester/'
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import Grades from '../factory/stv/grades.js';
|
||||
|
||||
export default {
|
||||
...Grades,
|
||||
getCertificate(lv_id, studiensemester_kurzbz) {
|
||||
let url = 'api/frontend/v1/lv/noten/getCertificate/' + encodeURIComponent(lv_id);
|
||||
if (!!studiensemester_kurzbz) {
|
||||
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
|
||||
}
|
||||
return {
|
||||
method: 'get',
|
||||
url: url
|
||||
};
|
||||
},
|
||||
getTeacherProposal(lv_id, studiensemester_kurzbz) {
|
||||
let url = 'api/frontend/v1/lv/noten/getTeacherProposal/' + encodeURIComponent(lv_id);
|
||||
if (!!studiensemester_kurzbz) {
|
||||
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
|
||||
}
|
||||
return {
|
||||
method: 'get',
|
||||
url: url
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,9 +1,16 @@
|
||||
export default {
|
||||
getTabs()
|
||||
getLETabs()
|
||||
{
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/lv/setup/getTabs/'
|
||||
url: '/api/frontend/v1/lv/setup/getLETabs/'
|
||||
};
|
||||
},
|
||||
getLVTabs()
|
||||
{
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/lv/setup/getLVTabs/'
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import verband from './stv/verband.js';
|
||||
import students from './stv/students.js';
|
||||
import filter from './stv/filter.js';
|
||||
import konto from './stv/konto.js';
|
||||
@@ -18,7 +17,6 @@ import jointstudies from "./stv/jointstudies.js";
|
||||
import courselist from './stv/courselist.js';
|
||||
|
||||
export default {
|
||||
verband,
|
||||
students,
|
||||
filter,
|
||||
konto,
|
||||
|
||||
@@ -35,9 +35,6 @@ export default {
|
||||
getMitarbeiter(searchString){
|
||||
return this.$fhcApi.get('api/frontend/v1/stv/abschlusspruefung/getMitarbeiter/' + searchString);
|
||||
},
|
||||
getPruefer(searchString){
|
||||
return this.$fhcApi.get('api/frontend/v1/stv/abschlusspruefung/getPruefer/' + searchString);
|
||||
},
|
||||
getNoten(){
|
||||
return this.$fhcApi.get('api/frontend/v1/stv/abschlusspruefung/getNoten/');
|
||||
},
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export default {
|
||||
get() {
|
||||
return this.$fhcApi.get('api/frontend/v1/stv/verband');
|
||||
},
|
||||
favorites: {
|
||||
get() {
|
||||
return this.$fhcApi.get('api/frontend/v1/stv/favorites');
|
||||
},
|
||||
set(favorites) {
|
||||
return this.$fhcApi.post('api/frontend/v1/stv/favorites/set', {
|
||||
favorites
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,6 @@ const app = Vue.createApp({
|
||||
|
||||
},
|
||||
computed: {
|
||||
viewData() {
|
||||
return { uid: this.uid}
|
||||
},
|
||||
student_uid_computed() {
|
||||
return this.student_uid ?? this.uid
|
||||
},
|
||||
@@ -55,10 +52,10 @@ const app = Vue.createApp({
|
||||
},
|
||||
template: `
|
||||
<template v-if="comp && uid">
|
||||
<AbgabetoolStudent v-if="comp == 'AbgabetoolStudent'" :viewData="viewData" :student_uid_prop="student_uid_computed"></AbgabetoolStudent>
|
||||
<AbgabetoolMitarbeiter v-if="comp == 'AbgabetoolMitarbeiter'" :viewData="viewData"></AbgabetoolMitarbeiter>
|
||||
<AbgabetoolAssistenz v-if="comp == 'AbgabetoolAssistenz'" :viewData="viewData" :stg_kz_prop="stg_kz_computed"></AbgabetoolAssistenz>
|
||||
<DeadlineOverview v-if="comp == 'DeadlinesOverview'" :viewData="viewData"></DeadlineOverview>
|
||||
<AbgabetoolStudent v-if="comp == 'AbgabetoolStudent'" :student_uid_prop="student_uid_computed"></AbgabetoolStudent>
|
||||
<AbgabetoolMitarbeiter v-if="comp == 'AbgabetoolMitarbeiter'"></AbgabetoolMitarbeiter>
|
||||
<AbgabetoolAssistenz v-if="comp == 'AbgabetoolAssistenz'" :stg_kz_prop="stg_kz_computed"></AbgabetoolAssistenz>
|
||||
<DeadlineOverview v-if="comp == 'DeadlinesOverview'"></DeadlineOverview>
|
||||
</template>
|
||||
`
|
||||
});
|
||||
|
||||
@@ -4,25 +4,27 @@ import Theme from '../../plugins/Theme.js';
|
||||
import contrast from '../../directives/contrast.js';
|
||||
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js";
|
||||
import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js";
|
||||
import MyLvPlan from "../../components/Cis/LvPlan/Personal.js";
|
||||
import MyLvPlan from "../../components/Cis/LvPlan/MyLvPlan.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 RoomInformation, {DEFAULT_MODE_RAUMINFO_DESKTOP, DEFAULT_MODE_RAUMINFO_MOBILE} from "../../components/Cis/Mylv/RoomInformation.js";
|
||||
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js";
|
||||
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js";
|
||||
import AbgabetoolAssistenz from "../../components/Cis/Abgabetool/AbgabetoolAssistenz.js";
|
||||
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js";
|
||||
import Studium from "../../components/Cis/Studium/Studium.js";
|
||||
import StgOrgLvPlan from "../../components/Cis/LvPlan/StgOrg.js";
|
||||
import OtherLvPlan from "../../components/Cis/LvPlan/OtherLvPlan.js";
|
||||
|
||||
import ApiRenderers from '../../api/factory/renderers.js';
|
||||
import ApiRouteInfo from '../../api/factory/routeinfo.js';
|
||||
import {capitalize} from "../../helpers/StringHelpers.js";
|
||||
|
||||
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
|
||||
const isMobile = window.matchMedia("(max-width: 767px)").matches;
|
||||
|
||||
const router = VueRouter.createRouter({
|
||||
history: VueRouter.createWebHistory(`/${ciPath}`),
|
||||
@@ -85,7 +87,7 @@ const router = VueRouter.createRouter({
|
||||
name: "RoomInformation",
|
||||
params: { // in this case always populate other params since they are not optional
|
||||
ort_kurzbz: to.params.ort_kurzbz,
|
||||
mode: DEFAULT_MODE_RAUMINFO,
|
||||
mode: isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP,
|
||||
focus_date: new Date().toISOString().split("T")[0]
|
||||
},
|
||||
};
|
||||
@@ -102,7 +104,7 @@ const router = VueRouter.createRouter({
|
||||
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_RAUMINFO;
|
||||
: (isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP);
|
||||
|
||||
// default to today date if not provided
|
||||
const d = new Date(route.params.focus_date)
|
||||
@@ -197,6 +199,26 @@ const router = VueRouter.createRouter({
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: `/Cis/StgOrgLvPlan/:mode?/:focus_date?/:stgkz?/:sem?/:verband?/:gruppe?`,
|
||||
name: 'StgOrgLvPlan',
|
||||
component: StgOrgLvPlan,
|
||||
props(route) {
|
||||
return {
|
||||
propsViewData: route.params
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: `/Cis/OtherLvPlan/:otherUid/:mode?/:focus_date?`,
|
||||
name: "OtherLvPlan",
|
||||
component: OtherLvPlan,
|
||||
props(route) {
|
||||
return {
|
||||
propsViewData: route.params
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: `/Cis4`,
|
||||
name: 'Cis4',
|
||||
@@ -227,26 +249,22 @@ const router = VueRouter.createRouter({
|
||||
})
|
||||
|
||||
const app = Vue.createApp({
|
||||
name: 'FhcApp',
|
||||
name: 'CisApp',
|
||||
data: () => ({
|
||||
appSideMenuEntries: {},
|
||||
renderers: null,
|
||||
windowWidth: 0,
|
||||
}),
|
||||
components: {},
|
||||
computed: {
|
||||
isMobile() {
|
||||
const smallScreen = window.matchMedia("(max-width: 767px)").matches;
|
||||
const touchCapable = ("ontouchstart" in window) || navigator.maxTouchPoints > 0;
|
||||
return smallScreen;// && touchCapable;
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return { // provide injectable & watchable language property
|
||||
language: Vue.computed(() => this.$p.user_language),
|
||||
renderers: Vue.computed(() => this.renderers),
|
||||
isMobile: this.isMobile
|
||||
isMobile: Vue.computed(() => this.isMobile),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isMobile: function() {
|
||||
return (this.windowWidth < 767);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isInternalRoute(href) {
|
||||
const internalBase = window.location.origin
|
||||
@@ -280,54 +298,21 @@ const app = Vue.createApp({
|
||||
this.$router.push(route);
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
handleWindowResize() {
|
||||
this.windowWidth = window.innerWidth;
|
||||
},
|
||||
},
|
||||
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;
|
||||
}
|
||||
});
|
||||
created() {
|
||||
this.windowWidth = window.innerWidth;
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
document.addEventListener('click', this.handleClick);
|
||||
|
||||
window.addEventListener("resize", this.handleWindowResize);
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('click', this.handleClick);
|
||||
window.removeEventListener("resize", this.handleWindowResize);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -353,4 +338,4 @@ app.mount('#fhccontent');
|
||||
|
||||
router.afterEach((to, from, failure) => {
|
||||
app.config.globalProperties.$api.call(ApiRouteInfo.info('cis4', to.fullPath));
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,12 @@
|
||||
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";
|
||||
import FhcSearchbar from "../../components/searchbar/searchbar.js";
|
||||
import CisMenu from "../../components/Cis/Menu.js";
|
||||
import PluginsPhrasen from '../../plugins/Phrasen.js';
|
||||
import Theme from "../../plugins/Theme.js";
|
||||
import ApiSearchbar from '../../api/factory/searchbar.js';
|
||||
import ApiLvPlan from "../../api/factory/lvPlan.js";
|
||||
|
||||
const app = Vue.createApp({
|
||||
name: 'CisApp',
|
||||
name: 'CisMenuApp',
|
||||
components: {
|
||||
FhcSearchbar,
|
||||
CisMenu
|
||||
@@ -133,14 +134,61 @@ const app = Vue.createApp({
|
||||
childactions: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
windowWidth: 0,
|
||||
};
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
isNarrow: Vue.computed(() => this.windowWidth < 992),
|
||||
isMobile: Vue.computed(() => this.windowWidth < 767),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
searchfunction: function(searchsettings) {
|
||||
return this.$api.call(ApiSearchbar.searchCis(searchsettings));
|
||||
}
|
||||
}
|
||||
},
|
||||
handleWindowResize() {
|
||||
this.windowWidth = window.innerWidth;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.windowWidth = window.innerWidth;
|
||||
},
|
||||
async mounted() {
|
||||
const openOtherLvPlanAction = {
|
||||
label: Vue.computed(() => this.$p.t("lehre/stundenplan")),
|
||||
icon: "fas fa-calendar-days",
|
||||
type: "link",
|
||||
action: function (data) {
|
||||
const uid = JSON.parse(data.data).uid;
|
||||
const link =
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root +
|
||||
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
|
||||
"/Cis/OtherLvPlan/" +
|
||||
uid;
|
||||
return link;
|
||||
},
|
||||
};
|
||||
let checkPermissionOtherLvPlanResult = await this.$api.call(
|
||||
ApiLvPlan.checkPermissionOtherLvPlan(),
|
||||
);
|
||||
if (
|
||||
checkPermissionOtherLvPlanResult.meta.status === "success" &&
|
||||
checkPermissionOtherLvPlanResult.data
|
||||
) {
|
||||
this.searchbaroptions.actions.employee.childactions.push(
|
||||
openOtherLvPlanAction,
|
||||
);
|
||||
this.searchbaroptions.actions.student.childactions.push(
|
||||
openOtherLvPlanAction,
|
||||
);
|
||||
}
|
||||
window.addEventListener("resize", this.handleWindowResize);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("resize", this.handleWindowResize);
|
||||
},
|
||||
});
|
||||
|
||||
FhcApps.makeExtendable(app);
|
||||
@@ -3,13 +3,10 @@ import DashboardAdmin from '../../components/Dashboard/Admin.js';
|
||||
|
||||
import PluginsPhrasen from '../../plugins/Phrasen.js';
|
||||
|
||||
import ApiRenderers from '../../api/factory/renderers.js';
|
||||
|
||||
const app = Vue.createApp({
|
||||
name: 'DashboardAdminApp',
|
||||
data: () => ({
|
||||
appSideMenuEntries: {},
|
||||
renderers: null
|
||||
appSideMenuEntries: {}
|
||||
}),
|
||||
components: {
|
||||
CoreNavigationCmpt,
|
||||
@@ -17,51 +14,16 @@ const app = Vue.createApp({
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
// TODO(chris): move those two into the components that need it
|
||||
renderers: Vue.computed(() => this.renderers),
|
||||
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.$api
|
||||
.call(ApiRenderers.loadRenderers())
|
||||
.then(res => {
|
||||
for (let rendertype of Object.keys(res.data)) {
|
||||
let modalTitle = null;
|
||||
let modalContent = null;
|
||||
let calendarEvent = null;
|
||||
if (res.data[rendertype].modalTitle)
|
||||
modalTitle = Vue.markRaw(Vue.defineAsyncComponent(() => import(res.data[rendertype].modalTitle)));
|
||||
if (res.data[rendertype].modalContent)
|
||||
modalContent = Vue.markRaw(Vue.defineAsyncComponent(() => import(res.data[rendertype].modalContent)));
|
||||
if (res.data[rendertype].calendarEvent)
|
||||
calendarEvent = Vue.markRaw(Vue.defineAsyncComponent(() => import(res.data[rendertype].calendarEvent)));
|
||||
|
||||
if (res.data[rendertype].calendarEventStyles) {
|
||||
var head = document.head;
|
||||
if (!head.querySelector(`link[href="${res.data[rendertype].calendarEventStyles}"]`)) {
|
||||
var link = document.createElement("link");
|
||||
link.type = "text/css";
|
||||
link.rel = "stylesheet";
|
||||
link.href = res.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;
|
||||
}
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemErrors);
|
||||
}
|
||||
});
|
||||
app.use(primevue.config.default, {
|
||||
zIndex: {
|
||||
overlay: 9000,
|
||||
tooltip: 8000
|
||||
}
|
||||
})
|
||||
app.use(PluginsPhrasen);
|
||||
app.directive('tooltip', primevue.tooltip);
|
||||
app.mount('#main');
|
||||
@@ -1,16 +0,0 @@
|
||||
import {CoreNavigationCmpt} from '../components/navigation/Navigation.js';
|
||||
import DashboardAdmin from '../components/Dashboard/Admin.js';
|
||||
import Phrases from "../plugin/Phrasen.js"
|
||||
|
||||
Vue.createApp({
|
||||
name: 'DashboardAdminApp',
|
||||
data: () => ({
|
||||
appSideMenuEntries: {}
|
||||
}),
|
||||
components: {
|
||||
CoreNavigationCmpt,
|
||||
DashboardAdmin
|
||||
},
|
||||
mounted() {
|
||||
}
|
||||
}).use(Phrases).mount('#main');
|
||||
@@ -12,74 +12,13 @@ const router = VueRouter.createRouter({
|
||||
component: LVVerwaltung
|
||||
},
|
||||
{
|
||||
name: `byEmp`,
|
||||
path: `/emp/:studiensemester_kurzbz/:emp/:stg?/:semester?`,
|
||||
name: 'stdsem',
|
||||
path: '/stdsem/:stdsem',
|
||||
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`,
|
||||
path: `/fachbereich/:fachbereich/:emp?`,
|
||||
component: LVVerwaltung
|
||||
},*/
|
||||
{
|
||||
name: `byStg`,
|
||||
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();
|
||||
}
|
||||
children: [
|
||||
{ name: 'emp', path: 'emp/:emp/:treemenu(.*)*', component: LVVerwaltung },
|
||||
{ name: 'treemenu', path: ':treemenu(.*)*', component: LVVerwaltung }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
@@ -91,7 +30,9 @@ const router = VueRouter.createRouter({
|
||||
|
||||
FhcApps.router.makeExtendable(router);
|
||||
|
||||
const app = Vue.createApp();
|
||||
const app = Vue.createApp({
|
||||
name: 'LvVwApp'
|
||||
});
|
||||
|
||||
FhcApps.makeExtendable(app);
|
||||
|
||||
|
||||
@@ -24,167 +24,27 @@ import PluginsPhrasen from "../plugins/Phrasen.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(),
|
||||
history: VueRouter.createWebHistory('/' + ciPath),
|
||||
routes: [
|
||||
{
|
||||
name: 'index',
|
||||
path: `/${ciPath}/studentenverwaltung`,
|
||||
component: FhcStudentenverwaltung
|
||||
},
|
||||
{
|
||||
name: 'studiensemester',
|
||||
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz`,
|
||||
path: '/studvw',
|
||||
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'});
|
||||
children: [
|
||||
{
|
||||
name: 'stdsem',
|
||||
path: 'stdsem/:stdsem',
|
||||
component: FhcStudentenverwaltung,
|
||||
children: [
|
||||
{ name: 'search', path: 'search/:query', component: FhcStudentenverwaltung },
|
||||
{ name: 'searchtypes', path: 'search/:types/:query', component: FhcStudentenverwaltung },
|
||||
{ name: 'prestudent', path: 'prestudent/:prestudent_id', component: FhcStudentenverwaltung },
|
||||
{ name: 'student', path: 'student/:student_uid', component: FhcStudentenverwaltung },
|
||||
{ name: 'person', path: 'person/:person_id', component: FhcStudentenverwaltung },
|
||||
{ name: 'treemenu', path: ':treemenu(.*)*', component: FhcStudentenverwaltung },
|
||||
]
|
||||
}
|
||||
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();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/search/:searchstr`,
|
||||
component: FhcStudentenverwaltung,
|
||||
props(route) {
|
||||
return {
|
||||
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
|
||||
url_mode: 'search',
|
||||
url_prestudent_id: route.params.searchstr
|
||||
};
|
||||
},
|
||||
beforeEnter(to, from, next) {
|
||||
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
|
||||
if (!isSemester) {
|
||||
return next({name: 'index'});
|
||||
}
|
||||
next();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'search_w_types',
|
||||
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/search/:types/:searchstr`,
|
||||
component: FhcStudentenverwaltung,
|
||||
props(route) {
|
||||
return {
|
||||
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
|
||||
url_mode: 'search',
|
||||
url_prestudent_id: route.params.type + '/' + route.params.searchstr
|
||||
};
|
||||
},
|
||||
beforeEnter(to, from, next) {
|
||||
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
|
||||
if (!isSemester) {
|
||||
return next({name: 'index'});
|
||||
}
|
||||
next();
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
@@ -197,11 +57,16 @@ const router = VueRouter.createRouter({
|
||||
|
||||
router.afterEach((to, from, failure) => {
|
||||
let title = 'Studierendenverwaltung FH-Complete';
|
||||
if (to.params.studiengang) {
|
||||
title = to.params.studiengang + ' ' + title;
|
||||
if (to.params.treemenu) {
|
||||
const index = to.params.treemenu.findIndex(
|
||||
(e, i) => i%2 == 0 && e == 'stg'
|
||||
);
|
||||
if (index >= 0) {
|
||||
title = to.params.treemenu[index + 1].toUpperCase() + ' ' + title;
|
||||
}
|
||||
}
|
||||
if (to.params.studiensemester_kurzbz) {
|
||||
title = to.params.studiensemester_kurzbz + ' ' + title;
|
||||
if (to.params.stdsem) {
|
||||
title = to.params.stdsem.toUpperCase() + ' ' + title;
|
||||
}
|
||||
document.title = title;
|
||||
});
|
||||
@@ -209,7 +74,7 @@ router.afterEach((to, from, failure) => {
|
||||
FhcApps.router.makeExtendable(router);
|
||||
|
||||
const app = Vue.createApp({
|
||||
name: 'StudentenverwaltungApp'
|
||||
name: 'StudVwApp'
|
||||
});
|
||||
|
||||
FhcApps.makeExtendable(app);
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
import TreemenuEntry from './Treemenu/Entry.js';
|
||||
|
||||
import dragClick from '../../directives/dragClick.js';
|
||||
|
||||
import ApiTreemenu from '../../api/factory/treemenu.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PvTreetable: primevue.treetable,
|
||||
PvColumn: primevue.column,
|
||||
TreemenuEntry
|
||||
},
|
||||
directives: {
|
||||
dragClick
|
||||
},
|
||||
emits: [
|
||||
'selectEntry',
|
||||
'drop'
|
||||
],
|
||||
props: {
|
||||
config: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
preselectedKey: {
|
||||
type: Array,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
nodes: [],
|
||||
selectedKey: [],
|
||||
expandedKeys: {},
|
||||
filters: {}, // TODO(chris): filter only 1st level?
|
||||
favorites: {on: false, list: []}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredNodes() {
|
||||
if (this.favorites.on)
|
||||
return this.nodes.filter(node => this.favorites.list.includes(node.data.path));
|
||||
|
||||
return this.nodes;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
preselectedKey(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.setPreselection();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reloadNodesWithProp(prop, nodes = undefined) {
|
||||
if (!nodes)
|
||||
nodes = this.nodes;
|
||||
|
||||
nodes.forEach(node => {
|
||||
if (node.data[prop]) {
|
||||
// reload
|
||||
delete node.children;
|
||||
this.onExpandTreeNode(node);
|
||||
} else if (node.children) {
|
||||
this.reloadNodesWithProp(prop, node.children);
|
||||
}
|
||||
});
|
||||
},
|
||||
findNodeByKey(key, arr) {
|
||||
if (!arr)
|
||||
arr = this.nodes;
|
||||
let res = arr.filter(n => n.key == key);
|
||||
if (res.length)
|
||||
return res.pop();
|
||||
res = arr.map(n => n.children ? this.findNodeByKey(key, n.children) : null).filter(a => a);
|
||||
if (res.length)
|
||||
return res.pop();
|
||||
return null;
|
||||
},
|
||||
async onExpandTreeNode(node) {
|
||||
if (!node.children) {
|
||||
if (node.data.path) {
|
||||
/**
|
||||
* NOTE(chris): activeEl is for keyboard navigation to
|
||||
* prevent the focus jumping down to the next parent
|
||||
* instead of the current submenu entry (which is not yet
|
||||
* loaded)
|
||||
*/
|
||||
let activeEl = null;
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
activeEl = document.activeElement;
|
||||
});
|
||||
});
|
||||
this.loading = true;
|
||||
|
||||
return this.$api
|
||||
.call(ApiTreemenu.get(this.config, node.data.path))
|
||||
.then(result => {
|
||||
const subNodes = result.data.map(this.mapResultToTreeData);
|
||||
const realNode = this.findNodeByKey(node.key);
|
||||
if (realNode)
|
||||
realNode.children = subNodes;
|
||||
else
|
||||
node.children = subNodes; // NOTE(chris): fallback should never be the case
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (activeEl != document.activeElement)
|
||||
return;
|
||||
|
||||
let treeitem = this.$refs.tree.$el.querySelector('[data-tree-item-key="' + node.key + '"]');
|
||||
if (!treeitem)
|
||||
return;
|
||||
|
||||
treeitem = treeitem.closest('[role="row"]');
|
||||
|
||||
if (!treeitem)
|
||||
return;
|
||||
|
||||
treeitem.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
code: 'ArrowDown',
|
||||
key: 'ArrowDown'
|
||||
}));
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
}
|
||||
}
|
||||
},
|
||||
onSelectTreeNode(node) {
|
||||
this.$emit('selectEntry', node.data);
|
||||
},
|
||||
mapNodesToNoSemReloadNodes(result, node) {
|
||||
if (node.data.no_sem_reload)
|
||||
result.push(node);
|
||||
if (node.children)
|
||||
result = node.children.reduce(this.mapNodesToNoSemReloadNodes, result);
|
||||
return result;
|
||||
},
|
||||
mapResultToTreeData(el) {
|
||||
const cp = {
|
||||
key: ("" + el.path).replace(/\//g, '-'),
|
||||
data: el,
|
||||
label: el.name // TODO(chris): phrase
|
||||
};
|
||||
|
||||
if (el.children)
|
||||
cp.children = el.children.map(this.mapResultToTreeData);
|
||||
else
|
||||
cp.leaf = el.leaf || false;
|
||||
|
||||
return cp;
|
||||
},
|
||||
async setPreselection()
|
||||
{
|
||||
if (!this.preselectedKey || !this.preselectedKey.length)
|
||||
{
|
||||
this.selectedKey = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.preselectedKey || !Array.isArray(this.preselectedKey))
|
||||
return;
|
||||
|
||||
// [1, 2, 3, 4, ...] => ['1-2', '3-4', ...]
|
||||
const parts = this.preselectedKey.reduce((r, c, i, a) => (i%2 && r.push(a[i-1] + '-' + c), r), []);
|
||||
let currentKey = parts[0];
|
||||
let currentNode = this.findNodeByKey(currentKey);
|
||||
|
||||
if (!currentNode)
|
||||
return;
|
||||
|
||||
if(this.selectedKey)
|
||||
{
|
||||
const currentSelectedKey = Object.keys(this.selectedKey).find(Boolean);
|
||||
if (currentSelectedKey) {
|
||||
if (currentSelectedKey == currentKey)
|
||||
return;
|
||||
/**
|
||||
* Do not select a new entry if the current is a child of the new one.
|
||||
* This happens if a child entry of a new stg is selected and the router
|
||||
* tries to select the stg root entry (because subtrees do not have
|
||||
* routes yet)
|
||||
*/
|
||||
const isChild = this.findNodeByKey(
|
||||
currentSelectedKey,
|
||||
currentNode.children || []
|
||||
);
|
||||
if (isChild)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 1; i < parts.length; i++)
|
||||
{
|
||||
this.expandedKeys[currentNode.key] = true;
|
||||
|
||||
await this.onExpandTreeNode(currentNode);
|
||||
|
||||
currentKey += '-' + parts[i];
|
||||
currentNode = this.findNodeByKey(currentKey);
|
||||
|
||||
if (!currentNode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedKey = {[currentNode.key]: true};
|
||||
this.onSelectTreeNode(currentNode);
|
||||
},
|
||||
async toggleTreeNode(node) {
|
||||
if (this.expandedKeys[node.key]) {
|
||||
delete this.expandedKeys[node.key];
|
||||
} else if (!node.leaf) {
|
||||
await this.onExpandTreeNode(node);
|
||||
this.expandedKeys[node.key] = true;
|
||||
}
|
||||
},
|
||||
filterFav() {
|
||||
this.favorites.on = !this.favorites.on;
|
||||
this.$api
|
||||
.call(ApiTreemenu.favorites.set(
|
||||
this.config,
|
||||
JSON.stringify(this.favorites)
|
||||
));
|
||||
},
|
||||
markFav(key) {
|
||||
let index = this.favorites.list.indexOf(key.data.path + '');
|
||||
|
||||
if (index != -1) {
|
||||
this.favorites.list.splice(index, 1);
|
||||
} else {
|
||||
this.favorites.list.push(key.data.path + '');
|
||||
}
|
||||
|
||||
this.$api
|
||||
.call(ApiTreemenu.favorites.set(
|
||||
this.config,
|
||||
JSON.stringify(this.favorites)
|
||||
));
|
||||
},
|
||||
unsetFavFocus(e) {
|
||||
if (e.target.dataset?.linkFavAdd !== undefined) {
|
||||
e.target.tabIndex = -1;
|
||||
} else {
|
||||
let items = e.target.querySelectorAll('[data-link-fav-add]:not([tabindex="-1"])');
|
||||
items.forEach(el => el.tabIndex = document.activeElement == el ? 0 : -1);
|
||||
}
|
||||
},
|
||||
setFavFocus(e) {
|
||||
if (e.target.dataset?.linkFavAdd !== undefined) {
|
||||
e.target.tabIndex = 0;
|
||||
} else {
|
||||
let items = e.target.querySelectorAll('[data-link-fav-add][tabindex="-1"]');
|
||||
items.forEach(el => el.tabIndex = 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$api
|
||||
.call(ApiTreemenu.get(this.config))
|
||||
.then(result => {
|
||||
this.nodes = result.data.map(el => {
|
||||
el.root = true;
|
||||
return this.mapResultToTreeData(el);
|
||||
});
|
||||
this.setPreselection();
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
|
||||
this.$api
|
||||
.call(ApiTreemenu.favorites.get(this.config))
|
||||
.then(result => {
|
||||
if (result.data) {
|
||||
this.favorites = JSON.parse(result.data);
|
||||
}
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
template: /* html */`
|
||||
<pv-treetable
|
||||
ref="tree"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
v-model:selection-keys="selectedKey"
|
||||
class="menu p-treetable-sm"
|
||||
:value="filteredNodes"
|
||||
selection-mode="single"
|
||||
scrollable
|
||||
scroll-height="flex"
|
||||
:filters="filters"
|
||||
@node-expand="onExpandTreeNode"
|
||||
@node-select="onSelectTreeNode"
|
||||
@focusin="setFavFocus"
|
||||
@focusout="unsetFavFocus"
|
||||
>
|
||||
<pv-column
|
||||
field="name"
|
||||
expander
|
||||
class="text-break"
|
||||
>
|
||||
<template #header>
|
||||
<div class="text-right">
|
||||
<div class="p-input-icon-left">
|
||||
<i class="pi pi-search"></i>
|
||||
<input
|
||||
type="text"
|
||||
v-model="filters['global']"
|
||||
class="form-control ps-5"
|
||||
placeholder="Search"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body="{ node }">
|
||||
<treemenu-entry
|
||||
:node="node"
|
||||
:data-tree-item-key="node.key"
|
||||
v-drag-click="() => toggleTreeNode(node)"
|
||||
@drop="$emit('drop', $event)"
|
||||
/>
|
||||
</template>
|
||||
</pv-column>
|
||||
<pv-column
|
||||
field="fav"
|
||||
class="flex-shrink-0 flex-grow-0"
|
||||
header-class="flex-shrink-0 flex-grow-0"
|
||||
>
|
||||
<template #header>
|
||||
<a
|
||||
v-if="favorites.on || favorites.list.length"
|
||||
href="#"
|
||||
@click.prevent="filterFav"
|
||||
>
|
||||
<i
|
||||
:class="favorites.on ? 'fa-solid' : 'fa-regular'"
|
||||
class="fa-star"
|
||||
></i>
|
||||
</a>
|
||||
</template>
|
||||
<template #body="{ node }">
|
||||
<a
|
||||
v-if="node.data.root"
|
||||
href="#"
|
||||
tabindex="-1"
|
||||
data-link-fav-add
|
||||
@click.prevent="markFav(node)"
|
||||
@keydown.enter.stop.prevent="markFav(node)"
|
||||
>
|
||||
<i
|
||||
:class="favorites.list.includes(node.data.path + '') ? 'fa-solid' : 'fa-regular'"
|
||||
class="fa-star"
|
||||
></i>
|
||||
</a>
|
||||
</template>
|
||||
</pv-column>
|
||||
<pv-column field="search" class="d-none"></pv-column>
|
||||
</pv-treetable>`
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import drop from '../../../directives/drop.js';
|
||||
import draggable from '../../../directives/draggable.js';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
drop,
|
||||
draggable
|
||||
},
|
||||
emits: [
|
||||
'drop'
|
||||
],
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
name() {
|
||||
if (Array.isArray(this.node.data.name))
|
||||
return this.$p.t(this.node.data.name);
|
||||
|
||||
return this.node.data.name;
|
||||
},
|
||||
title() {
|
||||
if (!this.node.data.title)
|
||||
return this.name;
|
||||
|
||||
if (Array.isArray(this.node.data.title))
|
||||
return this.$p.t(this.node.data.title);
|
||||
|
||||
return this.node.data.title;
|
||||
},
|
||||
dropConfig() {
|
||||
if (!this.node.data?.droplink)
|
||||
return null;
|
||||
|
||||
const allowed = [ ...this.node.data.droplink ];
|
||||
const effect = allowed.shift();
|
||||
|
||||
return { effect, allowed };
|
||||
},
|
||||
dragConfig() {
|
||||
if (!this.node.data?.draggable)
|
||||
return null;
|
||||
|
||||
let config = [ ...this.node.data.draggable ];
|
||||
const effect = config.shift();
|
||||
const value = JSON.parse(config.shift());
|
||||
|
||||
return { effect, value };
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<span
|
||||
class="treemenu-entry d-flex align-items-center w-100 h-100"
|
||||
:title="title"
|
||||
v-drop:[dropConfig]="(evt, data) => $emit('drop', { drop: node.data, drag: data })"
|
||||
v-draggable:[dragConfig?.effect]="dragConfig?.value"
|
||||
>
|
||||
{{ name }}
|
||||
</span>`
|
||||
};
|
||||
@@ -433,6 +433,7 @@ export default {
|
||||
:label="$p.t('wawi/ausgegebenam')"
|
||||
name="ausgegebenam"
|
||||
v-model="formData.ausgegebenam"
|
||||
model-type="yyyy-MM-dd"
|
||||
auto-apply
|
||||
:enable-time-picker="false"
|
||||
text-input
|
||||
@@ -449,6 +450,7 @@ export default {
|
||||
:label="$p.t('wawi/retouram')"
|
||||
name="retouram"
|
||||
v-model="formData.retouram"
|
||||
model-type="yyyy-MM-dd"
|
||||
auto-apply
|
||||
:enable-time-picker="false"
|
||||
text-input
|
||||
|
||||
@@ -4,7 +4,9 @@ export default {
|
||||
name: 'BootstrapModal',
|
||||
data: () => ({
|
||||
modal: null,
|
||||
fullscreen: false
|
||||
fullscreen: false,
|
||||
expandBtnHovered: false,
|
||||
expandBtnFocused: false,
|
||||
}),
|
||||
props: {
|
||||
backdrop: {
|
||||
@@ -70,6 +72,29 @@ export default {
|
||||
this.$emit('toggleFullscreen')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getExpandButtonStyles() {
|
||||
const hovered = this.expandBtnHovered;
|
||||
const focused = this.expandBtnFocused;
|
||||
return `display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-size: 1em;
|
||||
opacity: 0.5;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
transition: opacity 0.15s ease;
|
||||
opacity: ${focused ? '1' : hovered ? '0.75' : '0.5'};
|
||||
outline: ${focused ? '1px solid currentColor' : 'none'};
|
||||
outline-offset: 2px;`
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$refs.modal)
|
||||
this.modal = new bootstrap.Modal(this.$refs.modal, {
|
||||
@@ -140,9 +165,13 @@ export default {
|
||||
<div class="d-flex align-items-center ms-auto gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn mb-1"
|
||||
:style="getExpandButtonStyles"
|
||||
v-if="allowFullscreenExpand"
|
||||
@click="toggleFullscreen"
|
||||
@mouseenter="expandBtnHovered = true"
|
||||
@mouseleave="expandBtnHovered = false"
|
||||
@focusin="expandBtnFocused = true"
|
||||
@focusout="expandBtnFocused = false"
|
||||
:aria-label="fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'"
|
||||
>
|
||||
<i v-if="!fullscreen" class="fa-solid fa-expand"></i>
|
||||
|
||||
@@ -20,7 +20,8 @@ export default {
|
||||
},
|
||||
inject: {
|
||||
mode: "mode",
|
||||
dropableEvents: "dropableEvents"
|
||||
dropableEvents: "dropableEvents",
|
||||
timezone: "timezone"
|
||||
},
|
||||
props: {
|
||||
events: Array,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import LineEvent from './Line/Event.js';
|
||||
import LineBackground from './Line/Background.js';
|
||||
import LineEvent from "./Line/Event.js";
|
||||
import LineBackground from "./Line/Background.js";
|
||||
|
||||
/**
|
||||
* TODO(chris):
|
||||
@@ -10,54 +10,117 @@ export default {
|
||||
name: "GridLine",
|
||||
components: {
|
||||
LineEvent,
|
||||
LineBackground
|
||||
},
|
||||
inject: {
|
||||
axisRow: "axisRow"
|
||||
LineBackground,
|
||||
},
|
||||
inject: ["axisRow", "shouldCompactEvents", "compactibleEventTypes"],
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
start: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
end: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
events: {
|
||||
type: Array,
|
||||
default: []
|
||||
default: [],
|
||||
},
|
||||
backgrounds: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
eventsWithRowInfo() {
|
||||
const events = [];
|
||||
this.events.forEach(event => {
|
||||
const rows = [1, -1];
|
||||
formattedEvents() {
|
||||
let formattedEvents = this.events.map((event) => {
|
||||
event.rows = [1, -1];
|
||||
if (event.startsHere) {
|
||||
rows[0] = 't_' + event.start.diff(this.date).toMillis();
|
||||
event.rows[0] =
|
||||
"t_" + event.start.diff(this.date).toMillis();
|
||||
}
|
||||
if (event.endsHere) {
|
||||
rows[1] = 't_' + event.end.diff(this.date).toMillis();
|
||||
event.rows[1] = "t_" + event.end.diff(this.date).toMillis();
|
||||
}
|
||||
|
||||
events.push({
|
||||
...event,
|
||||
rows
|
||||
});
|
||||
return event;
|
||||
});
|
||||
return events;
|
||||
}
|
||||
|
||||
if (this.shouldCompactEvents && this.compactibleEventTypes?.length) {
|
||||
formattedEvents =
|
||||
this.compactEvents(formattedEvents, this.compactibleEventTypes);
|
||||
}
|
||||
|
||||
return formattedEvents;
|
||||
},
|
||||
},
|
||||
template: /* html */`
|
||||
methods: {
|
||||
compactEvents(events, compactibleEventTypes) {
|
||||
let formattedEvents = events
|
||||
.filter(
|
||||
(event) =>
|
||||
!compactibleEventTypes.includes(event.type),
|
||||
)
|
||||
.map((event) => {
|
||||
event.display = "default";
|
||||
return event;
|
||||
});
|
||||
let eventsToBeCompacted = events.filter((event) =>
|
||||
compactibleEventTypes.includes(event.type),
|
||||
);
|
||||
let compactedEvents = [];
|
||||
|
||||
eventsToBeCompacted.forEach((event) => {
|
||||
let existingCompactedEvent = compactedEvents.find(
|
||||
(compactedEvent) =>
|
||||
event.rows[0] === compactedEvent.rows[0] &&
|
||||
event.rows[1] === compactedEvent.rows[1],
|
||||
);
|
||||
|
||||
if (!existingCompactedEvent) {
|
||||
compactedEvents.push({
|
||||
events: [
|
||||
{
|
||||
farbe: event.orig.farbe,
|
||||
},
|
||||
],
|
||||
rows: event.rows,
|
||||
});
|
||||
} else {
|
||||
existingCompactedEvent.events.push({
|
||||
farbe: event.orig.farbe,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
compactedEvents.forEach((compactedEvent) => {
|
||||
if (compactedEvent.events.length < 4) {
|
||||
formattedEvents.push({
|
||||
display: "compacted",
|
||||
...compactedEvent,
|
||||
});
|
||||
} else {
|
||||
formattedEvents.push({
|
||||
display: "compacted",
|
||||
events: compactedEvent.events.slice(0, 3),
|
||||
rows: compactedEvent.rows,
|
||||
});
|
||||
formattedEvents.push({
|
||||
display: "compactedExtra",
|
||||
events: compactedEvent.events.slice(3),
|
||||
rows: compactedEvent.rows,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return formattedEvents;
|
||||
},
|
||||
},
|
||||
template: /* html */ `
|
||||
<div
|
||||
class="fhc-calendar-base-grid-line"
|
||||
style="position:relative;display:grid;grid-auto-flow:dense"
|
||||
@@ -69,17 +132,38 @@ export default {
|
||||
: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>
|
||||
<template v-for="(event, i) in formattedEvents" :key="i">
|
||||
<line-event
|
||||
v-if="!event.display || event.display === 'default'"
|
||||
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
|
||||
:event="event"
|
||||
>
|
||||
<template v-slot="slot">
|
||||
<slot name="event" v-bind="slot" />
|
||||
</template>
|
||||
</line-event>
|
||||
<div
|
||||
v-else-if="event.display === 'compacted'"
|
||||
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
|
||||
class="d-flex flex-row justify-content-center gap-1 align-items-center"
|
||||
>
|
||||
<span
|
||||
v-for="(subEvent, subEventIndex) in event.events"
|
||||
:key="subEventIndex"
|
||||
:style="subEvent.farbe ? {'background-color': '#' + subEvent.farbe} : {}"
|
||||
style="height:10px; width:10px;"
|
||||
class="border border-dark rounded-circle"
|
||||
></span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="event.display === 'compactedExtra'"
|
||||
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
|
||||
class="w-100 d-flex flex-row justify-content-center"
|
||||
>
|
||||
{{"+" + event.events.length}}
|
||||
</div>
|
||||
</template>
|
||||
<slot name="dropzone" />
|
||||
</div>
|
||||
`
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -3,24 +3,20 @@ import FhcCalendar from "./Base.js";
|
||||
import ApiLvPlan from '../../api/factory/lvPlan.js';
|
||||
|
||||
import { useEventLoader } from '../../composables/EventLoader.js';
|
||||
import { useRenderers } from '../../composables/Renderers.js';
|
||||
|
||||
import ModeDay from './Mode/Day.js';
|
||||
import ModeWeek from './Mode/Week.js';
|
||||
import ModeMonth from './Mode/Month.js';
|
||||
import ModeList from './Mode/List.js';
|
||||
|
||||
export default {
|
||||
name: "CalendarLvPlan",
|
||||
components: {
|
||||
FhcCalendar
|
||||
},
|
||||
inject: [
|
||||
"renderers"
|
||||
],
|
||||
inject: ["isMobile"],
|
||||
props: {
|
||||
timezone: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
date: {
|
||||
type: [Date, String, Number, luxon.DateTime],
|
||||
default: luxon.DateTime.local()
|
||||
@@ -34,6 +30,16 @@ export default {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
shouldCompactEvents: Vue.computed(
|
||||
() => this.$props.mode === "Month" && this.isMobile,
|
||||
),
|
||||
compactibleEventTypes: Vue.computed(
|
||||
() => this.compactibleEventTypes,
|
||||
),
|
||||
};
|
||||
},
|
||||
emits: [
|
||||
"update:date",
|
||||
"update:mode",
|
||||
@@ -41,11 +47,7 @@ export default {
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
modes: {
|
||||
day: Vue.markRaw(ModeDay),
|
||||
week: Vue.markRaw(ModeWeek),
|
||||
month: Vue.markRaw(ModeMonth)
|
||||
},
|
||||
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
|
||||
modeOptions: {
|
||||
day: {
|
||||
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
|
||||
@@ -53,9 +55,13 @@ export default {
|
||||
},
|
||||
week: {
|
||||
collapseEmptyDays: false
|
||||
}
|
||||
},
|
||||
list: {
|
||||
length: 7,
|
||||
},
|
||||
},
|
||||
teachingunits: null
|
||||
teachingunits: null,
|
||||
compactibleEventTypes: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -77,7 +83,20 @@ export default {
|
||||
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
modes() {
|
||||
let modes = {
|
||||
day: Vue.markRaw(ModeDay),
|
||||
month: Vue.markRaw(ModeMonth),
|
||||
};
|
||||
if (this.isMobile) {
|
||||
modes.list = Vue.markRaw(ModeList);
|
||||
} else {
|
||||
modes.week = Vue.markRaw(ModeWeek);
|
||||
}
|
||||
|
||||
return modes;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
eventStyle(event) {
|
||||
@@ -88,33 +107,47 @@ export default {
|
||||
updateRange(rangeInterval) {
|
||||
this.rangeInterval = rangeInterval;
|
||||
this.$emit('update:range', rangeInterval);
|
||||
}
|
||||
},
|
||||
resetEventLoader() {
|
||||
this.reset();
|
||||
},
|
||||
async getStunden() {
|
||||
let stundenResponse = await this.$api.call(ApiLvPlan.getStunden());
|
||||
this.teachingunits = stundenResponse.data.map((el) => ({
|
||||
id: el.stunde,
|
||||
start: el.beginn,
|
||||
end: el.ende,
|
||||
}));
|
||||
},
|
||||
async getCompactibleEventTypes() {
|
||||
let compactibleEventTypesResponse = await this.$api.call(
|
||||
ApiLvPlan.getCompactibleEventTypes(),
|
||||
);
|
||||
this.compactibleEventTypes = compactibleEventTypesResponse.data;
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const rangeInterval = Vue.ref(null);
|
||||
|
||||
const { events, lv } = useEventLoader(rangeInterval, props.getPromiseFunc);
|
||||
const { events, lv, reset } = useEventLoader(rangeInterval, props.getPromiseFunc);
|
||||
|
||||
Vue.watch(lv, newValue => {
|
||||
context.emit('update:lv', newValue);
|
||||
});
|
||||
|
||||
const { renderers } = useRenderers();
|
||||
|
||||
return {
|
||||
rangeInterval,
|
||||
events,
|
||||
lv
|
||||
lv,
|
||||
reset,
|
||||
renderers
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.$api
|
||||
.call(ApiLvPlan.getStunden())
|
||||
.then(res => {
|
||||
return this.teachingunits = res.data.map(el => ({
|
||||
id: el.stunde,
|
||||
start: el.beginn,
|
||||
end: el.ende
|
||||
}));
|
||||
});
|
||||
async created() {
|
||||
await this.getStunden();
|
||||
await this.getCompactibleEventTypes();
|
||||
},
|
||||
template: /* html */`
|
||||
<fhc-calendar
|
||||
@@ -136,6 +169,16 @@ export default {
|
||||
>
|
||||
<template v-slot="{ event, mode }">
|
||||
<div
|
||||
v-if="!event"
|
||||
class="h-100 d-flex justify-content-center align-items-center"
|
||||
>
|
||||
{{ $p.t('lehre/noLvFound') }}
|
||||
</div>
|
||||
<div v-else-if="!renderers || !renderers[event.type]" class="placeholder-glow">
|
||||
<span class="placeholder col-12"></span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
|
||||
:type="mode == 'day' ? 'button' : undefined"
|
||||
:style="eventStyle(event)"
|
||||
@@ -154,6 +197,9 @@ export default {
|
||||
v-else
|
||||
:is="renderers[event.type]?.calendarEvent"
|
||||
:event="event"
|
||||
:timeSlotDisplayBehavior="
|
||||
$props.mode.toLowerCase() === 'list' ? 'always' : 'default'
|
||||
"
|
||||
></component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -93,7 +93,7 @@ export default {
|
||||
mounted() {
|
||||
this.$emit('update:range', this.range);
|
||||
},
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<div
|
||||
class="fhc-calendar-mode-list flex-grow-1 position-relative"
|
||||
@cal-click-default.capture="handleClickDefaults"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import FhcCalendar from "./Base.js";
|
||||
|
||||
import { useEventLoader } from '../../composables/EventLoader.js';
|
||||
import { useRenderers } from '../../composables/Renderers.js';
|
||||
|
||||
import ModeList from '../Calendar/Mode/List.js';
|
||||
|
||||
@@ -9,22 +10,17 @@ export default {
|
||||
components: {
|
||||
FhcCalendar
|
||||
},
|
||||
inject: [
|
||||
"renderers"
|
||||
],
|
||||
props: {
|
||||
timezone: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
getPromiseFunc: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const timezone = FHC_JS_DATA_STORAGE_OBJECT.timezone;
|
||||
return {
|
||||
now: luxon.DateTime.now().setZone(this.timezone),
|
||||
timezone,
|
||||
now: luxon.DateTime.now().setZone(timezone),
|
||||
modes: {
|
||||
list: Vue.markRaw(ModeList)
|
||||
},
|
||||
@@ -59,10 +55,12 @@ export default {
|
||||
const rangeInterval = Vue.ref(null);
|
||||
|
||||
const { events } = useEventLoader(rangeInterval, props.getPromiseFunc);
|
||||
const { renderers } = useRenderers();
|
||||
|
||||
return {
|
||||
rangeInterval,
|
||||
events
|
||||
events,
|
||||
renderers
|
||||
};
|
||||
},
|
||||
template: /* html */`
|
||||
@@ -81,6 +79,9 @@ export default {
|
||||
>
|
||||
{{ $p.t('lehre/noLvFound') }}
|
||||
</div>
|
||||
<div v-else-if="!renderers || !renderers[event.type]" class="placeholder-glow">
|
||||
<span class="placeholder col-12"></span>
|
||||
</div>
|
||||
<component
|
||||
v-else-if="mode == 'eventheader'"
|
||||
:is="renderers[event.type]?.modalTitle"
|
||||
@@ -99,6 +100,7 @@ export default {
|
||||
<component
|
||||
:is="renderers[event.type]?.calendarEvent"
|
||||
:event="event"
|
||||
:timeSlotDisplayBehavior="'always'"
|
||||
></component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,8 @@ import BsModal from '../../Bootstrap/Modal.js';
|
||||
import VueDatePicker from '../../vueDatepicker.js.php';
|
||||
import ApiAbgabe from '../../../api/factory/abgabe.js'
|
||||
import { getDateStyleClass } from "./getDateStyleClass.js";
|
||||
import {compareISODateValues, formatDateTime, formatISODate, getViennaTodayISO} from "./dateUtils.js";
|
||||
|
||||
|
||||
export const AbgabeMitarbeiterDetail = {
|
||||
name: "AbgabeMitarbeiterDetail",
|
||||
@@ -46,12 +48,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
eidAkzeptiert: false,
|
||||
enduploadTermin: null,
|
||||
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
|
||||
speedDialItems: [{
|
||||
label: Vue.computed(() => this.$p.t('abgabetool/c4newAbgabetermin')),
|
||||
icon: "fa fa-plus",
|
||||
command: this.openCreateNewAbgabeModal,
|
||||
disabled: Vue.computed(() => !this.getAllowedToCreateNewTermin)
|
||||
},
|
||||
speedDialItems: [
|
||||
{
|
||||
label: Vue.computed(() => this.$p.t('abgabetool/c4benoten')),
|
||||
icon: "fa fa-user-check",
|
||||
@@ -81,6 +78,22 @@ export const AbgabeMitarbeiterDetail = {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getSavedTerminInfoString(termin) {
|
||||
const isUpdate = termin.updateamum != null;
|
||||
|
||||
const fullname = isUpdate
|
||||
? termin.updatevon_fullname
|
||||
: termin.insertvon_fullname;
|
||||
|
||||
const datetime = isUpdate
|
||||
? termin.updateamum
|
||||
: termin.insertamum;
|
||||
|
||||
return this.$p.t('ui/savedAtByV3', [formatDateTime(datetime), fullname])
|
||||
},
|
||||
terminIsInvalid(termin) {
|
||||
return termin.note?.positiv == false && !termin.beurteilungsnotiz
|
||||
},
|
||||
getNoteBezeichnung(termin){
|
||||
if(termin.noteBackend?.bezeichnung) {
|
||||
return termin.noteBackend?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌'
|
||||
@@ -113,6 +126,8 @@ export const AbgabeMitarbeiterDetail = {
|
||||
if(newTerminRes.note) {
|
||||
newTerminRes.note = noteOpt
|
||||
newTerminRes.noteBackend = noteOpt // certain UI elements should only reflect persisted state
|
||||
termin.allowedToDelete = false
|
||||
newTerminRes.allowedToDelete = false
|
||||
}
|
||||
newTerminRes.invertedFixtermin = !newTerminRes.fixtermin
|
||||
const existingTerminRes = res.data[1]
|
||||
@@ -132,13 +147,13 @@ export const AbgabeMitarbeiterDetail = {
|
||||
} else {
|
||||
const noteOptExisting = this.allowedNotenOptions.find(opt => opt.note == existingTerminRes.note)
|
||||
existingTerminRes.note = noteOptExisting
|
||||
|
||||
|
||||
termin.paabgabetyp_kurzbz = newTerminRes.paabgabetyp_kurzbz
|
||||
termin.noteBackend = noteOpt // do NOT take noteOptExisting -> should reflect the "yes the qgate grade is confirmed in backend ux behaviour"
|
||||
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
|
||||
}
|
||||
|
||||
this.projektarbeit.abgabetermine.sort((a, b) =>new Date(a.datum) - new Date(b.datum))
|
||||
this.projektarbeit.abgabetermine.sort((a, b) => compareISODateValues(a.datum, b.datum))
|
||||
|
||||
const index = this.projektarbeit.abgabetermine.findIndex(t => termin.paabgabe_id == t.paabgabe_id)
|
||||
|
||||
@@ -159,7 +174,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
'fixtermin': false,
|
||||
'invertedFixtermin': true,
|
||||
'kurzbz': '', // todo kurzbz textfield value vorschlag für qualgates
|
||||
'datum': new Date().toISOString().split('T')[0],
|
||||
'datum': getViennaTodayISO(),
|
||||
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
|
||||
'beurteilungsnotiz': '',
|
||||
'upload_allowed': false,
|
||||
@@ -337,16 +352,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
}
|
||||
},
|
||||
formatDate(dateParam) {
|
||||
// unsafe for datepickers, dont use there
|
||||
const date = new Date(dateParam)
|
||||
// handle missing leading 0
|
||||
const padZero = (num) => String(num).padStart(2, '0');
|
||||
|
||||
const month = padZero(date.getMonth() + 1); // Months are zero-based
|
||||
const day = padZero(date.getDate());
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}.${month}.${year}`
|
||||
return formatISODate(dateParam)
|
||||
},
|
||||
openCreateNewAbgabeModal() {
|
||||
if(this.projektarbeit?.betreuerart_kurzbz == 'Zweitbegutachter') {
|
||||
@@ -364,7 +370,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
'fixtermin': false,
|
||||
'invertedFixtermin': true,
|
||||
'kurzbz': '',
|
||||
'datum': new Date().toISOString().split('T')[0],
|
||||
'datum': getViennaTodayISO(),
|
||||
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
|
||||
'beurteilungsnotiz': '',
|
||||
'upload_allowed': typ.upload_allowed_default,
|
||||
@@ -398,7 +404,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
'fixtermin': false,
|
||||
'invertedFixtermin': true,
|
||||
'kurzbz': '',
|
||||
'datum': new Date().toISOString().split('T')[0],
|
||||
'datum': getViennaTodayISO(),
|
||||
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
|
||||
'beurteilungsnotiz': '',
|
||||
'upload_allowed': false,
|
||||
@@ -446,7 +452,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
}
|
||||
},
|
||||
getMessagePtStyle() {
|
||||
// adjust outer spacing and internal padding to appear similar to doenload button in size
|
||||
// adjust outer spacing and internal padding to appear similar to download button in size
|
||||
return {
|
||||
root: {
|
||||
style: {
|
||||
@@ -561,6 +567,12 @@ export const AbgabeMitarbeiterDetail = {
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
},
|
||||
getTooltipBeurteilungsnotiz() {
|
||||
return {
|
||||
value: this.$p.t('abgabetool/c4beurteilungsnotizBeiNegNote'),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
},
|
||||
getProjektarbeitTitel() {
|
||||
if(this.projektarbeit?.titel) return this.$capitalize(this.$p.t('abgabetool/c4titel')) + ': ' + this.projektarbeit.titel
|
||||
|
||||
@@ -591,7 +603,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
'fixtermin': false,
|
||||
'invertedFixtermin': true,
|
||||
'kurzbz': '',
|
||||
'datum': new Date().toISOString().split('T')[0],
|
||||
'datum': getViennaTodayISO(),
|
||||
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
|
||||
'beurteilungsnotiz': '',
|
||||
'upload_allowed': typ.upload_allowed_default,
|
||||
@@ -627,6 +639,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
dialogClass="bordered-modal modal-lg"
|
||||
:backdrop="true"
|
||||
@hideBsModal="showAutomagicModalPhrase=false;"
|
||||
bodyClass="px-4 py-4"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<div>
|
||||
@@ -656,6 +669,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
:enable-time-picker="false"
|
||||
locale="de"
|
||||
format="dd.MM.yyyy"
|
||||
model-type="yyyy-MM-dd"
|
||||
:text-input="true"
|
||||
auto-apply>
|
||||
</VueDatePicker>
|
||||
@@ -806,6 +820,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
:enable-time-picker="false"
|
||||
locale="de"
|
||||
format="dd.MM.yyyy"
|
||||
model-type="yyyy-MM-dd"
|
||||
:text-input="true"
|
||||
auto-apply>
|
||||
</VueDatePicker>
|
||||
@@ -816,7 +831,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
<div class="col-12 col-md-9">
|
||||
<Dropdown
|
||||
:style="{'width': '100%'}"
|
||||
:disabled="!termin.allowedToSave"
|
||||
:disabled="!termin.allowedToSave || termin.abgabedatum !== null || termin.noteBackend"
|
||||
:placeholder="getPlaceholderTermin(termin)"
|
||||
v-model="termin.bezeichnung"
|
||||
@change="handleChangeAbgabetyp(termin)"
|
||||
@@ -851,8 +866,10 @@ export const AbgabeMitarbeiterDetail = {
|
||||
</div>
|
||||
<div class="row mt-2" v-if="termin.bezeichnung?.benotbar">
|
||||
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4notizQualGatev2') )}}</div>
|
||||
<div class="col-12 col-md-9">
|
||||
<Textarea style="margin-bottom: 4px;" v-model="termin.beurteilungsnotiz" rows="1" class="w-100" :disabled="!termin.allowedToSave"></Textarea>
|
||||
<div class="col-12 col-md-9" v-tooltip.right="terminIsInvalid(termin) && getTooltipBeurteilungsnotiz">
|
||||
<Textarea style="margin-bottom: 4px;" v-model="termin.beurteilungsnotiz"
|
||||
:class="{ 'p-invalid': terminIsInvalid(termin) }"
|
||||
rows="1" class="w-100" :disabled="!termin.allowedToSave"></Textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -874,6 +891,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
:disabled="true"
|
||||
locale="de"
|
||||
format="dd.MM.yyyy"
|
||||
model-type="yyyy-MM-dd"
|
||||
>
|
||||
</VueDatePicker>
|
||||
</div>
|
||||
@@ -889,9 +907,6 @@ export const AbgabeMitarbeiterDetail = {
|
||||
<Message v-else-if="termin?.signatur == false" severity="error" :closable="false" :pt="getMessagePtStyle"> {{ $capitalize($p.t('abgabetool/c4keineSignatur')) }} </Message>
|
||||
<Message v-else-if="termin?.signatur == 'error'" severity="warn" :closable="false" :pt="getMessagePtStyle"> {{ $capitalize($p.t('abgabetool/c4signaturServerError')) }} </Message>
|
||||
</div>
|
||||
<!-- <div v-else class="col-auto">-->
|
||||
<!-- <Message severity="info" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4noFileFound') }} </Message>-->
|
||||
<!-- </div>-->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -904,10 +919,10 @@ export const AbgabeMitarbeiterDetail = {
|
||||
<div class="col-12 col-md-3 fw-bold align-content-center">
|
||||
{{ $capitalize( $p.t('abgabetool/c4actions') )}}
|
||||
</div>
|
||||
<div class="col-12 col-md-9">
|
||||
<div class="col-12 col-md-5">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<button v-if="termin.allowedToSave" style="max-height: 40px;" class="btn btn-primary border-0" @click="saveTermin(termin)">
|
||||
<button v-if="termin.allowedToSave && !terminIsInvalid(termin)" style="max-height: 40px;" class="btn btn-primary border-0" @click="saveTermin(termin)">
|
||||
{{ $capitalize( $p.t('abgabetool/c4save') )}}
|
||||
<i class="fa-solid fa-floppy-disk"></i>
|
||||
</button>
|
||||
@@ -932,6 +947,9 @@ export const AbgabeMitarbeiterDetail = {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 align-content-center text-end text-muted small">
|
||||
{{getSavedTerminInfoString(termin)}}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTab>
|
||||
</template>
|
||||
@@ -944,7 +962,8 @@ export const AbgabeMitarbeiterDetail = {
|
||||
<bs-modal
|
||||
ref="modalContainerZusatzdaten"
|
||||
class="bootstrap-prompt"
|
||||
dialogClass="bordered-modal modal-lg">
|
||||
dialogClass="bordered-modal modal-lg"
|
||||
bodyClass="px-4 py-4">
|
||||
<template v-slot:title>
|
||||
<div>
|
||||
{{$capitalize( $p.t('abgabetool/c4enduploadZusatzdaten') )}}
|
||||
|
||||
@@ -3,6 +3,9 @@ import BsModal from '../../Bootstrap/Modal.js';
|
||||
import VueDatePicker from '../../vueDatepicker.js.php';
|
||||
import ApiAbgabe from '../../../api/factory/abgabe.js'
|
||||
import FhcOverlay from "../../Overlay/FhcOverlay.js";
|
||||
import { formatISODate, getViennaTodayISO } from "./dateUtils.js";
|
||||
import { validateThesisTitle } from './titleValidation.js'
|
||||
|
||||
|
||||
export const AbgabeStudentDetail = {
|
||||
name: "AbgabeStudentDetail",
|
||||
@@ -20,7 +23,16 @@ export const AbgabeStudentDetail = {
|
||||
VueDatePicker,
|
||||
FhcOverlay
|
||||
},
|
||||
inject: ['notenOptions', 'isMobile', 'isViewMode', 'moodle_link'],
|
||||
inject: [
|
||||
'notenOptions',
|
||||
'isMobile',
|
||||
'isViewMode',
|
||||
'moodle_link',
|
||||
'confetti_on_endupload',
|
||||
'title_edit_allowed',
|
||||
'siginfolink_german',
|
||||
'siginfolink_english'
|
||||
],
|
||||
props: {
|
||||
projektarbeit: {
|
||||
type: Object,
|
||||
@@ -31,12 +43,14 @@ export const AbgabeStudentDetail = {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['titel-updated'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
eidAkzeptiert: false,
|
||||
enduploadTermin: null,
|
||||
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
|
||||
editingTitel: '',
|
||||
form: Vue.reactive({
|
||||
sprache: '',
|
||||
abstract: '',
|
||||
@@ -49,9 +63,120 @@ export const AbgabeStudentDetail = {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
confettiCannons() {
|
||||
const container = document.getElementById('confetti-container');
|
||||
if (!container) return;
|
||||
|
||||
const colors = ['#FFC107', '#FF5722', '#E91E63', '#00BCD4', '#4CAF50', '#9C27B0'];
|
||||
const shapes = ['50%', '0%'];
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// Corner Cannons - Slowed Down)
|
||||
const cannonCount = 150;
|
||||
|
||||
for (let i = 0; i < cannonCount; i++) {
|
||||
const leftConfetti = document.createElement('div');
|
||||
leftConfetti.classList.add('confetti-piece');
|
||||
leftConfetti.style.left = '0px';
|
||||
leftConfetti.style.top = '100%';
|
||||
|
||||
const rightConfetti = document.createElement('div');
|
||||
rightConfetti.classList.add('confetti-piece');
|
||||
rightConfetti.style.left = '100vw';
|
||||
rightConfetti.style.top = '100%';
|
||||
|
||||
const colorL = colors[Math.floor(Math.random() * colors.length)];
|
||||
const colorR = colors[Math.floor(Math.random() * colors.length)];
|
||||
const shapeL = shapes[Math.floor(Math.random() * shapes.length)];
|
||||
const shapeR = shapes[Math.floor(Math.random() * shapes.length)];
|
||||
|
||||
// Left Styles
|
||||
leftConfetti.style.background = colorL;
|
||||
leftConfetti.style.borderRadius = shapeL;
|
||||
leftConfetti.style.width = `${Math.random() * 10 + 6}px`;
|
||||
leftConfetti.style.height = `${Math.random() * 14 + 6}px`;
|
||||
leftConfetti.style.setProperty('--blast-x', `${Math.random() * 50 + 10}vw`);
|
||||
leftConfetti.style.setProperty('--blast-y', `-${Math.random() * 65 + 30}vh`);
|
||||
|
||||
// Right Styles
|
||||
rightConfetti.style.background = colorR;
|
||||
rightConfetti.style.borderRadius = shapeR;
|
||||
rightConfetti.style.width = `${Math.random() * 10 + 6}px`;
|
||||
rightConfetti.style.height = `${Math.random() * 14 + 6}px`;
|
||||
rightConfetti.style.setProperty('--blast-x', `-${Math.random() * 50 + 10}vw`);
|
||||
rightConfetti.style.setProperty('--blast-y', `-${Math.random() * 65 + 30}vh`);
|
||||
|
||||
// Increased durations to 3s - 5s for a floating gravity effect
|
||||
const durationL = Math.random() * 2 + 3;
|
||||
const durationR = Math.random() * 2 + 3;
|
||||
const delayL = Math.random() * 0.2;
|
||||
const delayR = Math.random() * 0.2;
|
||||
|
||||
leftConfetti.style.animation = `cannonBlast ${durationL}s linear ${delayL}s both`;
|
||||
rightConfetti.style.animation = `cannonBlast ${durationR}s linear ${delayR}s both`;
|
||||
|
||||
fragment.appendChild(leftConfetti);
|
||||
fragment.appendChild(rightConfetti);
|
||||
|
||||
setTimeout(() => leftConfetti.remove(), (delayL + durationL) * 1000);
|
||||
setTimeout(() => rightConfetti.remove(), (delayR + durationR) * 1000);
|
||||
}
|
||||
|
||||
container.appendChild(fragment);
|
||||
},
|
||||
openTitelEdit() {
|
||||
this.editingTitel = this.projektarbeit.titel ?? '';
|
||||
this.$refs.modalTitelEdit.show();
|
||||
},
|
||||
async saveTitel() {
|
||||
|
||||
const validation = validateThesisTitle(this.editingTitel);
|
||||
|
||||
if (!validation.isValid) {
|
||||
if (validation.error === 'empty') {
|
||||
this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4emptyThesisTitle'))
|
||||
} else if (validation.error === 'invalid_characters') {
|
||||
this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4invalidCharactersThesisTitle'))
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const confirmed = await this.$fhcAlert.confirm({
|
||||
message: this.$p.t('abgabetool/c4confirmTitelSpeichern'),
|
||||
acceptLabel: this.$capitalize(this.$p.t('ui/speichern')),
|
||||
acceptClass: 'p-button-primary',
|
||||
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
|
||||
rejectClass: 'p-button-secondary'
|
||||
});
|
||||
|
||||
if (confirmed === false) return;
|
||||
|
||||
this.loading = true;
|
||||
this.$api.call(
|
||||
ApiAbgabe.postStudentProjektarbeitTitel(
|
||||
this.projektarbeit.projektarbeit_id,
|
||||
validation.cleanedTitle
|
||||
)
|
||||
).then(res => {
|
||||
if (res.meta.status === 'success') {
|
||||
this.projektarbeit.titel = res.data;
|
||||
this.$emit('titel-updated', {
|
||||
projektarbeit_id: this.projektarbeit.projektarbeit_id,
|
||||
titel: res.data
|
||||
});
|
||||
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4titelSavedSuccess')));
|
||||
this.$refs.modalTitelEdit.hide();
|
||||
} else {
|
||||
this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4titelSaveError')));
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
getNoteBezeichnung(termin){
|
||||
const noteOpt = this.notenOptions.find(opt => opt.note == termin.note)
|
||||
|
||||
|
||||
if(noteOpt?.bezeichnung) {
|
||||
return noteOpt?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌'
|
||||
} else if(noteOpt?.benotbar === true && !termin.note) {
|
||||
@@ -65,7 +190,7 @@ export const AbgabeStudentDetail = {
|
||||
this.$fhcAlert.alertWarning(this.$capitalize(this.$p.t('global/warningChooseFile')));
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if(endupload) {
|
||||
if(await this.$fhcAlert.confirm({
|
||||
message: this.$p.t('abgabetool/confirmEnduploadSpeichern'),
|
||||
@@ -77,16 +202,16 @@ export const AbgabeStudentDetail = {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
},
|
||||
async triggerEndupload() {
|
||||
|
||||
|
||||
if (!await this.validate(this.enduploadTermin, true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// post endabgabe
|
||||
const formData = new FormData();
|
||||
formData.append('paabgabetyp_kurzbz', this.enduploadTermin.paabgabetyp_kurzbz)
|
||||
@@ -94,14 +219,14 @@ export const AbgabeStudentDetail = {
|
||||
formData.append('paabgabe_id', this.enduploadTermin.paabgabe_id)
|
||||
formData.append('student_uid', this.projektarbeit.student_uid)
|
||||
formData.append('bperson_id', this.projektarbeit.bperson_id)
|
||||
|
||||
|
||||
formData.append('sprache', this.form['sprache'].sprache)
|
||||
formData.append('abstract', this.form['abstract'])
|
||||
formData.append('abstract_en', this.form['abstract_en'])
|
||||
formData.append('schlagwoerter', this.form['schlagwoerter'])
|
||||
formData.append('schlagwoerter_en', this.form['schlagwoerter_en'])
|
||||
formData.append('seitenanzahl', this.form['seitenanzahl'])
|
||||
|
||||
|
||||
for (let i = 0; i < this.enduploadTermin.file.length; i++) {
|
||||
formData.append('file', this.enduploadTermin.file[i]);
|
||||
}
|
||||
@@ -109,39 +234,31 @@ export const AbgabeStudentDetail = {
|
||||
this.$api.call(ApiAbgabe.postStudentProjektarbeitEndupload(formData))
|
||||
.then(res => {
|
||||
this.handleUploadRes(res, this.enduploadTermin)
|
||||
if(this.confetti_on_endupload && res.meta.status == "success") {
|
||||
this.confettiCannons()
|
||||
}
|
||||
}).finally(()=> {
|
||||
this.loading = false
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
|
||||
this.$refs.modalContainerEnduploadZusatzdaten.hide()
|
||||
},
|
||||
downloadAbgabe(termin) {
|
||||
const url = `/api/frontend/v1/Abgabe/getStudentProjektarbeitAbgabeFile?paabgabe_id=${termin.paabgabe_id}&student_uid=${this.projektarbeit.student_uid}&projektarbeit_id=${this.projektarbeit.projektarbeit_id}`;
|
||||
|
||||
window.open(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url)
|
||||
// this.$api.call(ApiAbgabe.getStudentProjektarbeitAbgabeFile(termin.paabgabe_id, this.projektarbeit.student_uid))
|
||||
},
|
||||
formatDate(dateParam) {
|
||||
const date = new Date(dateParam)
|
||||
// handle missing leading 0
|
||||
const padZero = (num) => String(num).padStart(2, '0');
|
||||
|
||||
const month = padZero(date.getMonth() + 1); // Months are zero-based
|
||||
const day = padZero(date.getDate());
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}.${month}.${year}`
|
||||
return formatISODate(dateParam)
|
||||
},
|
||||
async upload(termin) {
|
||||
|
||||
// only do this on endupload
|
||||
if (! await this.validate(termin))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if(termin.bezeichnung?.paabgabetyp_kurzbz === 'end') {
|
||||
// open endupload form modal for further inputs
|
||||
this.enduploadTermin = termin
|
||||
this.$refs.modalContainerEnduploadZusatzdaten.show()
|
||||
} else {
|
||||
@@ -151,7 +268,7 @@ export const AbgabeStudentDetail = {
|
||||
formData.append('paabgabe_id', termin.paabgabe_id)
|
||||
formData.append('student_uid', this.projektarbeit.student_uid)
|
||||
formData.append('bperson_id', this.projektarbeit.bperson_id)
|
||||
|
||||
|
||||
for (let i = 0; i < termin.file.length; i++) {
|
||||
formData.append('file', termin.file[i]);
|
||||
}
|
||||
@@ -161,7 +278,7 @@ export const AbgabeStudentDetail = {
|
||||
.then(res => {
|
||||
this.handleUploadRes(res, termin)
|
||||
}).finally(()=> {
|
||||
this.loading = false
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -169,21 +286,18 @@ export const AbgabeStudentDetail = {
|
||||
if(res.meta.status == "success") {
|
||||
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4fileUploadSuccessv3')))
|
||||
|
||||
// update 'abgabedatum' for successful upload -> shows the pdf icon and date once set
|
||||
termin.abgabedatum = new Date().toISOString().split('T')[0];
|
||||
termin.abgabedatum = getViennaTodayISO();
|
||||
if(res?.data?.signatur !== undefined) {
|
||||
termin.signatur = res.data.signatur
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4fileUploadErrorv3')))
|
||||
}
|
||||
|
||||
|
||||
if(res.meta.signaturInfo) {
|
||||
this.$fhcAlert.alertInfo(res.meta.signaturInfo)
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
getOptionLabel(option) {
|
||||
return option.sprache
|
||||
@@ -195,7 +309,6 @@ export const AbgabeStudentDetail = {
|
||||
},
|
||||
watch: {
|
||||
projektarbeit(newVal) {
|
||||
// default select german if projektarbeit sprache was null
|
||||
this.form.sprache = newVal.sprache ? this.allActiveLanguages.find(lang => lang.sprache == newVal.sprache) : this.allActiveLanguages.find(lang => lang.sprache == 'German')
|
||||
this.form.abstract = newVal.abstract ?? ''
|
||||
this.form.abstract_en = newVal.abstract_en ?? ''
|
||||
@@ -203,15 +316,22 @@ export const AbgabeStudentDetail = {
|
||||
this.form.schlagwoerter_en = newVal.schlagwoerter_en ?? ''
|
||||
this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter ?? ''
|
||||
this.form.seitenanzahl = newVal.seitenanzahl ?? 1
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getSignaturInfoLink() {
|
||||
if(this.$p.user_language.value == 'German' && this.siginfolink_german) return this.siginfolink_german
|
||||
else if (this.$p.user_language.value == 'English' && this.siginfolink_english) return this.siginfolink_english
|
||||
},
|
||||
getSignaturInfoAvailable() {
|
||||
if(this.$p.user_language.value == 'German' && this.siginfolink_german) return true
|
||||
else if (this.$p.user_language.value == 'English' && this.siginfolink_english) return true
|
||||
else return false
|
||||
},
|
||||
getMoodleLink() {
|
||||
return this.moodle_link + this.projektarbeit.studiengang_kz
|
||||
return this.moodle_link + this.projektarbeit.studiengang_kz
|
||||
},
|
||||
getMessagePtStyle() {
|
||||
// adjust outer spacing and internal padding to appear similar to doenload button in size
|
||||
return {
|
||||
root: {
|
||||
style: {
|
||||
@@ -244,102 +364,84 @@ export const AbgabeStudentDetail = {
|
||||
})
|
||||
return qgatefound
|
||||
},
|
||||
isTitelEditAllowed() {
|
||||
return this.title_edit_allowed && !this.isViewMode && !this.projektarbeit?.note;
|
||||
},
|
||||
getTooltipVerspaetet() {
|
||||
return {
|
||||
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')), class: "custom-tooltip" }
|
||||
},
|
||||
getTooltipVerpasst() {
|
||||
return {
|
||||
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')), class: "custom-tooltip" }
|
||||
},
|
||||
getTooltipAbzugeben() {
|
||||
return {
|
||||
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')), class: "custom-tooltip" }
|
||||
},
|
||||
getTooltipStandard() {
|
||||
return {
|
||||
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandardv2')),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandardv2')), class: "custom-tooltip" }
|
||||
},
|
||||
getTooltipAbgegeben() {
|
||||
return {
|
||||
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')), class: "custom-tooltip" }
|
||||
},
|
||||
getTooltipFixtermin() {
|
||||
return {
|
||||
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')), class: "custom-tooltip" }
|
||||
},
|
||||
getTooltipAbgabeDetected() {
|
||||
return {
|
||||
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgabeDetected')),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgabeDetected')), class: "custom-tooltip" }
|
||||
},
|
||||
getTooltipNotAllowedToUpload() {
|
||||
if(this.isViewMode) {
|
||||
return {
|
||||
value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')), class: "custom-tooltip" }
|
||||
} else {
|
||||
return {
|
||||
value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegular')),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegularv2')), class: "custom-tooltip" }
|
||||
}
|
||||
},
|
||||
getTooltipBeurteilungerforderlich() {
|
||||
return {
|
||||
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')), class: "custom-tooltip" }
|
||||
},
|
||||
getTooltipBestanden() {
|
||||
return {
|
||||
value: this.$p.t('abgabetool/c4tooltipBestanden'),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
return { value: this.$p.t('abgabetool/c4tooltipBestanden'), class: "custom-tooltip" }
|
||||
},
|
||||
getTooltipNichtBestanden() {
|
||||
return {
|
||||
value: this.$p.t('abgabetool/c4tooltipNichtBestanden'),
|
||||
class: "custom-tooltip"
|
||||
}
|
||||
return { value: this.$p.t('abgabetool/c4tooltipNichtBestanden'), class: "custom-tooltip" }
|
||||
},
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
template: `
|
||||
<FhcOverlay :active="loading"></FhcOverlay>
|
||||
|
||||
<div v-if="projektarbeit">
|
||||
|
||||
<h5>{{$capitalize( $p.t('abgabetool/c4abgabeStudentenbereich') )}}</h5>
|
||||
<h5>{{$capitalize( $p.t('abgabetool/c4abgabeStudentenbereichv2') )}}</h5>
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<p> {{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}</p>
|
||||
<p> {{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}}</p>
|
||||
<p> {{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}</p>
|
||||
<p>{{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}</p>
|
||||
|
||||
<p class="d-flex align-items-center gap-2 mb-2" style="min-width: 0;">
|
||||
<span
|
||||
:title="projektarbeit.titel"
|
||||
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 480px;"
|
||||
>{{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}}</span>
|
||||
<button
|
||||
v-if="isTitelEditAllowed"
|
||||
class="btn btn-sm btn-outline-secondary border-0 p-1"
|
||||
v-tooltip.right="{ value: $capitalize($p.t('abgabetool/c4titelBearbeiten')), class: 'custom-tooltip' }"
|
||||
@click="openTitelEdit"
|
||||
>
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p>{{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}</p>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<p>{{ $p.t('abgabetool/c4checkoutStgMoodleInfos') }}
|
||||
<a :href="getMoodleLink" target="_blank">Moodle</a>
|
||||
</p>
|
||||
<div class="row">
|
||||
<p>{{ $p.t('abgabetool/c4checkoutStgMoodleInfos') }}
|
||||
<a :href="getMoodleLink" target="_blank">Moodle</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="getSignaturInfoAvailable">
|
||||
<a :href="getSignaturInfoLink" target="_blank">{{$p.t('abgabetool/c4signaturinfo')}} <i class="fa-solid fa-circle-info"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -357,7 +459,6 @@ export const AbgabeStudentDetail = {
|
||||
<i v-else-if="termin.dateStyle == 'beurteilungerforderlich'" v-tooltip.right="getTooltipBeurteilungerforderlich" class="fa-solid fa-list-check"></i>
|
||||
<i v-else-if="termin.dateStyle == 'bestanden'" v-tooltip.right="getTooltipBestanden" class="fa-solid fa-check"></i>
|
||||
<i v-else-if="termin.dateStyle == 'nichtbestanden'" v-tooltip.right="getTooltipNichtBestanden" class="fa-solid fa-circle-exclamation"></i>
|
||||
|
||||
</div>
|
||||
<div class="text-start px-2" style="min-width: 150px; max-width: 300px; margin-left: 40px">
|
||||
<span>{{ termin ? $p.t('abgabetool/c4paatyp' + termin.paabgabetyp_kurzbz) : '' }}</span>
|
||||
@@ -408,8 +509,6 @@ export const AbgabeStudentDetail = {
|
||||
</div>
|
||||
</template>
|
||||
</Inplace>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row mt-2">
|
||||
@@ -430,7 +529,7 @@ export const AbgabeStudentDetail = {
|
||||
</VueDatePicker>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mt-2">
|
||||
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabetyp') )}}</div>
|
||||
<div class="col-12 col-md-9">
|
||||
@@ -466,7 +565,7 @@ export const AbgabeStudentDetail = {
|
||||
<div class="col-12 col-md-9">
|
||||
<template v-if="termin?.abgabedatum">
|
||||
<div class="row">
|
||||
<div style="width:100px; align-content: center;">
|
||||
<div style="width:100px; align-content: end;">
|
||||
<h6>{{ termin.abgabedatum?.split("-").reverse().join(".") }}</h6>
|
||||
</div>
|
||||
|
||||
@@ -481,9 +580,6 @@ export const AbgabeStudentDetail = {
|
||||
<Message v-else-if="termin?.signatur == false" severity="error" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4keineSignatur') }} </Message>
|
||||
<Message v-else-if="termin?.signatur == 'error'" severity="warn" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4signaturServerError') }} </Message>
|
||||
</div>
|
||||
<!-- <div v-else class="col-auto">-->
|
||||
<!-- <Message severity="info" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4noFileFound') }} </Message>-->
|
||||
<!-- </div>-->
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -542,25 +638,66 @@ export const AbgabeStudentDetail = {
|
||||
<h5>{{ $capitalize( $p.t('abgabetool/c4keineAbgabetermineGefunden') )}}</h5>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="confetti_on_endupload" id="confetti-container"></div>
|
||||
|
||||
<bs-modal
|
||||
ref="modalTitelEdit"
|
||||
class="bootstrap-prompt"
|
||||
dialogClass="bordered-modal"
|
||||
bodyClass="px-4 py-4"
|
||||
>
|
||||
<template v-slot:title>
|
||||
{{$capitalize( $p.t('abgabetool/c4titelBearbeiten') )}}
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
<div class="mb-2">
|
||||
<label class="form-label fw-bold">
|
||||
{{$capitalize( $p.t('abgabetool/c4titel') )}}
|
||||
</label>
|
||||
<Textarea
|
||||
v-model="editingTitel"
|
||||
rows="2"
|
||||
maxlength="1024"
|
||||
class="form-control w-100"
|
||||
@keydown.enter.prevent="saveTitel"
|
||||
/>
|
||||
<div class="form-text text-end">{{ editingTitel.length }} / 1024</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="$refs.modalTitelEdit.hide()"
|
||||
>
|
||||
{{$capitalize( $p.t('abgabetool/c4Cancel') )}}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="!editingTitel.trim()"
|
||||
@click="saveTitel"
|
||||
>
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
{{$capitalize( $p.t('ui/speichern') )}}
|
||||
</button>
|
||||
</template>
|
||||
</bs-modal>
|
||||
|
||||
<bs-modal
|
||||
ref="modalContainerEnduploadZusatzdaten"
|
||||
class="bootstrap-prompt"
|
||||
dialogClass="bordered-modal modal-lg">
|
||||
dialogClass="bordered-modal modal-lg"
|
||||
bodyClass="px-4 py-4">
|
||||
<template v-slot:title>
|
||||
<div>
|
||||
{{$capitalize( $p.t('abgabetool/c4enduploadZusatzdaten') )}}
|
||||
</div>
|
||||
<div class="row mb-3 align-items-start">
|
||||
|
||||
<p class="ml-4 mr-4">Student UID: {{ projektarbeit?.student_uid}}</p>
|
||||
|
||||
</div>
|
||||
<div class="row mb-3 align-items-start">
|
||||
|
||||
<p class="ml-4 mr-4">{{$capitalize( $p.t('abgabetool/c4titel') )}}: {{ projektarbeit?.titel }}</p>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
@@ -576,15 +713,6 @@ export const AbgabeStudentDetail = {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- lektor fills these out-->
|
||||
<!-- <div class="row mb-3 align-items-start">-->
|
||||
<!-- <div class="row">Kontrollierte Schlagwörter</div>-->
|
||||
<!-- <div class="row">-->
|
||||
<!-- <Textarea v-model="form.kontrollschlagwoerter"></Textarea>-->
|
||||
<!-- </div>-->
|
||||
<!-- -->
|
||||
<!-- -->
|
||||
<!-- </div>-->
|
||||
<div class="row mb-3 align-items-start">
|
||||
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterGer') )}}</div>
|
||||
<div class="row">
|
||||
@@ -631,7 +759,6 @@ export const AbgabeStudentDetail = {
|
||||
<div class="col-9"></div>
|
||||
<div class="col-2"><p>{{$capitalize( $p.t('abgabetool/c4gelesenUndAkzeptiert') )}}</p></div>
|
||||
<div class="col-1">
|
||||
|
||||
<Checkbox
|
||||
v-model="eidAkzeptiert"
|
||||
:binary="true"
|
||||
@@ -647,8 +774,8 @@ export const AbgabeStudentDetail = {
|
||||
<div v-show="!allowedToSaveZusatzdaten">{{ $p.t('abgabetool/c4zusatzdatenausfuellen') }}</div>
|
||||
<button class="btn btn-primary" :disabled="!getAllowedToSendEndupload" @click="triggerEndupload">{{$capitalize( $p.t('ui/hochladen') )}}</button>
|
||||
</template>
|
||||
|
||||
</bs-modal>
|
||||
|
||||
`,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import AbgabeterminStatusLegende from "./StatusLegende.js";
|
||||
import {formatDateTime} from "./dateUtils.js";
|
||||
|
||||
export const AbgabeStudentTimeline = {
|
||||
name: "AbgabeStudentTimeline",
|
||||
components: {
|
||||
AbgabeterminStatusLegende,
|
||||
Timeline: primevue.timeline,
|
||||
},
|
||||
props: {
|
||||
projekte: { type: Array, default: () => [] },
|
||||
notenOptions: { type: Array, default: () => [] },
|
||||
formatDateFn: { type: Function, required: true }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
legendExpanded: false,
|
||||
expandedProjects: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
student() {
|
||||
return this.projekte?.[0] ?? null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
projekte: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
// open all projects by default whenever the student changes
|
||||
const state = {}
|
||||
val?.forEach(p => { state[p.projektarbeit_id] = true })
|
||||
this.expandedProjects = state
|
||||
this.legendExpanded = false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getNoteBezeichnung(projektarbeit) {
|
||||
if(projektarbeit.note && this.notenOptions) {
|
||||
const noteOpt = this.notenOptions.find(opt => opt.note == projektarbeit.note)
|
||||
return noteOpt?.bezeichnung
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
getSavedTerminInfoString(termin) {
|
||||
const isUpdate = termin.updateamum != null;
|
||||
|
||||
const fullname = isUpdate
|
||||
? termin.updatevon_fullname
|
||||
: termin.insertvon_fullname;
|
||||
|
||||
const datetime = isUpdate
|
||||
? termin.updateamum
|
||||
: termin.insertamum;
|
||||
|
||||
return this.$p.t('ui/savedAtByV3', [formatDateTime(datetime), fullname])
|
||||
},
|
||||
getItemBezeichnung(item) {
|
||||
if (!item?.bezeichnung) return ''
|
||||
return item.bezeichnung?.bezeichnung ?? item.bezeichnung
|
||||
},
|
||||
getItemNote(item) {
|
||||
if (!item?.note) return ''
|
||||
if (item.note?.bezeichnung) return item.note.bezeichnung
|
||||
return this.notenOptions?.find(n => n.note == item.note)?.bezeichnung ?? String(item.note)
|
||||
},
|
||||
getIconClass(dateStyle) {
|
||||
return ({
|
||||
verspaetet: 'fa-solid fa-triangle-exclamation',
|
||||
verpasst: 'fa-solid fa-calendar-xmark',
|
||||
abzugeben: 'fa-solid fa-hourglass-half',
|
||||
standard: 'fa-solid fa-clock',
|
||||
abgegeben: 'fa-solid fa-paperclip',
|
||||
beurteilungerforderlich: 'fa-solid fa-list-check',
|
||||
bestanden: 'fa-solid fa-check',
|
||||
nichtbestanden: 'fa-solid fa-circle-exclamation',
|
||||
})[dateStyle] ?? ''
|
||||
},
|
||||
getBetreuerLabel(projekt) {
|
||||
return projekt.erstbetreuer_full_name
|
||||
|| (projekt.betreuer_vorname ? `${projekt.betreuer_vorname} ${projekt.betreuer_nachname}`.trim() : null)
|
||||
},
|
||||
toggleProject(id) {
|
||||
this.expandedProjects[id] = !this.expandedProjects[id]
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div v-if="student">
|
||||
|
||||
<div class="d-flex align-items-baseline gap-2 mb-3 pb-2 border-bottom">
|
||||
<span class="fw-bold fs-6">{{ student.student_vorname }} {{ student.student_nachname }}</span>
|
||||
<span class="text-muted small">{{ student.student_uid }}</span>
|
||||
<span v-if="student.matrikelnr" class="text-muted small ms-auto">{{ student.matrikelnr }}</span>
|
||||
</div>
|
||||
|
||||
<div v-for="projekt in projekte" :key="projekt.projektarbeit_id" class="mb-2">
|
||||
|
||||
<button
|
||||
class="btn btn-sm w-100 text-start d-flex align-items-center gap-2 rounded bg-light border-0 py-2 px-3"
|
||||
@click="toggleProject(projekt.projektarbeit_id)"
|
||||
>
|
||||
<i
|
||||
:class="expandedProjects[projekt.projektarbeit_id] ? 'fa-solid fa-chevron-down' : 'fa-solid fa-chevron-right'"
|
||||
style="width: 12px; flex-shrink: 0;"
|
||||
></i>
|
||||
<span class="fw-semibold text-truncate flex-grow-1" :title="projekt.titel">
|
||||
{{ projekt.titel || projekt.projekttyp_kurzbz || projekt.projektarbeit_id }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="projekt.note"
|
||||
class="small fw-semibold mx-2 flex-shrink-0"
|
||||
>
|
||||
{{ getNoteBezeichnung(projekt) }}
|
||||
</span>
|
||||
|
||||
<span class="text-muted small me-1 flex-shrink-0">{{ projekt.studiensemester_kurzbz }}</span>
|
||||
<span class="badge bg-secondary flex-shrink-0">{{ projekt.abgabetermine?.length ?? 0 }}</span>
|
||||
</button>
|
||||
|
||||
<div v-show="expandedProjects[projekt.projektarbeit_id]" class="px-2 pt-1">
|
||||
|
||||
<div v-if="getBetreuerLabel(projekt)" class="text-muted small px-1 mb-2">
|
||||
{{ projekt.betreuerart || $capitalize($p.t('abgabetool/c4erstbetreuerv2')) }}:
|
||||
{{ getBetreuerLabel(projekt) }}
|
||||
</div>
|
||||
|
||||
<Timeline :value="projekt.abgabetermine" align="right">
|
||||
|
||||
<template #marker="slotProps">
|
||||
<!-- padding:0 overrides the 34px accordion left-padding from the -header CSS class -->
|
||||
<div
|
||||
:class="slotProps.item.dateStyle + '-header'"
|
||||
style="height: 26px; width: 26px; padding: 0; border-radius: 4px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;"
|
||||
>
|
||||
<i :class="getIconClass(slotProps.item.dateStyle)" style="font-size: 0.75rem;"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #opposite="slotProps">
|
||||
<div class="text-end small text-nowrap text-muted">
|
||||
{{ formatDateFn(slotProps.item.datum) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #content="slotProps">
|
||||
<div class="small pb-1">
|
||||
|
||||
<div class="d-flex align-items-center gap-1 fw-semibold">
|
||||
|
||||
<div
|
||||
:class="slotProps.item.dateStyle + '-header'"
|
||||
style="height:20px;width:20px;padding:0;border-radius:3px;display:flex;align-items:center;justify-content:center;flex-shrink:0;"
|
||||
>
|
||||
<i :class="getIconClass(slotProps.item.dateStyle)" style="font-size:.65rem;"></i>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{{ getItemBezeichnung(slotProps.item) }}
|
||||
</span>
|
||||
|
||||
<i
|
||||
v-if="slotProps.item.fixtermin"
|
||||
class="fa-solid fa-lock text-muted"
|
||||
title="Fixtermin"
|
||||
></i>
|
||||
|
||||
<i
|
||||
v-if="slotProps.item.abgabedatum"
|
||||
class="fa-solid fa-file text-muted"
|
||||
:title="$capitalize($p.t('abgabetool/c4abgabedatum'))"
|
||||
></i>
|
||||
|
||||
<div class="flex-grow-1 text-end">
|
||||
<span
|
||||
v-if="slotProps.item.noteBackend?.bezeichnung"
|
||||
class="fw-bold"
|
||||
>
|
||||
{{ slotProps.item.noteBackend?.positiv
|
||||
? $capitalize($p.t('abgabetool/c4positivBenotet'))
|
||||
: $capitalize($p.t('abgabetool/c4negativBenotet'))
|
||||
}}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-else-if="slotProps.item.bezeichnung?.benotbar"
|
||||
class="fw-bold text-muted"
|
||||
>
|
||||
{{ $capitalize($p.t('abgabetool/c4notYetGraded')) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="slotProps.item.kurzbz"
|
||||
class="text-muted fst-italic ms-4"
|
||||
>
|
||||
{{ slotProps.item.kurzbz }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="slotProps.item.abgabedatum"
|
||||
class="small text-muted ms-4"
|
||||
>
|
||||
{{ $capitalize($p.t('abgabetool/c4abgabedatum')) }}:
|
||||
{{ formatDateFn(slotProps.item.abgabedatum) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="slotProps.item.beurteilungsnotiz"
|
||||
class="small ms-4 mt-1 text-muted"
|
||||
>
|
||||
{{ slotProps.item.beurteilungsnotiz }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="slotProps.item.insertamum"
|
||||
class="small text-muted ms-4 mt-1"
|
||||
style="font-size: .72rem;"
|
||||
>
|
||||
<i class="fa-solid fa-clock-rotate-left me-1"></i>
|
||||
{{ getSavedTerminInfoString(slotProps.item) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</Timeline>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 border-top pt-2">
|
||||
<button
|
||||
class="btn btn-link btn-sm p-0 text-muted text-decoration-none d-flex align-items-center gap-1"
|
||||
@click="legendExpanded = !legendExpanded"
|
||||
>
|
||||
<i :class="legendExpanded ? 'fa-solid fa-chevron-down' : 'fa-solid fa-chevron-right'" style="font-size: 0.7rem;"></i>
|
||||
Legende
|
||||
</button>
|
||||
<div v-show="legendExpanded" class="mt-2">
|
||||
<AbgabeterminStatusLegende />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
export default AbgabeStudentTimeline;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,9 @@ import VueDatePicker from '../../vueDatepicker.js.php';
|
||||
import ApiAbgabe from '../../../api/factory/abgabe.js'
|
||||
import FhcOverlay from "../../Overlay/FhcOverlay.js";
|
||||
import { getDateStyleClass } from "./getDateStyleClass.js";
|
||||
import { dateFilter } from '../../../tabulator/filters/Dates.js';
|
||||
import { dateFilter } from '../../../tabulator/filters/DatesManual.js';
|
||||
import {splitMailsHelper} from "../../../helpers/EmailHelpers.js";
|
||||
import { formatISODate, getViennaTodayISO, toViennaDate } from "./dateUtils.js";
|
||||
|
||||
export const AbgabetoolMitarbeiter = {
|
||||
name: "AbgabetoolMitarbeiter",
|
||||
@@ -31,19 +32,14 @@ export const AbgabetoolMitarbeiter = {
|
||||
old_abgabe_beurteilung_link: Vue.computed(() => this.old_abgabe_beurteilung_link)
|
||||
}
|
||||
},
|
||||
props: {
|
||||
viewData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({name: '', uid: ''}),
|
||||
validator(value) {
|
||||
return value && value.uid // && value.name -> extensive viewData use only for cis4 onwards
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tableData: null,
|
||||
filteredRows: null,
|
||||
count: 0,
|
||||
filteredcount: 0,
|
||||
selectedcount: 0,
|
||||
qgate1FilterSelected: [],
|
||||
qgate2FilterSelected: [],
|
||||
abgabetypenBetreuer: null,
|
||||
detailIsFullscreen: false,
|
||||
phrasenPromise: null,
|
||||
@@ -58,7 +54,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
allowedNotenOptions: null,
|
||||
notenOptionsNonFinal: null,
|
||||
serienTermin: Vue.reactive({
|
||||
datum: new Date(),
|
||||
datum: getViennaTodayISO(),
|
||||
bezeichnung: {
|
||||
paabgabetyp_kurzbz: 'zwischen',
|
||||
bezeichnung: 'Zwischenabgabe'
|
||||
@@ -80,7 +76,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
abgabeTableOptions: {
|
||||
minHeight: 250,
|
||||
index: 'projektarbeit_id',
|
||||
layout: 'fitDataStretch',
|
||||
layout: 'fitData',
|
||||
placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')),
|
||||
selectable: true,
|
||||
selectableCheck: this.selectionCheck,
|
||||
@@ -138,38 +134,82 @@ export const AbgabetoolMitarbeiter = {
|
||||
handleClick: this.selectAllHandler
|
||||
},
|
||||
width: 50,
|
||||
cssClass: 'sticky-col'
|
||||
cssClass: 'sticky-col',
|
||||
visible: true
|
||||
},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.detailFormatter, headerFilter: false, headerSort: false, widthGrow: 1, tooltip: false, cssClass: 'sticky-col'},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerartv2'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), field: 'prevTermin',
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.formAction, headerFilter: false, headerSort: false, width: 140, minWidth: 50, visible: true, tooltip: false, cssClass: 'sticky-col'},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, minWidth: 140, visible: false,tooltip: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100,visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100,visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4studstatus'))), field: 'studienstatus', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 150, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, minWidth: 100,visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, visible: false, sorter: this.notenSorter, minWidth: 200, formatter: this.centeredTextFormatter},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, visible: true, minWidth: 100},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, width: 500, visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'betreuer_full_name', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPre'))), field: 'betreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerVorname'))), field: 'betreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerNachname'))), field: 'betreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPost'))), field: 'betreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))), field: 'zweitbetreuer_full_name', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPre'))), field: 'zweitbetreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPost'))), field: 'zweitbetreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerartv2'))), field: 'betreuerart_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))),
|
||||
headerFilter: dateFilter,
|
||||
headerFilterFunc: this.headerFilterTerminCol,
|
||||
sorter: this.sortFuncTerminCol,
|
||||
formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false},
|
||||
tooltip: this.toolTipFuncPrevTermin,
|
||||
field: 'prevTermin', formatter: this.abgabeterminFormatter, width: 250, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin',
|
||||
headerFilter: dateFilter,
|
||||
headerFilterFunc: this.headerFilterTerminCol,
|
||||
sorter: this.sortFuncTerminCol,
|
||||
formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false},
|
||||
tooltip: this.toolTipFuncNextTermin,
|
||||
formatter: this.abgabeterminFormatter, width: 250, visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))),
|
||||
headerFilter: 'list',
|
||||
headerFilterParams: { valuesLookup: this.getQGateStatusList },
|
||||
field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false},
|
||||
headerFilter: this.qgateHeaderFilterEditor,
|
||||
headerFilterFunc: this.qgateHeaderFilterFunc,
|
||||
headerFilterParams: {},
|
||||
field: 'qgate1Status',
|
||||
formatter: this.centeredTextFormatter,
|
||||
titleFormatter: this.shortLongTitleFormatter,
|
||||
titleFormatterParams: {
|
||||
shortForm: 'QG1'
|
||||
},
|
||||
width: 50,
|
||||
tooltip: (e, cell) => {
|
||||
const data = cell.getData();
|
||||
return data.qgate1Status
|
||||
}
|
||||
},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))),
|
||||
headerFilter: 'list',
|
||||
headerFilterParams: { valuesLookup: this.getQGateStatusList },
|
||||
field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}
|
||||
headerFilter: this.qgateHeaderFilterEditor,
|
||||
headerFilterFunc: this.qgateHeaderFilterFunc,
|
||||
headerFilterParams: {},
|
||||
field: 'qgate2Status',
|
||||
formatter: this.centeredTextFormatter,
|
||||
titleFormatter: this.shortLongTitleFormatter,
|
||||
titleFormatterParams: {
|
||||
shortForm: 'QG2'
|
||||
},
|
||||
width: 50,
|
||||
tooltip: (e, cell) => {
|
||||
const data = cell.getData();
|
||||
return data.qgate2Status
|
||||
}
|
||||
}
|
||||
],
|
||||
persistence: false,
|
||||
persistenceID: 'abgabeTableBetreuer2026-02-26'
|
||||
persistenceID: 'abgabeTableBetreuer2026-05-26'
|
||||
},
|
||||
abgabeTableEventHandlers: [{
|
||||
event: "tableBuilt",
|
||||
@@ -200,11 +240,388 @@ export const AbgabetoolMitarbeiter = {
|
||||
})
|
||||
|
||||
this.selectedData = data
|
||||
this.selectedcount = data.length;
|
||||
}
|
||||
},
|
||||
{
|
||||
event: 'dataFiltered',
|
||||
handler: (filters, rows) => {
|
||||
this.filteredRows = rows;
|
||||
this.filteredcount = rows.length;
|
||||
|
||||
if (!this.selectedData.length) return;
|
||||
|
||||
const visibleData = new Set(rows.map(r => r.getData()));
|
||||
const filteredOut = this.selectedData.filter(sd => !visibleData.has(sd));
|
||||
|
||||
if (!filteredOut.length) return;
|
||||
|
||||
const filteredOutSet = new Set(filteredOut);
|
||||
this.$refs.abgabeTable.tabulator.getSelectedRows()
|
||||
.filter(r => filteredOutSet.has(r.getData()))
|
||||
.forEach(r => r.deselect());
|
||||
}
|
||||
}
|
||||
]};
|
||||
},
|
||||
methods: {
|
||||
notenSorter(a, b, aRow, bRow, column, dir, sorterParams) {
|
||||
const aData = aRow.getData()
|
||||
const bData = bRow.getData()
|
||||
return aData.note - bData.note
|
||||
},
|
||||
async openBenotung(type, link) {
|
||||
if(type === 'new') {
|
||||
window.open(link, '_blank')
|
||||
} else if(type === 'old') {
|
||||
if(await this.$fhcAlert.confirm({
|
||||
message: this.$p.t('abgabetool/c4aeltereParbeitBenotenv2'),
|
||||
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
|
||||
acceptClass: 'btn btn-danger',
|
||||
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
|
||||
rejectClass: 'btn btn-outline-secondary'
|
||||
}) === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
window.open(link, '_blank')
|
||||
} else {
|
||||
// show info text that no endupload with abgabe has been found
|
||||
if(await this.$fhcAlert.confirm({
|
||||
message: this.$p.t('abgabetool/c4keinEnduploadErfolgt'),
|
||||
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
|
||||
acceptClass: 'btn btn-danger',
|
||||
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
|
||||
rejectClass: 'btn btn-outline-secondary'
|
||||
}) === false) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
formAction(cell) {
|
||||
|
||||
const actionButtons = document.createElement('div');
|
||||
actionButtons.className = "d-flex gap-3";
|
||||
actionButtons.style.display = "flex";
|
||||
actionButtons.style.alignItems = "stretch";
|
||||
actionButtons.style.justifyContent = "start";
|
||||
actionButtons.style.height = "100%";
|
||||
|
||||
const val = cell.getValue();
|
||||
const data = cell.getRow().getData()
|
||||
|
||||
const createButton = (iconClass, titleKey, clickHandler) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-outline-secondary';
|
||||
btn.style.display = "flex";
|
||||
btn.style.alignItems = "center"; // center icon vertically
|
||||
btn.style.justifyContent = "center"; // center icon horizontally
|
||||
btn.style.height = "100%"; // fill parent container height
|
||||
btn.style.aspectRatio = "1 / 1"; // keep square shape (optional)
|
||||
btn.style.padding = "0"; // remove extra padding for compactness
|
||||
if(iconClass == 'fa fa-timeline') btn.style.transform = "rotate(90deg)";
|
||||
btn.innerHTML = `<i class="${iconClass}" style="color:#00649C; font-size:1.1rem;"></i>`;
|
||||
btn.title = this.$capitalize(this.$p.t(titleKey));
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
clickHandler();
|
||||
});
|
||||
return btn;
|
||||
};
|
||||
|
||||
actionButtons.append(
|
||||
createButton('fa fa-folder-open', 'abgabetool/c4details', () => this.setDetailComponent(val)),
|
||||
);
|
||||
|
||||
if(data.isCurrent && data.abgabetermine?.find(termin => termin.paabgabetyp_kurzbz == 'end' && termin.abgabedatum !== null) && data.beurteilungLinkNew) {
|
||||
actionButtons.append(createButton('fa fa-user-check', 'abgabetool/c4benoten', () => this.openBenotung('new', data.beurteilungLinkNew)))
|
||||
} else if(data.abgabetermine?.find(termin => termin.paabgabetyp_kurzbz == 'end' && termin.abgabedatum !== null) && data.beurteilungLinkOld) {
|
||||
actionButtons.append(createButton('fa fa-user-check', 'abgabetool/c4benoten', () => this.openBenotung('old', data.beurteilungLinkOld)))
|
||||
}
|
||||
|
||||
if(this.checkForZweitbetreuerTokenMailAvailability(data)) {
|
||||
actionButtons.append(createButton('fa fa-envelope-open-text', 'abgabetool/c4zweitBegutachterTokenMailSenden', () => this.sendZweitbetreuerToken(data)))
|
||||
|
||||
}
|
||||
|
||||
return actionButtons;
|
||||
},
|
||||
checkForZweitbetreuerTokenMailAvailability(data) {
|
||||
const hasEndabgabeWithUpload = !!data.abgabetermine.find(termin => termin.abgabedatum !== null && termin.paabgabetyp_kurzbz == 'end')
|
||||
const hasZweitbetreuerWithoutBenutzerUid = data.zweitbetreuer_person_id !== null && data.zweitbetreuer_benutzer_uid === null
|
||||
|
||||
return hasEndabgabeWithUpload && hasZweitbetreuerWithoutBenutzerUid
|
||||
},
|
||||
sendZweitbetreuerToken(data) {
|
||||
|
||||
this.$api.call(ApiAbgabe.sendZweitbetreuerTokenMail(data.projektarbeit_id, data.betreuer_person_id, data.student_uid))
|
||||
.then(res => {
|
||||
if(res.meta.status == 'success') this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/c4zweitBegutachterTokenMailSuccess'))
|
||||
})
|
||||
},
|
||||
getDateStyleHtml(dateStyle) {
|
||||
const iconMap = {
|
||||
'verspaetet': '<i class="fa-solid fa-triangle-exclamation"></i>',
|
||||
'verpasst': '<i class="fa-solid fa-calendar-xmark"></i>',
|
||||
'abzugeben': '<i class="fa-solid fa-hourglass-half"></i>',
|
||||
'standard': '<i class="fa-solid fa-clock"></i>',
|
||||
'abgegeben': '<i class="fa-solid fa-paperclip"></i>',
|
||||
'beurteilungerforderlich': '<i class="fa-solid fa-list-check"></i>',
|
||||
'bestanden': '<i class="fa-solid fa-check"></i>',
|
||||
'nichtbestanden': '<i class="fa-solid fa-circle-exclamation"></i>',
|
||||
};
|
||||
return iconMap[dateStyle] ?? '';
|
||||
},
|
||||
statusHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) {
|
||||
const options = [
|
||||
{ label: this.$p.t('abgabetool/c4positivBenotet'), value: 'bestanden', dateStyle: 'bestanden' },
|
||||
{ label: this.$p.t('abgabetool/c4negativBenotet'), value: 'nichtbestanden', dateStyle: 'nichtbestanden' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipVerspaetet'), value: 'verspaetet', dateStyle: 'verspaetet' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipVerpasst'), value: 'verpasst', dateStyle: 'verpasst' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipAbzugeben'), value: 'abzugeben', dateStyle: 'abzugeben' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipAbgegeben'), value: 'abgegeben', dateStyle: 'abgegeben' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich'), value: 'beurteilungerforderlich', dateStyle: 'beurteilungerforderlich' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipStandardv2'), value: 'standard', dateStyle: 'standard' },
|
||||
];
|
||||
|
||||
const field = cell.getField();
|
||||
const stateKey = field + 'FilterSelected'; // e.g. dateStyleFilterSelected
|
||||
let selected = [...(this[stateKey] || [])];
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = 'position: relative; width: 100%;';
|
||||
|
||||
const display = document.createElement('input');
|
||||
display.readOnly = true;
|
||||
display.placeholder = '';
|
||||
display.style.cssText = 'padding: 4px; width: 100%; box-sizing: border-box; cursor: default; border: 1px solid; outline: none; background: #fff; appearance: none; caret-color: transparent;';
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: 220px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);';
|
||||
|
||||
const updateDisplay = () => {
|
||||
display.value = options
|
||||
.filter(o => selected.includes(o.value))
|
||||
.map(o => o.label)
|
||||
.join(', ');
|
||||
};
|
||||
|
||||
options.forEach(opt => {
|
||||
const row = document.createElement('label');
|
||||
row.style.cssText = 'display: flex; align-items: center; gap: 0; cursor: pointer; white-space: nowrap; padding-right: 8px;';
|
||||
row.addEventListener('mousedown', e => e.preventDefault());
|
||||
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.value = opt.value;
|
||||
cb.checked = selected.includes(opt.value);
|
||||
cb.style.cssText = 'margin: 0 6px;';
|
||||
cb.addEventListener('change', () => {
|
||||
selected = cb.checked
|
||||
? [...selected, opt.value]
|
||||
: selected.filter(v => v !== opt.value);
|
||||
this[stateKey] = [...selected];
|
||||
updateDisplay();
|
||||
success([...selected]);
|
||||
});
|
||||
|
||||
// icon badge — same look as cell
|
||||
const badge = document.createElement('div');
|
||||
badge.className = opt.dateStyle + '-header';
|
||||
badge.style.cssText = `min-width: 36px; height: 36px; display: flex; align-items: center;
|
||||
justify-content: center; flex-shrink: 0; padding: 0px 17px 0px 17px;`;
|
||||
badge.innerHTML = this.getDateStyleHtml(opt.dateStyle);
|
||||
|
||||
const labelText = document.createElement('span');
|
||||
labelText.textContent = opt.label;
|
||||
labelText.style.cssText = 'margin-left: 6px;';
|
||||
|
||||
row.appendChild(cb);
|
||||
row.appendChild(badge);
|
||||
row.appendChild(labelText);
|
||||
dropdown.appendChild(row);
|
||||
});
|
||||
|
||||
updateDisplay();
|
||||
|
||||
display.addEventListener('click', () => {
|
||||
if (dropdown.style.display === 'none') {
|
||||
const rect = display.getBoundingClientRect();
|
||||
dropdown.style.top = rect.bottom + 'px';
|
||||
dropdown.style.left = rect.left + 'px';
|
||||
dropdown.style.display = 'block';
|
||||
} else {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
display.addEventListener('blur', () => {
|
||||
setTimeout(() => { dropdown.style.display = 'none'; }, 150);
|
||||
});
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
wrapper.appendChild(display);
|
||||
cell.getElement().addEventListener('remove', () => dropdown.remove());
|
||||
onRendered(() => display.focus());
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
statusHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) {
|
||||
if (!filterVal || !filterVal.length) return true;
|
||||
// rowVal is the raw dateStyle string on the flat table
|
||||
return filterVal.some(val => val === rowVal);
|
||||
},
|
||||
qgateHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) {
|
||||
|
||||
const options = [
|
||||
{ label: '[+] ' + this.$p.t('abgabetool/c4positivBenotet'), value: 'positive' },
|
||||
{ label: '[-] ' + this.$p.t('abgabetool/c4negativBenotet'), value: 'negative' },
|
||||
{ label: '[~] ' + this.$p.t('abgabetool/c4notYetGraded'), value: 'not_graded' },
|
||||
{ label: '[?] ' + this.$p.t('abgabetool/c4notSubmitted'), value: 'not_submitted' },
|
||||
{ label: '[o] ' + this.$p.t('abgabetool/c4notHappenedYet'), value: 'not_happened' },
|
||||
{ label: '[--] ' + this.$p.t('abgabetool/c4keinTerminVorhanden'), value: 'no_termin' },
|
||||
];
|
||||
|
||||
const field = cell.getField();
|
||||
const stateKey = field === 'qgate1Status' ? 'qgate1FilterSelected' : 'qgate2FilterSelected';
|
||||
let selected = [...(this[stateKey] || [])]; // restore persistence state
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = 'position: relative; width: 100%;';
|
||||
|
||||
const display = document.createElement('input');
|
||||
display.readOnly = true;
|
||||
display.placeholder = '';
|
||||
display.style.cssText = 'padding: 4px; width: 100%; box-sizing: border-box; cursor: default; border: 1px solid; outline: none; background: #fff; appearance: none; caret-color: transparent;';
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: 180px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);';
|
||||
|
||||
options.forEach(opt => {
|
||||
const row = document.createElement('label');
|
||||
row.style.cssText = 'display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer; white-space: nowrap;';
|
||||
row.addEventListener('mousedown', e => e.preventDefault());
|
||||
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.value = opt.value;
|
||||
cb.checked = selected.includes(opt.value); // sync with persistence
|
||||
cb.addEventListener('change', () => {
|
||||
if (cb.checked) {
|
||||
selected.push(opt.value);
|
||||
} else {
|
||||
selected = selected.filter(v => v !== opt.value);
|
||||
}
|
||||
this[stateKey] = [...selected]; // sync with persistence
|
||||
display.value = options.filter(o => selected.includes(o.value)).map(o => o.label).join(', ');
|
||||
success([...selected]);
|
||||
});
|
||||
|
||||
row.appendChild(cb);
|
||||
row.appendChild(document.createTextNode(opt.label));
|
||||
dropdown.appendChild(row);
|
||||
});
|
||||
|
||||
display.value = options.filter(o => selected.includes(o.value)).map(o => o.label).join(', ');
|
||||
|
||||
display.addEventListener('click', () => {
|
||||
if (dropdown.style.display === 'none') {
|
||||
const rect = display.getBoundingClientRect();
|
||||
dropdown.style.top = rect.bottom + 'px';
|
||||
dropdown.style.left = rect.left + 'px';
|
||||
dropdown.style.display = 'block';
|
||||
} else {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
display.addEventListener('blur', () => {
|
||||
setTimeout(() => { dropdown.style.display = 'none'; }, 150);
|
||||
});
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
wrapper.appendChild(display);
|
||||
|
||||
cell.getElement().addEventListener('remove', () => dropdown.remove());
|
||||
|
||||
onRendered(() => display.focus());
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
qgateHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) {
|
||||
if (!filterVal || !filterVal.length) return true;
|
||||
|
||||
const matches = (val) => {
|
||||
switch (val) {
|
||||
case 'positive': return rowVal === this.$p.t('abgabetool/c4positivBenotet');
|
||||
case 'negative': return rowVal === this.$p.t('abgabetool/c4negativBenotet');
|
||||
case 'not_graded': return rowVal === this.$p.t('abgabetool/c4notYetGraded');
|
||||
case 'not_submitted':return rowVal === this.$p.t('abgabetool/c4notSubmitted');
|
||||
case 'not_happened': return rowVal === this.$p.t('abgabetool/c4notHappenedYet');
|
||||
case 'no_termin': return rowVal === this.$p.t('abgabetool/c4keinTerminVorhanden');
|
||||
default: return true;
|
||||
}
|
||||
};
|
||||
|
||||
// OR logic — row passes if it matches any selected filter
|
||||
return filterVal.some(val => matches(val));
|
||||
},
|
||||
shortLongTitleFormatter(cell, formatterParams, onRendered) {
|
||||
const longForm = cell.getValue()
|
||||
const shortForm = formatterParams?.shortForm
|
||||
|
||||
if(longForm && shortForm) {
|
||||
return `<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
|
||||
${longForm}
|
||||
</span>
|
||||
<span class="short-text" style="font-weight: bold; display: none;">
|
||||
${shortForm}
|
||||
</span>`
|
||||
} else {
|
||||
return `<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
|
||||
${longForm}
|
||||
</span>`
|
||||
}
|
||||
|
||||
},
|
||||
toolTipFuncPrevTermin(e, cell, onRendered) {
|
||||
const data = cell.getData();
|
||||
return this.mapDateStyleToTabulatorTooltip(data.prevTermin.dateStyle);
|
||||
},
|
||||
toolTipFuncNextTermin(e, cell, onRendered) {
|
||||
const data = cell.getData();
|
||||
return this.mapDateStyleToTabulatorTooltip(data.nextTermin.dateStyle);
|
||||
},
|
||||
mapDateStyleToTabulatorTooltip(dateStyleString) {
|
||||
switch(dateStyleString) {
|
||||
case 'bestanden':
|
||||
return this.$p.t('abgabetool/c4tooltipBestanden')
|
||||
break;
|
||||
case 'nichtbestanden':
|
||||
return this.$p.t('abgabetool/c4tooltipNichtBestanden')
|
||||
break;
|
||||
case 'beurteilungerforderlich':
|
||||
return this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')
|
||||
break;
|
||||
case 'verspaetet':
|
||||
return this.$p.t('abgabetool/c4tooltipVerspaetet')
|
||||
break;
|
||||
case 'abgegeben':
|
||||
return this.$p.t('abgabetool/c4tooltipAbgegeben')
|
||||
break;
|
||||
case 'verpasst':
|
||||
return this.$p.t('abgabetool/c4tooltipVerpasst')
|
||||
break;
|
||||
case 'abzugeben':
|
||||
return this.$p.t('abgabetool/c4tooltipAbzugeben')
|
||||
break;
|
||||
case 'standard':
|
||||
return this.$p.t('abgabetool/c4tooltipStandardv2')
|
||||
break;
|
||||
default: return ''
|
||||
}
|
||||
},
|
||||
handlePaUpdated(projektarbeit) {
|
||||
this.checkAbgabetermineProjektarbeit(projektarbeit)
|
||||
this.$refs.abgabeTable.tabulator.redraw(true)
|
||||
@@ -217,7 +634,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
})
|
||||
const uniqueRecipients = [...new Set(recipientList)];
|
||||
const subject = ""; // empty subject line
|
||||
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p)
|
||||
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, null, this.$fhcAlert, this.$p)
|
||||
},
|
||||
getQGateStatusList() {
|
||||
return [
|
||||
@@ -257,7 +674,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
if (val instanceof Date) {
|
||||
dt = luxon.DateTime.fromJSDate(val);
|
||||
} else if (typeof val === "string") {
|
||||
dt = luxon.DateTime.fromISO(val);
|
||||
dt = toViennaDate(val);
|
||||
} else { // fallback
|
||||
dt = luxon.DateTime.fromMillis(Number(val));
|
||||
}
|
||||
@@ -386,6 +803,9 @@ export const AbgabetoolMitarbeiter = {
|
||||
}
|
||||
this.stateRestored = true
|
||||
|
||||
// ensure that the filterCollapseables thingy has the correct values
|
||||
this.$refs.abgabeTable.setSelectedFields();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
@@ -451,6 +871,32 @@ export const AbgabetoolMitarbeiter = {
|
||||
projekt.qgate2StatusRank = 1
|
||||
}
|
||||
})
|
||||
|
||||
// set shorthand statuscode once real status has been determined
|
||||
projekt.qgate1StatusShort = this.mapRankToShortStatus(projekt.qgate1StatusRank)
|
||||
projekt.qgate2StatusShort = this.mapRankToShortStatus(projekt.qgate2StatusRank)
|
||||
},
|
||||
mapRankToShortStatus(rank) {
|
||||
switch(rank){
|
||||
case 0: // kein termin vorhanden
|
||||
return '--'
|
||||
break;
|
||||
case 1: // noch nicht stattgefunden
|
||||
return 'o'
|
||||
break;
|
||||
case 2: // noch nicht abgegeben
|
||||
return '?'
|
||||
break;
|
||||
case 3: // noch nicht benotet
|
||||
return '~'
|
||||
break;
|
||||
case 4: // negativ benotet
|
||||
return '-'
|
||||
break;
|
||||
case 5: // positiv benotet
|
||||
return '+'
|
||||
break;
|
||||
}
|
||||
},
|
||||
checkAbgabetermineProjektarbeit(projekt) {
|
||||
const now = luxon.DateTime.now()
|
||||
@@ -460,7 +906,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
// while already looping through each termin, calculate datestyle beforehand
|
||||
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
|
||||
|
||||
const date = luxon.DateTime.fromISO(termin.datum).endOf('day')
|
||||
const date = toViennaDate(termin.datum).endOf('day')
|
||||
termin.luxonDate = date
|
||||
termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past
|
||||
|
||||
@@ -482,7 +928,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
// seperate check for quality gates
|
||||
this.checkQualityGateStatus(projekt)
|
||||
},
|
||||
abgabterminFormatter(cell) {
|
||||
abgabeterminFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
|
||||
if(val) {
|
||||
@@ -514,14 +960,15 @@ export const AbgabetoolMitarbeiter = {
|
||||
break
|
||||
}
|
||||
|
||||
const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung
|
||||
const typKurzbz = val.paabgabetyp_kurzbz ?? val.bezeichnung?.paabgabetyp_kurzbz
|
||||
const bezeichnung = this.$p.t('abgabetool/c4paatyp' + typKurzbz)
|
||||
|
||||
return '<div style="display: flex; height: 100%">' +
|
||||
'<div class=' + val.dateStyle + "-header" + ' style="width:48px; height: 100%; padding: 0px; display: flex; align-items: center; justify-content: center;">' +
|
||||
icon +
|
||||
'<div class=' + val.dateStyle + "-header" + ' style="min-width:48px; height: 100%; padding: 0px; display: flex; align-items: center; justify-content: center;">' +
|
||||
icon +
|
||||
'</div>' +
|
||||
'<div style="margin-left: 4px;">' +
|
||||
'<p style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+bezeichnung+' - '+ this.formatDate(val.datum)+'</p>' +
|
||||
'<p style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">'+bezeichnung+' - '+ this.formatDate(val.datum)+'</p>' +
|
||||
'</div>'+
|
||||
'</div>'
|
||||
|
||||
@@ -545,16 +992,19 @@ export const AbgabetoolMitarbeiter = {
|
||||
},
|
||||
selectAllHandler(e, cell) {
|
||||
const table = cell.getTable();
|
||||
const rows = table.getRows();
|
||||
const rows = this.filteredRows ?? table.getRows();
|
||||
|
||||
// custom select all logic
|
||||
const allowed = rows.filter(r => r.getData().selectable);
|
||||
// since betreuerpage acctually has logic behind selectable flag, it is important to go over allowed only here
|
||||
const selected = allowed.every(r => r.isSelected());
|
||||
|
||||
if(selected) {
|
||||
if(selected){
|
||||
allowed.forEach(r => r.deselect());
|
||||
e.target.checked = false;
|
||||
} else {
|
||||
allowed.forEach(r => r.select());
|
||||
e.target.checked = true;
|
||||
}
|
||||
|
||||
// stop built-in handler
|
||||
@@ -565,18 +1015,10 @@ export const AbgabetoolMitarbeiter = {
|
||||
this.detailIsFullscreen = !this.detailIsFullscreen
|
||||
},
|
||||
getOptionLabelAbgabetyp(option){
|
||||
return option.bezeichnung
|
||||
return this.$p.t('abgabetool/c4paatyp' + option.paabgabetyp_kurzbz)
|
||||
},
|
||||
formatDate(dateParam) {
|
||||
const date = new Date(dateParam)
|
||||
// handle missing leading 0
|
||||
const padZero = (num) => String(num).padStart(2, '0');
|
||||
|
||||
const month = padZero(date.getMonth() + 1); // Months are zero-based
|
||||
const day = padZero(date.getDate());
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}.${month}.${year}`;
|
||||
return formatISODate(dateParam);
|
||||
},
|
||||
undoSelection(cell) {
|
||||
// checks if cells row is selected and unselects -> imitates columns which dont trigger row selection
|
||||
@@ -589,6 +1031,8 @@ export const AbgabetoolMitarbeiter = {
|
||||
},
|
||||
selectionCheck(row) {
|
||||
const data = row.getData()
|
||||
|
||||
// zweitbetreuer cant select projektarbeiten for serientermine
|
||||
if(data?.betreuerart_kurzbz == 'Zweitbegutachter') return false
|
||||
return true
|
||||
},
|
||||
@@ -612,7 +1056,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
addSeries() {
|
||||
this.saving = true
|
||||
this.$api.call(ApiAbgabe.postSerientermin(
|
||||
this.serienTermin.datum.toISOString(),
|
||||
this.serienTermin.datum,
|
||||
this.serienTermin.bezeichnung.paabgabetyp_kurzbz,
|
||||
this.serienTermin.bezeichnung.bezeichnung,
|
||||
this.serienTermin.kurzbz,
|
||||
@@ -710,7 +1154,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
termin.allowedToSave = paIsBenotet ? false : true
|
||||
|
||||
// lektoren are not allowed to delete deadlines with existing submissions
|
||||
termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum
|
||||
termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum && !termin.note
|
||||
|
||||
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
|
||||
|
||||
@@ -727,28 +1171,37 @@ export const AbgabetoolMitarbeiter = {
|
||||
|
||||
},
|
||||
centeredTextFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
if(!val) return
|
||||
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<p style="max-width: 100%; width: 100%; overflow-wrap: break-word; word-break: break-word; white-space: normal; margin: 0px; text-align: center">'+val+'</p></div>'
|
||||
const longForm = cell.getValue()
|
||||
if(!longForm) return
|
||||
const data = cell.getData()
|
||||
const entry = Object.entries(data).find(entry => entry[1] == longForm)
|
||||
|
||||
// shortFormKey must have same keyname as longForm but with 'Short' appended
|
||||
const shortForm = data[entry[0]+'Short']
|
||||
|
||||
if(shortForm && longForm) {
|
||||
return `<div style="display: flex; justify-content: start; align-items: center; height: 100%; width: 100%;">
|
||||
<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
|
||||
${longForm}
|
||||
</span>
|
||||
<span class="short-text" style="font-weight: bold; display: none;">
|
||||
${shortForm}
|
||||
</span>
|
||||
</div>`;
|
||||
} else {
|
||||
return '<div style="display: flex; justify-content: start; align-items: center; height: 100%">' +
|
||||
'<p style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">'+longForm+'</p></div>'
|
||||
}
|
||||
},
|
||||
detailFormatter(cell) {
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
return '<div style="display: flex; justify-content: start; align-items: center; height: 100%">' +
|
||||
'<a><i class="fa fa-folder-open" style="color:#00649C"></i></a></div>'
|
||||
},
|
||||
beurteilungFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
if(val) {
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<a><i class="fa fa-file-pdf" style="color:#00649C"></i></a></div>'
|
||||
} else return '-'
|
||||
},
|
||||
pkzTextFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<a style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+val+'</a></div>'
|
||||
return '<div style="display: flex; justify-content: start; align-items: center; height: 100%">' +
|
||||
'<a style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">'+val+'</a></div>'
|
||||
},
|
||||
tableResolve(resolve) {
|
||||
this.tableBuiltResolve = resolve
|
||||
@@ -763,15 +1216,19 @@ export const AbgabetoolMitarbeiter = {
|
||||
return (projekt.typ + projekt.kurzbz)?.toUpperCase()
|
||||
},
|
||||
setupData(data){
|
||||
|
||||
|
||||
this.projektarbeiten = data[0]
|
||||
this.domain = data[1]
|
||||
|
||||
this.tableData = data[0]?.retval?.map(projekt => {
|
||||
this.projektarbeiten = data[0]?.retval?.map(projekt => {
|
||||
this.checkAbgabetermineProjektarbeit(projekt)
|
||||
projekt.selectable = projekt.betreuerart_kurzbz !== 'Zweitbegutachter'
|
||||
|
||||
if(this.notenOptions && projekt.note) {
|
||||
const opt = this.notenOptions.find(n => n.note == projekt.note)
|
||||
// TODO: mehrsprachig englisch -> nevermind the english field in
|
||||
// notenoption->bezeichnung_mehrsprachig is ALWAYS german
|
||||
projekt.note_bez = opt?.bezeichnung
|
||||
}
|
||||
|
||||
return {
|
||||
...projekt,
|
||||
details: {
|
||||
@@ -787,9 +1244,10 @@ export const AbgabetoolMitarbeiter = {
|
||||
titel: projekt.titel
|
||||
}
|
||||
})
|
||||
this.count = this.projektarbeiten.length
|
||||
|
||||
this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns)
|
||||
this.$refs.abgabeTable.tabulator.setData(this.tableData);
|
||||
this.$refs.abgabeTable.tabulator.setData(this.projektarbeiten);
|
||||
},
|
||||
loadProjektarbeiten(all = false, callback) {
|
||||
this.$api.call(ApiAbgabe.getMitarbeiterProjektarbeiten(all))
|
||||
@@ -838,9 +1296,20 @@ export const AbgabetoolMitarbeiter = {
|
||||
}
|
||||
|
||||
this.serienTermin.upload_allowed = newVal.upload_allowed_default
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
countsToHTML() {
|
||||
return this.$p.t('global/ausgewaehlt')
|
||||
+ ': <strong>' + (this.selectedcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gefiltert')
|
||||
+ ': '
|
||||
+ '<strong>' + (this.filteredcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gesamt')
|
||||
+ ': <strong>' + (this.count || 0) + '</strong>';
|
||||
},
|
||||
emailItems() {
|
||||
const menu = []
|
||||
|
||||
@@ -869,6 +1338,8 @@ export const AbgabetoolMitarbeiter = {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
document.documentElement.classList.add('abgabetool');
|
||||
|
||||
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
|
||||
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
|
||||
// fetch config to avoid hard coded links
|
||||
@@ -910,12 +1381,16 @@ export const AbgabetoolMitarbeiter = {
|
||||
mounted() {
|
||||
this.setupMounted()
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.documentElement.classList.remove('abgabetool');
|
||||
},
|
||||
template: `
|
||||
<template v-if="phrasenResolved">
|
||||
<FhcOverlay :active="loading || saving"></FhcOverlay>
|
||||
|
||||
<bs-modal ref="modalContainerAddSeries" class="bootstrap-prompt"
|
||||
dialogClass="modal-lg">
|
||||
dialogClass="modal-lg"
|
||||
bodyClass="px-4 py-4">
|
||||
<template v-slot:title>
|
||||
<div>
|
||||
{{ $p.t('abgabetool/neueTerminserie') }}
|
||||
@@ -935,6 +1410,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
:enable-time-picker="false"
|
||||
locale="de"
|
||||
format="dd.MM.yyyy"
|
||||
model-type="yyyy-MM-dd"
|
||||
:text-input="true"
|
||||
auto-apply>
|
||||
</VueDatePicker>
|
||||
@@ -982,7 +1458,8 @@ export const AbgabetoolMitarbeiter = {
|
||||
|
||||
<bs-modal ref="modalContainerAbgabeDetail" class="bootstrap-prompt"
|
||||
dialogClass="modal-xl" :allowFullscreenExpand="true"
|
||||
@toggle-fullscreen="handleToggleFullscreenDetail">
|
||||
@toggle-fullscreen="handleToggleFullscreenDetail"
|
||||
bodyClass="px-4 py-4">
|
||||
<template v-slot:title>
|
||||
<div>
|
||||
{{$p.t('abgabetool/c4abgabeMitarbeiterDetailTitle')}}
|
||||
@@ -1001,12 +1478,13 @@ export const AbgabetoolMitarbeiter = {
|
||||
<!-- low max height on this vsplit wrapper to avoid padding scrolls, elements have their inherent height anyways -->
|
||||
<div id="abgabetable" style="max-height:40vw;">
|
||||
|
||||
<h2>{{$p.t('abgabetool/abgabetoolTitle')}}</h2>
|
||||
<h2>{{$p.t('abgabetool/abgabetoolTitleBetreuer')}}</h2>
|
||||
<hr>
|
||||
<core-filter-cmpt
|
||||
:title="''"
|
||||
@uuidDefined="handleUuidDefined"
|
||||
ref="abgabeTable"
|
||||
:description="countsToHTML"
|
||||
:newBtnShow="true"
|
||||
:newBtnLabel="$p.t('abgabetool/neueTerminserie')"
|
||||
:newBtnDisabled="!selectedData.length"
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import AbgabeDetail from "./AbgabeStudentDetail.js";
|
||||
import ApiAbgabe from '../../../api/factory/abgabe.js'
|
||||
import ApiAuthinfo from '../../../api/factory/authinfo.js';
|
||||
import BsModal from "../../Bootstrap/Modal.js";
|
||||
import FhcOverlay from "../../Overlay/FhcOverlay.js";
|
||||
import { getDateStyleClass} from "./getDateStyleClass.js";
|
||||
import { validateThesisTitle } from './titleValidation.js'
|
||||
|
||||
export const AbgabetoolStudent = {
|
||||
name: "AbgabetoolStudent",
|
||||
components: {
|
||||
Accordion: primevue.accordion,
|
||||
AccordionTab: primevue.accordiontab,
|
||||
Textarea: primevue.textarea,
|
||||
BsModal,
|
||||
AbgabeDetail,
|
||||
FhcOverlay
|
||||
@@ -17,21 +20,17 @@ export const AbgabetoolStudent = {
|
||||
return {
|
||||
notenOptions: Vue.computed(() => this.notenOptions),
|
||||
isViewMode: Vue.computed(() => this.isViewMode),
|
||||
moodle_link: Vue.computed(() => this.moodle_link)
|
||||
moodle_link: Vue.computed(() => this.moodle_link),
|
||||
title_edit_allowed: Vue.computed(() => this.title_edit_allowed),
|
||||
confetti_on_endupload: Vue.computed(() => this.confetti_on_endupload),
|
||||
siginfolink_german: Vue.computed(() => this.siginfolink_german),
|
||||
siginfolink_english: Vue.computed(() => this.siginfolink_english)
|
||||
}
|
||||
},
|
||||
props: {
|
||||
student_uid_prop: {
|
||||
default: null
|
||||
},
|
||||
viewData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({uid: ''}),
|
||||
validator(value) {
|
||||
return value && value.uid
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -44,14 +43,86 @@ export const AbgabetoolStudent = {
|
||||
detail: null,
|
||||
projektarbeiten: null,
|
||||
selectedProjektarbeit: null,
|
||||
moodle_link: null
|
||||
moodle_link: null,
|
||||
uid: null,
|
||||
title_edit_allowed: null,
|
||||
confetti_on_endupload: null,
|
||||
siginfolink_german: null,
|
||||
siginfolink_english: null,
|
||||
editingTitel: '',
|
||||
editingProjektarbeit: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isViewMode() {
|
||||
return this.student_uid !== this.uid
|
||||
},
|
||||
student_uid() {
|
||||
return this.student_uid_prop || this.uid || null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openTitelEdit(projektarbeit, event) {
|
||||
// stop the click from toggling the accordion tab
|
||||
event.stopPropagation();
|
||||
this.editingProjektarbeit = projektarbeit;
|
||||
this.editingTitel = projektarbeit.titel ?? '';
|
||||
this.$refs.modalTitelEdit.show();
|
||||
},
|
||||
async saveTitel() {
|
||||
const validation = validateThesisTitle(this.editingTitel);
|
||||
|
||||
if (!validation.isValid) {
|
||||
if (validation.error === 'empty') {
|
||||
this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4emptyThesisTitle'))
|
||||
} else if (validation.error === 'invalid_characters') {
|
||||
this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4invalidCharactersThesisTitle'))
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const confirmed = await this.$fhcAlert.confirm({
|
||||
message: this.$p.t('abgabetool/c4confirmTitelSpeichern'),
|
||||
acceptLabel: this.$capitalize(this.$p.t('ui/speichern')),
|
||||
acceptClass: 'p-button-primary',
|
||||
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
|
||||
rejectClass: 'p-button-secondary'
|
||||
});
|
||||
|
||||
if (confirmed === false) return;
|
||||
|
||||
this.loading = true;
|
||||
this.$api.call(
|
||||
ApiAbgabe.postStudentProjektarbeitTitel(
|
||||
this.editingProjektarbeit.projektarbeit_id,
|
||||
validation.cleanedTitle
|
||||
)
|
||||
).then(res => {
|
||||
if (res.meta.status === 'success') {
|
||||
// update the local list entry in-place so the accordion header reflects it immediately
|
||||
this.editingProjektarbeit.titel = res.data;
|
||||
// keep the open detail modal in sync if it happens to be showing this projektarbeit
|
||||
if (this.selectedProjektarbeit?.projektarbeit_id === this.editingProjektarbeit.projektarbeit_id) {
|
||||
this.selectedProjektarbeit.titel = res.data;
|
||||
}
|
||||
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4titelSavedSuccess')));
|
||||
this.$refs.modalTitelEdit.hide();
|
||||
} else {
|
||||
this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4titelSaveError')));
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleTitelUpdated(projektarbeit_id, titel) {
|
||||
const pa = this.projektarbeiten?.find(p => p.projektarbeit_id === projektarbeit_id);
|
||||
if (pa) pa.titel = titel;
|
||||
},
|
||||
checkQualityGatesStrict(termine) {
|
||||
let qgate1Passed = false
|
||||
let qgate2Passed = false
|
||||
|
||||
|
||||
termine.forEach(t => {
|
||||
const noteOption = this.notenOptions?.find(opt => opt.note == t.note)
|
||||
if(noteOption && noteOption.positiv) {
|
||||
@@ -68,7 +139,7 @@ export const AbgabetoolStudent = {
|
||||
checkQualityGatesOptional(termine) {
|
||||
const qgate1found = termine.find(t => t.paabgabetyp_kurzbz == 'qualgate1')
|
||||
const qgate2found = termine.find(t => t.paabgabetyp_kurzbz == 'qualgate2')
|
||||
|
||||
|
||||
let qgate1positiv = true
|
||||
if(qgate1found) {
|
||||
qgate1positiv = false
|
||||
@@ -109,47 +180,35 @@ export const AbgabetoolStudent = {
|
||||
this.loadAbgaben(details).then((res)=> {
|
||||
const pa = this.projektarbeiten?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
|
||||
pa.abgabetermine = res.data[0].retval
|
||||
|
||||
|
||||
const paIsBenotet = pa.note !== null
|
||||
|
||||
|
||||
pa.abgabetermine.forEach(termin => {
|
||||
termin.file = []
|
||||
termin.allowedToUpload = false
|
||||
|
||||
|
||||
if(termin.paabgabetyp_kurzbz == 'end') {
|
||||
// old assumed production logic when qgates are required
|
||||
// termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesStrict(pa.abgabetermine)
|
||||
|
||||
const inTime = termin.fixtermin ? !this.isPastDate(termin.datum) : true
|
||||
termin.allowedToUpload = inTime && this.checkQualityGatesOptional(pa.abgabetermine)
|
||||
|
||||
|
||||
// development purposes
|
||||
// termin.allowedToUpload = this.checkQualityGatesStrict(pa.abgabetermine)
|
||||
// termin.allowedToUpload = true
|
||||
|
||||
} else if(termin.fixtermin) {
|
||||
termin.allowedToUpload = !this.isPastDate(termin.datum)
|
||||
} else {
|
||||
// this could confuse people since we should dont show people this flag
|
||||
termin.allowedToUpload = termin.upload_allowed
|
||||
termin.allowedToUpload = termin.upload_allowed
|
||||
}
|
||||
|
||||
// blocks client upload button if projektarbeitet is already beurteilt und thus further abgaben on any termin should be blocked
|
||||
if(paIsBenotet) termin.allowedToUpload = false
|
||||
|
||||
|
||||
|
||||
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
|
||||
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
|
||||
})
|
||||
|
||||
|
||||
pa.betreuer = this.buildBetreuer(pa)
|
||||
pa.student_uid = this.student_uid
|
||||
|
||||
|
||||
this.selectedProjektarbeit = pa
|
||||
|
||||
this.$refs.modalContainerAbgabeDetail.show()
|
||||
|
||||
|
||||
}).finally(()=>{this.loading=false})
|
||||
},
|
||||
centeredTextFormatter(cell) {
|
||||
@@ -171,8 +230,8 @@ export const AbgabetoolStudent = {
|
||||
},
|
||||
mailFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
|
||||
'<a href='+val+'><i class="fa fa-envelope" style="color:#00649C"></i></a></div>'
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
|
||||
'<a href='+val+'><i class="fa fa-envelope" style="color:#00649C"></i></a></div>'
|
||||
},
|
||||
beurteilungFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
@@ -182,19 +241,17 @@ export const AbgabetoolStudent = {
|
||||
} else return '-'
|
||||
},
|
||||
buildMailToLink(projekt) {
|
||||
// should always be "projekt.mitarbeiter_uid +'@'+ this.domain", built in backend
|
||||
return 'mailto:' + projekt.email
|
||||
},
|
||||
buildBetreuer(abgabe) {
|
||||
return (abgabe.btitelpre ? abgabe.btitelpre + ' ' : '') + abgabe.bvorname + ' ' + abgabe.bnachname + (abgabe.btitelpost ? ' ' + abgabe.btitelpost : '')
|
||||
},
|
||||
async setupData(data){
|
||||
// this.projektarbeiten = data[0]
|
||||
const projektarbeiten = data[0] ?? null
|
||||
if(!projektarbeiten) return
|
||||
this.projektarbeiten = projektarbeiten.map(projekt => {
|
||||
let mode = 'detailTermine'
|
||||
|
||||
|
||||
return {
|
||||
...projekt,
|
||||
details: {
|
||||
@@ -228,16 +285,14 @@ export const AbgabetoolStudent = {
|
||||
.then(res => {
|
||||
resolve(res)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
async setupMounted() {
|
||||
this.loadProjektarbeiten()
|
||||
},
|
||||
getAccTabHeaderForProjektarbeit(projektarbeit) {
|
||||
let title = ''
|
||||
|
||||
title += projektarbeit.titel ?? this.$p.t('abgabetool/keinTitel')
|
||||
|
||||
return title
|
||||
},
|
||||
getMailLink(projektarbeit) {
|
||||
@@ -245,6 +300,31 @@ export const AbgabetoolStudent = {
|
||||
return 'mailto:'+projektarbeit.email
|
||||
} else return ''
|
||||
},
|
||||
getZweitbetreuerMailLink(projektarbeit) {
|
||||
if(projektarbeit.zweitbetreuer_mail) {
|
||||
return 'mailto:'+projektarbeit.zweitbetreuer_mail
|
||||
} else return ''
|
||||
},
|
||||
hasZweitbetreuer(projektarbeit) {
|
||||
return !!(projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer)
|
||||
},
|
||||
getErstbetreuerEmailLabel(projektarbeit) {
|
||||
// with only one betreuer keep the generic label; with multiple betreuer
|
||||
// distinguish the email rows by their betreuungsart bezeichnung
|
||||
if(!this.hasZweitbetreuer(projektarbeit)) {
|
||||
return this.$capitalize(this.$p.t('abgabetool/c4betreuerEmailKontaktv2'))
|
||||
}
|
||||
const art = projektarbeit.betreuerart_kurzbz
|
||||
? this.$p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz)
|
||||
: this.$p.t('abgabetool/c4betreuerv2')
|
||||
return this.$capitalize(this.$p.t('abgabetool/c4emailBetreuungsart', [art]))
|
||||
},
|
||||
getZweitbetreuerEmailLabel(projektarbeit) {
|
||||
const art = projektarbeit.zweitbetreuer_betreuerart_kurzbz
|
||||
? this.$p.t('abgabetool/c4betrart' + projektarbeit.zweitbetreuer_betreuerart_kurzbz)
|
||||
: ''
|
||||
return this.$capitalize(this.$p.t('abgabetool/c4emailBetreuungsart', [art]))
|
||||
},
|
||||
getNoteBezeichnung(projektarbeit) {
|
||||
if(projektarbeit.note && this.notenOptions) {
|
||||
const noteOpt = this.notenOptions.find(opt => opt.note == projektarbeit.note)
|
||||
@@ -258,25 +338,21 @@ export const AbgabetoolStudent = {
|
||||
},
|
||||
handleDownloadBeurteilung2(projektarbeit) {
|
||||
window.open(projektarbeit.beurteilung2)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
},
|
||||
computed: {
|
||||
isViewMode() {
|
||||
return this.student_uid !== this.viewData.uid
|
||||
},
|
||||
student_uid() {
|
||||
return this.student_uid_prop || this.viewData?.uid || null
|
||||
}
|
||||
async fetchAuthUID() {
|
||||
const authIdResponse = await this.$api.call(ApiAuthinfo.getAuthUID());
|
||||
this.uid = authIdResponse.data.uid;
|
||||
},
|
||||
},
|
||||
watch: {},
|
||||
async created() {
|
||||
// make sure zoom media query doesnt spill ever to other CIS4 sites
|
||||
document.documentElement.classList.add('abgabetool');
|
||||
|
||||
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
|
||||
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
|
||||
|
||||
|
||||
this.loading = true
|
||||
//TODO: SWITCH TO NOTEN API ONCE NOTENTOOL IS IN MASTER TO AVOID DUPLICATE API
|
||||
await this.$api.call(ApiAbgabe.getNoten()).then(res => {
|
||||
if(res.meta.status == 'success') {
|
||||
this.notenOptions = res.data[0]
|
||||
@@ -289,43 +365,94 @@ export const AbgabetoolStudent = {
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
// fetch abgabetypen options
|
||||
this.$api.call(ApiAbgabe.getPaAbgabetypen()).then(res => {
|
||||
this.abgabeTypeOptions = res.data
|
||||
}).catch(e => {
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
// fetch config to avoid hard coded links
|
||||
this.$api.call(ApiAbgabe.getConfigStudent()).then(res => {
|
||||
this.moodle_link = res.data?.moodle_link
|
||||
this.title_edit_allowed = res.data?.title_edit_allowed
|
||||
this.confetti_on_endupload = res.data?.confetti_on_endupload
|
||||
this.siginfolink_german = res.data?.siginfolink_german
|
||||
this.siginfolink_english = res.data?.siginfolink_english
|
||||
}).catch(e => {
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
await this.fetchAuthUID();
|
||||
},
|
||||
mounted() {
|
||||
this.setupMounted()
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.documentElement.classList.remove('abgabetool');
|
||||
},
|
||||
template: `
|
||||
<template v-if="phrasenResolved">
|
||||
<FhcOverlay :active="loading"></FhcOverlay>
|
||||
|
||||
<bs-modal ref="modalContainerAbgabeDetail" class="bootstrap-prompt"
|
||||
dialogClass="modal-xl" :allowFullscreenExpand="true">
|
||||
dialogClass="modal-xl" :allowFullscreenExpand="true" bodyClass="px-4 py-4">
|
||||
<template v-slot:title>
|
||||
<div>
|
||||
{{$capitalize( $p.t('abgabetool/c4abgabeStudentDetailTitle') )}}
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
<AbgabeDetail :projektarbeit="selectedProjektarbeit"></AbgabeDetail>
|
||||
<AbgabeDetail
|
||||
:projektarbeit="selectedProjektarbeit"
|
||||
@titel-updated="handleTitelUpdated"
|
||||
></AbgabeDetail>
|
||||
</template>
|
||||
</bs-modal>
|
||||
<bs-modal
|
||||
ref="modalTitelEdit"
|
||||
class="bootstrap-prompt"
|
||||
dialogClass="bordered-modal"
|
||||
bodyClass="px-4 py-4"
|
||||
>
|
||||
<template v-slot:title>
|
||||
{{$capitalize( $p.t('abgabetool/c4titelBearbeiten') )}}
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
<div class="mb-2">
|
||||
<label class="form-label fw-bold">
|
||||
{{$capitalize( $p.t('abgabetool/c4titel') )}}
|
||||
</label>
|
||||
<Textarea
|
||||
v-model="editingTitel"
|
||||
rows="2"
|
||||
maxlength="1024"
|
||||
class="form-control w-100"
|
||||
@keydown.enter.prevent="saveTitel"
|
||||
/>
|
||||
<div class="form-text text-end">{{ editingTitel.length }} / 1024</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="$refs.modalTitelEdit.hide()"
|
||||
>
|
||||
{{$capitalize( $p.t('abgabetool/c4Cancel') )}}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="!editingTitel.trim()"
|
||||
@click="saveTitel"
|
||||
>
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
{{$capitalize( $p.t('ui/speichern') )}}
|
||||
</button>
|
||||
</template>
|
||||
</bs-modal>
|
||||
|
||||
<h2>{{$capitalize( $p.t('abgabetool/abgabetoolTitle') )}}</h2>
|
||||
<hr>
|
||||
|
||||
<div v-if="projektarbeiten === null">
|
||||
<div v-if="projektarbeiten === null || projektarbeiten?.length == 0">
|
||||
{{$capitalize( $p.t('abgabetool/c4abgabeStudentNoProjectsFound') )}}
|
||||
</div>
|
||||
|
||||
@@ -335,8 +462,12 @@ export const AbgabetoolStudent = {
|
||||
|
||||
<template #header>
|
||||
<div class="d-flex row w-100">
|
||||
<div class="text-start" :class="projektarbeit.note != null ? 'col-6' : 'col-12'">
|
||||
<span>{{getAccTabHeaderForProjektarbeit(projektarbeit)}}</span>
|
||||
<div class="text-start" :class="projektarbeit.note != null ? 'col-6' : 'col-12'"
|
||||
style="min-width: 0;">
|
||||
<span
|
||||
:title="getAccTabHeaderForProjektarbeit(projektarbeit)"
|
||||
style="display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 600px;"
|
||||
>{{getAccTabHeaderForProjektarbeit(projektarbeit)}}</span>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<span>{{getNoteBezeichnung(projektarbeit)}}</span>
|
||||
@@ -385,7 +516,7 @@ export const AbgabetoolStudent = {
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4betreuerEmailKontaktv2') )}}</div>
|
||||
<div class="col-4 col-md-3 fw-bold">{{ getErstbetreuerEmailLabel(projektarbeit) }}</div>
|
||||
<div class="col-8 col-md-9">
|
||||
<a :href="getMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a>
|
||||
</div>
|
||||
@@ -396,18 +527,46 @@ export const AbgabetoolStudent = {
|
||||
{{ projektarbeit.zweitbetreuer?.first }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer" class="row mt-2">
|
||||
<div class="col-4 col-md-3 fw-bold">{{ getZweitbetreuerEmailLabel(projektarbeit) }}</div>
|
||||
<div class="col-8 col-md-9">
|
||||
<a v-if="projektarbeit.zweitbetreuer_mail" :href="getZweitbetreuerMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4projekttyp') )}}</div>
|
||||
<div class="col-8 col-md-9">
|
||||
{{ projektarbeit.projekttypbezeichnung }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-2">
|
||||
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4titel') )}}</div>
|
||||
<div class="col-8 col-md-9">
|
||||
{{ projektarbeit.titel }}
|
||||
<div class="col-8 col-md-9 d-flex align-items-center gap-2" style="min-width: 0;">
|
||||
<span
|
||||
:title="projektarbeit.titel"
|
||||
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||
>{{ projektarbeit.titel }}</span>
|
||||
<button
|
||||
v-if="title_edit_allowed && !isViewMode && projektarbeit.note == null"
|
||||
class="btn btn-sm btn-outline-secondary border-0 p-1"
|
||||
v-tooltip.right="{ value: $capitalize($p.t('abgabetool/c4titelBearbeiten')), class: 'custom-tooltip' }"
|
||||
@click="openTitelEdit(projektarbeit, $event)"
|
||||
>
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-2">
|
||||
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4note') )}}</div>
|
||||
|
||||
<div class="col-8 col-md-9">
|
||||
<span>{{getNoteBezeichnung(projektarbeit)}}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</AccordionTab>
|
||||
</template>
|
||||
</Accordion>
|
||||
|
||||
@@ -10,14 +10,6 @@ export const DeadlineOverview = {
|
||||
person_uid_prop: {
|
||||
default: null
|
||||
},
|
||||
viewData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({name: '', uid: ''}),
|
||||
validator(value) {
|
||||
return value && value.name && value.uid
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const zone = 'Europe/Vienna';
|
||||
|
||||
export function getViennaTodayISO() {
|
||||
return luxon.DateTime.now().setZone(zone).toISODate();
|
||||
}
|
||||
|
||||
export function formatISODate(dateParam) {
|
||||
if (!dateParam) return '';
|
||||
|
||||
const date = luxon.DateTime.fromISO(String(dateParam), { zone });
|
||||
return date.isValid ? date.toFormat('dd.MM.yyyy') : '';
|
||||
}
|
||||
|
||||
export function formatDateTime(dateParam) {
|
||||
if (!dateParam) return '';
|
||||
|
||||
const date = luxon.DateTime.fromSQL(dateParam);
|
||||
return date.isValid ? date.toFormat("dd.MM.yyyy HH:mm") : '';
|
||||
}
|
||||
|
||||
export function toViennaDate(dateParam) {
|
||||
if (!dateParam) return null;
|
||||
|
||||
return luxon.DateTime.fromISO(String(dateParam), { zone });
|
||||
}
|
||||
|
||||
export function compareISODateValues(a, b) {
|
||||
if (!a && !b) return 0;
|
||||
if (!a) return 1;
|
||||
if (!b) return -1;
|
||||
|
||||
return String(a).localeCompare(String(b));
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
const zone = 'Europe/Vienna';
|
||||
const today = luxon.DateTime.now().setZone(zone);
|
||||
|
||||
export function getDateStyleClass(termin, notenOptions) {
|
||||
const today = luxon.DateTime.now().setZone(zone);
|
||||
const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day');
|
||||
const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null;
|
||||
termin.diffindays = datum.diff(today, 'days').days;
|
||||
@@ -28,10 +28,11 @@ export function getDateStyleClass(termin, notenOptions) {
|
||||
|
||||
// no submission yet
|
||||
if (datum < today) return 'verpasst';
|
||||
if (termin.diffindays <= 12) return 'abzugeben';
|
||||
return 'standard';
|
||||
|
||||
}
|
||||
|
||||
// GENERIC STATUS
|
||||
return datum < today ? 'verpasst' : 'standard';
|
||||
}
|
||||
// GENERIC STATUS — applies to all termine
|
||||
if (datum < today) return 'verpasst';
|
||||
if (termin.diffindays <= 12) return 'abzugeben';
|
||||
return 'standard';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Validates the thesis title on the frontend
|
||||
* @param {string} title - The raw input from the title field
|
||||
* @returns {object} Validation result containing status and cleaned title
|
||||
*/
|
||||
export function validateThesisTitle(title) {
|
||||
if (!title) {
|
||||
return { isValid: false, error: 'empty' };
|
||||
}
|
||||
|
||||
// Replicate strip_tags / trim
|
||||
const cleanedTitle = title.replace(/<\/?[^>]+(>|$)/g, "").trim();
|
||||
|
||||
if (cleanedTitle === '') {
|
||||
return { isValid: false, error: 'empty' };
|
||||
}
|
||||
|
||||
// Replicate the emoji/pictograph rejection
|
||||
const emojiRegex = /\p{Extended_Pictographic}/u;
|
||||
if (emojiRegex.test(cleanedTitle)) {
|
||||
return { isValid: false, error: 'invalid_characters' };
|
||||
}
|
||||
|
||||
return { isValid: true, cleanedTitle: cleanedTitle };
|
||||
}
|
||||
@@ -2,115 +2,137 @@ import Pagination from "../../Pagination/Pagination.js";
|
||||
import StudiengangInformation from "./StudiengangInformation/StudiengangInformation.js";
|
||||
import BsConfirm from "../../Bootstrap/Confirm.js";
|
||||
|
||||
import ApiCms from '../../../api/factory/cms.js';
|
||||
import ApiCms from "../../../api/factory/cms.js";
|
||||
|
||||
export default {
|
||||
name: "NewsComponent",
|
||||
components: {
|
||||
Pagination,
|
||||
StudiengangInformation,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
content: null,
|
||||
maxPageCount: 0,
|
||||
page_size: 10,
|
||||
page:1,
|
||||
};
|
||||
},
|
||||
watch:{
|
||||
'$p.user_language.value':function(sprache){
|
||||
this.fetchNews();
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
sprache: function(){
|
||||
return this.$p.user_language.value;
|
||||
components: {
|
||||
Pagination,
|
||||
StudiengangInformation,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchNews() {
|
||||
return this.$api
|
||||
.call(ApiCms.getNews(this.page, this.page_size, this.sprache))
|
||||
.then(res => res.data)
|
||||
.then(result => {
|
||||
this.content = result;
|
||||
inject: ["isMobile"],
|
||||
data() {
|
||||
return {
|
||||
content: null,
|
||||
maxPageCount: 0,
|
||||
page_size: 10,
|
||||
page: 1,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
"$p.user_language.value": function (sprache) {
|
||||
this.fetchNews();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
sprache: function () {
|
||||
return this.$p.user_language.value;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async fetchNews() {
|
||||
let newsResponse = await this.$api.call(
|
||||
ApiCms.getNews(this.page, this.page_size, this.sprache),
|
||||
);
|
||||
this.content = newsResponse.data;
|
||||
|
||||
document.querySelectorAll("#cms [data-confirm]").forEach((el) => {
|
||||
el.addEventListener("click", (evt) => {
|
||||
evt.preventDefault();
|
||||
BsConfirm.popup(el.dataset.confirm)
|
||||
.then(() => {
|
||||
Axios.get(el.href)
|
||||
.then((res) => {
|
||||
// TODO(chris): check for success then show message and/or reload
|
||||
location = location;
|
||||
})
|
||||
.catch((err) => console.error("ERROR:", err));
|
||||
document.querySelectorAll("#cms [data-confirm]").forEach((el) => {
|
||||
el.addEventListener("click", (evt) => {
|
||||
evt.preventDefault();
|
||||
BsConfirm.popup(el.dataset.confirm)
|
||||
.then(() => {
|
||||
Axios.get(el.href)
|
||||
.then((res) => {
|
||||
// TODO(chris): check for success then show message and/or reload
|
||||
location = location;
|
||||
})
|
||||
.catch(() => {
|
||||
});
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("#cms [data-href]").forEach((el) => {
|
||||
el.href = el.dataset.href.replace(
|
||||
/^ROOT\//,
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
);
|
||||
});
|
||||
Vue.nextTick(()=>{
|
||||
document.querySelectorAll(".card-header").forEach((el) => {
|
||||
el.classList.add("fhc-primary");
|
||||
});
|
||||
document.querySelectorAll(".row").forEach((el) => {
|
||||
el.classList.add("w-100");
|
||||
el.classList.add("align-items-center");
|
||||
|
||||
});
|
||||
document.querySelectorAll(".row h2").forEach((el) => {
|
||||
el.classList.add("mb-0");
|
||||
});
|
||||
.catch((err) => console.error("ERROR:", err));
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("#cms [data-href]").forEach((el) => {
|
||||
el.href = el.dataset.href.replace(
|
||||
/^ROOT\//,
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root,
|
||||
);
|
||||
});
|
||||
|
||||
})
|
||||
await this.$nextTick();
|
||||
this.formatExternalHtml();
|
||||
},
|
||||
async loadNewPageContent(data) {
|
||||
let newsResponse = await this.$api.call(
|
||||
ApiCms.getNews(data.page, data.rows),
|
||||
);
|
||||
this.content = newsResponse.data;
|
||||
|
||||
await this.$nextTick();
|
||||
|
||||
this.formatExternalHtml();
|
||||
},
|
||||
formatExternalHtml() {
|
||||
document
|
||||
.querySelectorAll(".news-list-item .card-header")
|
||||
.forEach((el) => {
|
||||
el.classList.add("fhc-primary");
|
||||
});
|
||||
document.querySelectorAll(".news-list-item .row").forEach((el) => {
|
||||
el.classList.add("w-100");
|
||||
el.classList.add("align-items-center");
|
||||
});
|
||||
document
|
||||
.querySelectorAll(".news-list-item .row h2")
|
||||
.forEach((el) => {
|
||||
el.classList.add("mb-0");
|
||||
});
|
||||
},
|
||||
loadNewPageContent(data) {
|
||||
this.$api
|
||||
.call(ApiCms.getNews(data.page, data.rows))
|
||||
.then(res => res.data)
|
||||
.then(result => {
|
||||
this.content = result;
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchNews();
|
||||
afterPageUpdated(event) {
|
||||
this.page = event.page;
|
||||
this.page_size = event.rows;
|
||||
this.$refs.newsPageHeading.scrollIntoView({block: 'end'});
|
||||
this.loadNewPageContent(event);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchNews();
|
||||
|
||||
this.$api
|
||||
.call(ApiCms.getNewsRowCount())
|
||||
.then(res => res.data)
|
||||
.then(result => {
|
||||
.then((res) => res.data)
|
||||
.then((result) => {
|
||||
this.maxPageCount = result;
|
||||
});
|
||||
},
|
||||
template: /*html*/ `
|
||||
<h2 class="fhc-primary-color">News</h2>
|
||||
<hr/>
|
||||
<pagination v-show="content?true:false" :page_size="page_size" @page="page=$event.page; loadNewPageContent($event)" :maxPageCount="maxPageCount">
|
||||
</pagination>
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
<div class="col" v-html="content">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div style="width:15rem">
|
||||
<studiengang-information></studiengang-information>
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
|
||||
<h2 ref="newsPageHeading" class="fhc-primary-color">News</h2>
|
||||
<hr/>
|
||||
<pagination
|
||||
v-if="content?true:false"
|
||||
:page="page"
|
||||
:page_size="page_size"
|
||||
@pageUpdated="afterPageUpdated($event)"
|
||||
:maxPageCount="maxPageCount"
|
||||
></pagination>
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
<div class="col" v-html="content">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div style="width:15rem">
|
||||
<studiengang-information></studiengang-information>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pagination
|
||||
v-if="content?true:false"
|
||||
:page="page"
|
||||
:page_size="page_size"
|
||||
@pageUpdated="afterPageUpdated($event)"
|
||||
:maxPageCount="maxPageCount"
|
||||
></pagination>
|
||||
</div>
|
||||
<pagination v-show="content?true:false" :page_size="page_size" @page="loadNewPageContent" :maxPageCount="maxPageCount">
|
||||
</pagination>
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -29,51 +29,51 @@ components:{
|
||||
},
|
||||
template:/*html*/`
|
||||
<div id="fhc-studiengang-informationen">
|
||||
<template v-if="studiengang?.bezeichnung && semester">
|
||||
<div class="card card-body mb-3 border-0">
|
||||
<div class="mb-1">
|
||||
<h2 class="h4 mb-1 pb-0">{{$p.t('lehre','studiengang')}}:</h2>
|
||||
<span class="mb-1">{{studiengang?.bezeichnung}}</span>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<h2 class="h4 mb-1 pb-0">Moodle:</h2>
|
||||
<a class="fhc-link-color mb-1" target="_blank" :href="moodleLink">{{studiengang?.kurzbzlang}}</a>
|
||||
</div>
|
||||
<div :class="{'mb-1':studiengang?.zusatzinfo_html}">
|
||||
<h2 class="h4 mb-1 pb-0">{{$p.t('lehre','studiensemester')}}: </h2>
|
||||
<span class="mb-1">{{semester}}</span>
|
||||
</div>
|
||||
<div class="zusatzinfo" v-if="studiengang?.zusatzinfo_html" v-html="studiengang?.zusatzinfo_html"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-for="{title, collection} in collection_array">
|
||||
<template v-if="Array.isArray(collection) && collection.length !==0">
|
||||
<h2 class="h5 text-truncate">{{title}}</h2>
|
||||
<template v-if="displayWidget">
|
||||
<div class="d-flex flex-wrap flex-row mb-3 gap-2">
|
||||
<template v-for="person in collection">
|
||||
<studiengang-person displayWidget v-bind="person"></studiengang-person>
|
||||
</template>
|
||||
<template v-if="studiengang?.bezeichnung && semester">
|
||||
<div class="card card-body mb-3 border-0">
|
||||
<div class="mb-1">
|
||||
<h2 class="h4 mb-1 pb-0">{{$p.t('lehre','studiengang')}}:</h2>
|
||||
<span class="mb-1">{{studiengang?.bezeichnung}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="person in collection">
|
||||
<div class="mb-3">
|
||||
<studiengang-person v-bind="person"></studiengang-person>
|
||||
<div class="mb-1">
|
||||
<h2 class="h4 mb-1 pb-0">Moodle:</h2>
|
||||
<a class="fhc-link-color mb-1" target="_blank" :href="moodleLink">{{studiengang?.kurzbzlang}}</a>
|
||||
</div>
|
||||
<div :class="{'mb-1':studiengang?.zusatzinfo_html}">
|
||||
<h2 class="h4 mb-1 pb-0">{{$p.t('lehre','studiensemester')}}: </h2>
|
||||
<span class="mb-1">{{semester}}</span>
|
||||
</div>
|
||||
<div class="zusatzinfo" v-if="studiengang?.zusatzinfo_html" v-html="studiengang?.zusatzinfo_html"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-for="{title, collection} in collection_array">
|
||||
<template v-if="Array.isArray(collection) && collection.length !==0">
|
||||
<h2 class="h5 text-truncate">{{title}}</h2>
|
||||
<template v-if="displayWidget">
|
||||
<div class="d-flex flex-wrap flex-row mb-3 gap-2">
|
||||
<template v-for="person in collection">
|
||||
<studiengang-person displayWidget v-bind="person"></studiengang-person>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="person in collection">
|
||||
<div class="mb-3">
|
||||
<studiengang-person v-bind="person"></studiengang-person>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="hochschulvertr && Array.isArray(hochschulvertr) && hochschulvertr.length >0">
|
||||
<studiengang-vertretung showBezeichnung :title="$p.t('studiengangInformation', 'Hochschulvertretung')" :vertretungsList="hochschulvertr"></studiengang-vertretung>
|
||||
</template>
|
||||
<template v-if="stdv && Array.isArray(stdv) && stdv.length >0">
|
||||
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Studienvertretung').concat(studiengang.kurzbzlang??'')" :vertretungsList="stdv"></studiengang-vertretung>
|
||||
</template>
|
||||
<template v-if="jahrgangsvertr && Array.isArray(jahrgangsvertr) && jahrgangsvertr.length >0">
|
||||
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Jahrgangsvertretung')" :vertretungsList="jahrgangsvertr"></studiengang-vertretung>
|
||||
</template>
|
||||
<template v-if="hochschulvertr && Array.isArray(hochschulvertr) && hochschulvertr.length >0">
|
||||
<studiengang-vertretung showBezeichnung :title="$p.t('studiengangInformation', 'Hochschulvertretung')" :vertretungsList="hochschulvertr"></studiengang-vertretung>
|
||||
</template>
|
||||
<template v-if="stdv && Array.isArray(stdv) && stdv.length >0">
|
||||
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Studienvertretung').concat(studiengang?.kurzbzlang??'')" :vertretungsList="stdv"></studiengang-vertretung>
|
||||
</template>
|
||||
<template v-if="jahrgangsvertr && Array.isArray(jahrgangsvertr) && jahrgangsvertr.length >0">
|
||||
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Jahrgangsvertretung')" :vertretungsList="jahrgangsvertr"></studiengang-vertretung>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
`,
|
||||
|
||||
@@ -27,7 +27,7 @@ export default {
|
||||
|
||||
<dl class="stgkontaktinfo">
|
||||
<dt><i class="fa fa-phone me-2"></i></dt>
|
||||
<dd class="mb-3"><a class="fhc-link-color" :href="phone.link">{{phone.number}}</a></dd>
|
||||
<dd class="mb-3"><a class="fhc-link-color" :href="phone?.link">{{phone?.number}}</a></dd>
|
||||
|
||||
<dt><i class="fa fa-home me-2"></i></dt>
|
||||
<dd class="mb-3">{{ort}}</dd>
|
||||
|
||||
@@ -3,7 +3,8 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
|
||||
import ApiLvPlan from '../../../api/factory/lvPlan.js';
|
||||
import ApiAuthinfo from '../../../api/factory/authinfo.js';
|
||||
|
||||
export const DEFAULT_MODE_LVPLAN = 'Week'
|
||||
export const DEFAULT_MODE_LVPLAN_MOBILE = 'List';
|
||||
export const DEFAULT_MODE_LVPLAN_DESKTOP = 'Week';
|
||||
|
||||
export default {
|
||||
name: 'LvPlanLehrveranstaltung',
|
||||
@@ -19,15 +20,21 @@ export default {
|
||||
lv: null
|
||||
};
|
||||
},
|
||||
inject: ["isMobile"],
|
||||
computed:{
|
||||
currentDay() {
|
||||
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
|
||||
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
|
||||
return luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
|
||||
return this.propsViewData?.focus_date;
|
||||
},
|
||||
currentMode() {
|
||||
if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase()))
|
||||
return DEFAULT_MODE_LVPLAN;
|
||||
let validModes = ['day', 'month'];
|
||||
validModes.push(this.isMobile ? 'list' : 'week');
|
||||
|
||||
const defaultMode = this.isMobile ? DEFAULT_MODE_LVPLAN_MOBILE : DEFAULT_MODE_LVPLAN_DESKTOP;
|
||||
|
||||
if (!this.propsViewData?.mode || !validModes.includes(this.propsViewData?.mode.toLowerCase()))
|
||||
return defaultMode;
|
||||
return this.propsViewData?.mode;
|
||||
},
|
||||
currentLv() {
|
||||
@@ -47,6 +54,17 @@ export default {
|
||||
return this.lv.bezeichnung;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async isMobile() {
|
||||
await this.$nextTick();
|
||||
this.handleChangeMode(
|
||||
this.currentMode,
|
||||
luxon.DateTime.fromISO(this.currentDay, {
|
||||
zone: this.timezone,
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleChangeDate(day, newMode) {
|
||||
return this.handleChangeMode(newMode, day);
|
||||
@@ -95,7 +113,6 @@ export default {
|
||||
<fhc-calendar
|
||||
v-else-if="lv"
|
||||
ref="calendar"
|
||||
:timezone="viewData.timezone"
|
||||
:get-promise-func="getPromiseFunc"
|
||||
:date="currentDay"
|
||||
:mode="currentMode"
|
||||
|
||||
@@ -3,7 +3,8 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
|
||||
import ApiLvPlan from '../../../api/factory/lvPlan.js';
|
||||
import ApiAuthinfo from '../../../api/factory/authinfo.js';
|
||||
|
||||
export const DEFAULT_MODE_LVPLAN = 'Week'
|
||||
export const DEFAULT_MODE_LVPLAN_MOBILE = 'List';
|
||||
export const DEFAULT_MODE_LVPLAN_DESKTOP = 'Week';
|
||||
|
||||
export default {
|
||||
name: 'LvPlan',
|
||||
@@ -11,31 +12,32 @@ export default {
|
||||
FhcCalendar
|
||||
},
|
||||
props: {
|
||||
viewData: Object, // NOTE(chris): this is inherited from router-view
|
||||
propsViewData: Object
|
||||
},
|
||||
data() {
|
||||
const now = luxon.DateTime.now().setZone(this.viewData.timezone);
|
||||
return {
|
||||
studiensemester_kurzbz: null,
|
||||
studiensemester_start: null,
|
||||
studiensemester_ende: null,
|
||||
uid: null,
|
||||
lv: null
|
||||
lv: null,
|
||||
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
|
||||
};
|
||||
},
|
||||
inject: ["isMobile"],
|
||||
computed:{
|
||||
currentDay() {
|
||||
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
|
||||
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.timezone).toISODate();
|
||||
},
|
||||
currentMode() {
|
||||
return this.propsViewData?.mode || DEFAULT_MODE_LVPLAN;
|
||||
const defaultMode = this.isMobile ? DEFAULT_MODE_LVPLAN_MOBILE : DEFAULT_MODE_LVPLAN_DESKTOP;
|
||||
return this.propsViewData?.mode || defaultMode;
|
||||
},
|
||||
downloadLinks() {
|
||||
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
|
||||
return false;
|
||||
|
||||
const opts = { zone: this.viewData.timezone };
|
||||
const opts = { zone: this.timezone };
|
||||
const start = luxon.DateTime
|
||||
.fromISO(this.studiensemester_start, opts)
|
||||
.toUnixInteger();
|
||||
@@ -115,7 +117,7 @@ export default {
|
||||
<fhc-calendar
|
||||
ref="calendar"
|
||||
v-model:lv="lv"
|
||||
:timezone="viewData.timezone"
|
||||
:timezone="timezone"
|
||||
:get-promise-func="getPromiseFunc"
|
||||
:date="currentDay"
|
||||
:mode="currentMode"
|
||||
|
||||
+43
-17
@@ -3,7 +3,8 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
|
||||
import ApiLvPlan from '../../../api/factory/lvPlan.js';
|
||||
import ApiAuthinfo from '../../../api/factory/authinfo.js';
|
||||
|
||||
export const DEFAULT_MODE_LVPLAN = 'Week'
|
||||
export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
|
||||
export const DEFAULT_MODE_LVPLAN_MOBILE = "List";
|
||||
|
||||
export default {
|
||||
name: 'LvPlanPersonal',
|
||||
@@ -11,7 +12,6 @@ export default {
|
||||
FhcCalendar
|
||||
},
|
||||
props: {
|
||||
viewData: Object, // NOTE(chris): this is inherited from router-view
|
||||
propsViewData: Object
|
||||
},
|
||||
data() {
|
||||
@@ -21,18 +21,30 @@ export default {
|
||||
studiensemester_ende: null,
|
||||
uid: null,
|
||||
isMitarbeiter: false,
|
||||
isStudent: false
|
||||
isStudent: false,
|
||||
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
|
||||
};
|
||||
},
|
||||
inject: ["isMobile"],
|
||||
computed:{
|
||||
currentDay() {
|
||||
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
|
||||
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
|
||||
return luxon.DateTime.now().setZone(this.timezone).toISODate();
|
||||
return this.propsViewData?.focus_date;
|
||||
},
|
||||
currentMode() {
|
||||
if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase()))
|
||||
return DEFAULT_MODE_LVPLAN;
|
||||
let validModes = ["day", "month"];
|
||||
validModes.push(this.isMobile ? "list" : "week");
|
||||
|
||||
const defaultMode = this.isMobile
|
||||
? DEFAULT_MODE_LVPLAN_MOBILE
|
||||
: DEFAULT_MODE_LVPLAN_DESKTOP;
|
||||
|
||||
if (
|
||||
!this.propsViewData?.mode ||
|
||||
!validModes.includes(this.propsViewData?.mode.toLowerCase())
|
||||
)
|
||||
return defaultMode;
|
||||
return this.propsViewData?.mode;
|
||||
},
|
||||
downloadLinks() {
|
||||
@@ -47,7 +59,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const opts = { zone: this.viewData.timezone };
|
||||
const opts = { zone: this.timezone };
|
||||
const start = luxon.DateTime
|
||||
.fromISO(this.studiensemester_start, opts)
|
||||
.toUnixInteger();
|
||||
@@ -70,6 +82,17 @@ export default {
|
||||
];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async isMobile() {
|
||||
await this.$nextTick();
|
||||
this.handleChangeMode(
|
||||
this.currentMode,
|
||||
luxon.DateTime.fromISO(this.currentDay, {
|
||||
zone: this.timezone,
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleChangeDate(day, newMode) {
|
||||
return this.handleChangeMode(newMode, day);
|
||||
@@ -102,16 +125,18 @@ export default {
|
||||
this.$api.call(ApiLvPlan.eventsPersonal(start.toISODate(), end.toISODate())),
|
||||
this.$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
|
||||
];
|
||||
}
|
||||
},
|
||||
async fetchAuthInfo() {
|
||||
const authInfoResponse = await this.$api.call(ApiAuthinfo.getAuthInfo());
|
||||
|
||||
const authInfo = authInfoResponse.data;
|
||||
this.uid = authInfo.uid;
|
||||
this.isMitarbeiter = authInfo.isMitarbeiter;
|
||||
this.isStudent = authInfo.isStudent;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$api
|
||||
.call(ApiAuthinfo.getAuthInfo())
|
||||
.then(res => {
|
||||
this.uid = res.data.uid;
|
||||
this.isMitarbeiter = res.data.isMitarbeiter;
|
||||
this.isStudent = res.data.isStudent;
|
||||
});
|
||||
async created() {
|
||||
await this.fetchAuthInfo();
|
||||
},
|
||||
template: /*html*/`
|
||||
<div class="cis-lvplan-personal d-flex flex-column h-100">
|
||||
@@ -123,8 +148,9 @@ export default {
|
||||
</h2>
|
||||
<hr>
|
||||
<fhc-calendar
|
||||
v-if="timezone"
|
||||
ref="calendar"
|
||||
:timezone="viewData.timezone"
|
||||
:timezone="timezone"
|
||||
:get-promise-func="getPromiseFunc"
|
||||
:date="currentDay"
|
||||
:mode="currentMode"
|
||||
@@ -0,0 +1,300 @@
|
||||
import FormForm from "../../Form/Form.js";
|
||||
import FormInput from "../../Form/Input.js";
|
||||
import FhcCalendar from "../../Calendar/LvPlan.js";
|
||||
|
||||
import ApiLvPlan from "../.././../api/factory/lvPlan.js";
|
||||
import ApiOtherLvPlan from "../.././../api/factory/otherLvPlan.js";
|
||||
import ApiAuthinfo from "../../../api/factory/authinfo.js";
|
||||
|
||||
export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
|
||||
export const DEFAULT_MODE_LVPLAN_MOBILE = "List";
|
||||
|
||||
export default {
|
||||
name: "OtherLvPlan",
|
||||
components: {
|
||||
FormForm,
|
||||
FormInput,
|
||||
FhcCalendar,
|
||||
},
|
||||
props: {
|
||||
propsViewData: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localProps: {},
|
||||
studiensemester_kurzbz: null,
|
||||
studiensemester_start: null,
|
||||
studiensemester_ende: null,
|
||||
isOtherPersonMitarbeiter: false,
|
||||
isOtherPersonStudent: false,
|
||||
currentStgBezeichnung: null,
|
||||
listVerband: [],
|
||||
listGroup: [],
|
||||
rangeIntervalFirst: null,
|
||||
otherPersonData: {
|
||||
fullNameWithTitle: "",
|
||||
photo: "",
|
||||
},
|
||||
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
|
||||
};
|
||||
},
|
||||
inject: ["isMobile"],
|
||||
computed: {
|
||||
currentDay() {
|
||||
if (
|
||||
!this.propsViewData?.focus_date ||
|
||||
isNaN(new Date(this.propsViewData?.focus_date))
|
||||
)
|
||||
return luxon.DateTime.now().setZone(this.timezone).toISODate();
|
||||
return this.propsViewData?.focus_date;
|
||||
},
|
||||
currentMode() {
|
||||
let validModes = ["day", "month"];
|
||||
validModes.push(this.isMobile ? "list" : "week");
|
||||
|
||||
const defaultMode = this.isMobile
|
||||
? DEFAULT_MODE_LVPLAN_MOBILE
|
||||
: DEFAULT_MODE_LVPLAN_DESKTOP;
|
||||
|
||||
if (
|
||||
!this.propsViewData?.mode ||
|
||||
!validModes.includes(this.propsViewData?.mode.toLowerCase())
|
||||
)
|
||||
return defaultMode;
|
||||
return this.propsViewData?.mode;
|
||||
},
|
||||
downloadLinks() {
|
||||
// do not show download links in otherLvPlan Mode
|
||||
return false;
|
||||
|
||||
if (
|
||||
!this.studiensemester_start ||
|
||||
!this.studiensemester_ende ||
|
||||
!this.propsViewData.otherUid
|
||||
)
|
||||
return false;
|
||||
|
||||
const type = this.isOtherPersonStudent
|
||||
? "student"
|
||||
: this.isOtherPersonMitarbeiter
|
||||
? "lektor"
|
||||
: null;
|
||||
|
||||
if (!type) return;
|
||||
|
||||
const opts = { zone: this.timezone };
|
||||
const start = luxon.DateTime.fromISO(
|
||||
this.studiensemester_start,
|
||||
opts,
|
||||
).toUnixInteger();
|
||||
const ende = luxon.DateTime.fromISO(
|
||||
this.studiensemester_ende,
|
||||
opts,
|
||||
).toUnixInteger();
|
||||
|
||||
const download_link =
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root +
|
||||
"cis/private/lvplan/stpl_kalender.php" +
|
||||
"?type=" +
|
||||
type +
|
||||
"&pers_uid=" +
|
||||
this.propsViewData.otherUid +
|
||||
"&begin=" +
|
||||
start +
|
||||
"&ende=" +
|
||||
ende;
|
||||
|
||||
return [
|
||||
{
|
||||
title: "excel",
|
||||
icon: "fa-solid fa-file-excel",
|
||||
link: download_link + "&format=excel",
|
||||
},
|
||||
{
|
||||
title: "csv",
|
||||
icon: "fa-solid fa-file-csv",
|
||||
link: download_link + "&format=csv",
|
||||
},
|
||||
{
|
||||
title: "ical1",
|
||||
icon: "fa-regular fa-calendar",
|
||||
link: download_link + "&format=ical&version=1&target=ical",
|
||||
},
|
||||
{
|
||||
title: "ical2",
|
||||
icon: "fa-regular fa-calendar",
|
||||
link: download_link + "&format=ical&version=2&target=ical",
|
||||
},
|
||||
];
|
||||
},
|
||||
get_image_base64_src: function () {
|
||||
if (!this.otherPersonData.photo?.length) {
|
||||
return "";
|
||||
}
|
||||
return "data:image/jpeg;base64," + this.otherPersonData.photo;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"propsViewData.otherUid": {
|
||||
handler() {
|
||||
this.$router.go();
|
||||
},
|
||||
},
|
||||
async isMobile() {
|
||||
await this.$nextTick();
|
||||
this.handleChangeMode(
|
||||
this.currentMode,
|
||||
luxon.DateTime.fromISO(this.currentDay, {
|
||||
zone: this.timezone,
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleChangeDate(day, newMode) {
|
||||
return this.handleChangeMode(newMode, day);
|
||||
},
|
||||
handleChangeMode(newMode, day) {
|
||||
const mode = newMode[0].toUpperCase() + newMode.slice(1);
|
||||
const focus_date = day.toISODate();
|
||||
|
||||
this.$router.push({
|
||||
name: "OtherLvPlan",
|
||||
params: {
|
||||
mode,
|
||||
focus_date,
|
||||
},
|
||||
});
|
||||
},
|
||||
updateRange(rangeInterval) {
|
||||
this.$api
|
||||
.call(
|
||||
ApiLvPlan.studiensemesterDateInterval(
|
||||
rangeInterval.end.startOf("week").toISODate(),
|
||||
),
|
||||
)
|
||||
.then((res) => {
|
||||
this.studiensemester_kurzbz =
|
||||
res.data.studiensemester_kurzbz;
|
||||
this.studiensemester_start = res.data.start;
|
||||
this.studiensemester_ende = res.data.ende;
|
||||
});
|
||||
},
|
||||
getPromiseFunc(start, end) {
|
||||
return [
|
||||
this.$api.call(
|
||||
ApiLvPlan.eventsPersonal(
|
||||
start.toISODate(),
|
||||
end.toISODate(),
|
||||
this.propsViewData.otherUid,
|
||||
),
|
||||
),
|
||||
this.$api.call(
|
||||
ApiLvPlan.getLvPlanReservierungen(
|
||||
start.toISODate(),
|
||||
end.toISODate(),
|
||||
this.propsViewData.otherUid,
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
async fetchViewData() {
|
||||
const viewDataResponse = await this.$api.call(
|
||||
ApiOtherLvPlan.getOtherLvPlanViewData(
|
||||
this.propsViewData.otherUid,
|
||||
),
|
||||
);
|
||||
|
||||
const viewData = viewDataResponse.data;
|
||||
|
||||
this.isOtherPersonMitarbeiter =
|
||||
!!viewData?.user_data?.is_mitarbeiter;
|
||||
this.isOtherPersonStudent = !!viewData?.user_data?.is_student;
|
||||
let fullNameWithTitleFragments = [
|
||||
viewData?.user_data?.titel,
|
||||
viewData?.user_data?.vorname,
|
||||
viewData?.user_data?.nachname,
|
||||
viewData?.user_data?.postnomen,
|
||||
];
|
||||
fullNameWithTitleFragments = fullNameWithTitleFragments.filter(
|
||||
(fragment) => !!fragment,
|
||||
);
|
||||
this.otherPersonData.fullNameWithTitle =
|
||||
fullNameWithTitleFragments.join(" ");
|
||||
this.otherPersonData.photo = viewData?.user_data?.foto;
|
||||
},
|
||||
async redirectToMyLvPlanIfAuthUid() {
|
||||
const authInfoResponse = await this.$api.call(
|
||||
ApiAuthinfo.getAuthInfo(),
|
||||
);
|
||||
const authId = authInfoResponse.data.uid;
|
||||
if (authId === this.propsViewData.otherUid) {
|
||||
this.$router.push({ name: "MyLvPlan" });
|
||||
}
|
||||
},
|
||||
copyFullNameWithTitleToClipboard() {
|
||||
navigator.clipboard.writeText(
|
||||
this.otherPersonData.fullNameWithTitle,
|
||||
);
|
||||
this.$fhcAlert.alertSuccess(
|
||||
this.$p.t("profil/name_title_clipboard_copy_confirmation"),
|
||||
);
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.redirectToMyLvPlanIfAuthUid();
|
||||
await this.fetchViewData();
|
||||
await this.$p.loadCategory(["profil"]);
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="d-flex flex-column h-100">
|
||||
<h2>
|
||||
<div class="d-flex flex-row justify-content-between align-items-center">
|
||||
<span>
|
||||
{{ $p.t('lehre/stundenplan') + (studiensemester_kurzbz ? " " + studiensemester_kurzbz : "") }}
|
||||
</span>
|
||||
<div @click="this.$router.push({name: 'ProfilView', params: {uid: propsViewData.otherUid}})" type="button" class="d-flex flex-row align-items-center gap-3">
|
||||
<span v-if="otherPersonData.fullNameWithTitle?.length">
|
||||
{{ otherPersonData.fullNameWithTitle }}
|
||||
<span @click.stop="copyFullNameWithTitleToClipboard()" :title="$p.t('LvPlan/copy')" class="fs-4 h-100">
|
||||
<i class="fa-regular fa-copy"></i>
|
||||
</span>
|
||||
</span>
|
||||
<img v-if="otherPersonData.photo?.length" alt="profile picture" class=" img-thumbnail " style=" max-height:60px; " :src="get_image_base64_src"/>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<hr>
|
||||
<fhc-calendar
|
||||
v-if="timezone"
|
||||
ref="calendar"
|
||||
:timezone="timezone"
|
||||
:get-promise-func="getPromiseFunc"
|
||||
:date="currentDay"
|
||||
:mode="currentMode"
|
||||
@update:date="handleChangeDate"
|
||||
@update:mode="handleChangeMode"
|
||||
@update:range="updateRange"
|
||||
class="responsive-calendar"
|
||||
>
|
||||
<div
|
||||
v-if="downloadLinks"
|
||||
class="d-flex gap-1 justify-items-start"
|
||||
>
|
||||
<div v-for="{ title, icon, link } in downloadLinks">
|
||||
<a
|
||||
:href="link"
|
||||
:aria-label="title"
|
||||
class="py-1 btn btn-outline-secondary"
|
||||
>
|
||||
<div class="d-flex flex-column">
|
||||
<i aria-hidden="true" :class="icon"></i>
|
||||
<span style="font-size:.5rem">{{ title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</fhc-calendar>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,431 @@
|
||||
import FormForm from "../../Form/Form.js";
|
||||
import FormInput from "../../Form/Input.js";
|
||||
import FhcCalendar from "../../Calendar/LvPlan.js";
|
||||
|
||||
import ApiLvPlan from '../.././../api/factory/lvPlan.js';
|
||||
import ApiStgOrgLvPlan from '../.././../api/factory/stgOrgLvPlan.js';
|
||||
import ApiAuthinfo from '../../../api/factory/authinfo.js';
|
||||
|
||||
export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
|
||||
export const DEFAULT_MODE_LVPLAN_MOBILE = "List";
|
||||
|
||||
export default {
|
||||
name: "LvPlanStgOrg",
|
||||
components: {
|
||||
FormForm,
|
||||
FormInput,
|
||||
FhcCalendar,
|
||||
},
|
||||
props: {
|
||||
propsViewData: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localProps: {},
|
||||
studiensemester_kurzbz: null,
|
||||
studiensemester_start: null,
|
||||
studiensemester_ende: null,
|
||||
uid: null,
|
||||
isMitarbeiter: false,
|
||||
isStudent: false,
|
||||
currentStgBezeichnung: null,
|
||||
formData: {
|
||||
stgkz: null,
|
||||
sem: null,
|
||||
verband: null,
|
||||
gruppe: null,
|
||||
},
|
||||
listStg: [],
|
||||
listSem: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
listVerband: [],
|
||||
listGroup: [],
|
||||
rangeIntervalFirst: null,
|
||||
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
|
||||
};
|
||||
},
|
||||
inject: ["isMobile"],
|
||||
computed: {
|
||||
maxSemester() {
|
||||
const currentStg = this.listStg.find(
|
||||
(item) => item.studiengang_kz === this.formData.stgkz,
|
||||
);
|
||||
return currentStg?.max_semester;
|
||||
},
|
||||
currentDay() {
|
||||
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
|
||||
return luxon.DateTime.now().setZone(this.timezone).toISODate();
|
||||
return this.propsViewData?.focus_date;
|
||||
},
|
||||
currentMode() {
|
||||
let validModes = ["day", "month"];
|
||||
validModes.push(this.isMobile ? "list" : "week");
|
||||
|
||||
const defaultMode = this.isMobile
|
||||
? DEFAULT_MODE_LVPLAN_MOBILE
|
||||
: DEFAULT_MODE_LVPLAN_DESKTOP;
|
||||
|
||||
if (
|
||||
!this.propsViewData?.mode ||
|
||||
!validModes.includes(this.propsViewData?.mode.toLowerCase())
|
||||
)
|
||||
return defaultMode;
|
||||
return this.propsViewData?.mode;
|
||||
},
|
||||
downloadLinks() {
|
||||
// do not show download links in StgOrg Mode
|
||||
return false;
|
||||
|
||||
if (
|
||||
!this.studiensemester_start ||
|
||||
!this.studiensemester_ende ||
|
||||
!this.uid
|
||||
)
|
||||
return false;
|
||||
|
||||
let type = false;
|
||||
type = this.isStudent ? "student" : type;
|
||||
type = this.isMitarbeiter ? "lektor" : type;
|
||||
if (false === type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const opts = { zone: this.timezone };
|
||||
const start = luxon.DateTime
|
||||
.fromISO(this.studiensemester_start, opts)
|
||||
.toUnixInteger();
|
||||
const ende = luxon.DateTime
|
||||
.fromISO(this.studiensemester_ende, opts)
|
||||
.toUnixInteger();
|
||||
|
||||
const download_link =
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root +
|
||||
"cis/private/lvplan/stpl_kalender.php" +
|
||||
"?type=" +
|
||||
type +
|
||||
"&pers_uid=" +
|
||||
this.uid +
|
||||
"&begin=" +
|
||||
start +
|
||||
"&ende=" +
|
||||
ende;
|
||||
|
||||
return [
|
||||
{
|
||||
title: "excel",
|
||||
icon: "fa-solid fa-file-excel",
|
||||
link: download_link + "&format=excel",
|
||||
},
|
||||
{
|
||||
title: "csv",
|
||||
icon: "fa-solid fa-file-csv",
|
||||
link: download_link + "&format=csv",
|
||||
},
|
||||
{
|
||||
title: "ical1",
|
||||
icon: "fa-regular fa-calendar",
|
||||
link: download_link + "&format=ical&version=1&target=ical",
|
||||
},
|
||||
{
|
||||
title: "ical2",
|
||||
icon: "fa-regular fa-calendar",
|
||||
link: download_link + "&format=ical&version=2&target=ical",
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async isMobile() {
|
||||
await this.$nextTick();
|
||||
this.handleChangeMode(
|
||||
this.currentMode,
|
||||
luxon.DateTime.fromISO(this.currentDay, {
|
||||
zone: this.timezone,
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadLvPlan() {
|
||||
if (!this.formData.stgkz) {
|
||||
this.$fhcAlert.alertError(this.$p.t("LvPlan", "chooseStg"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.formData.sem &&
|
||||
(this.formData.verband || this.formData.gruppe)
|
||||
) {
|
||||
this.$fhcAlert.alertError(
|
||||
this.$p.t("LvPlan", "error_SemMissing"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.formData.verband && this.formData.gruppe) {
|
||||
this.$fhcAlert.alertError(
|
||||
this.$p.t("LvPlan", "error_VerbandMissing"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
mode: this.currentMode,
|
||||
focus_date: this.currentDay,
|
||||
stgkz: this.formData.stgkz,
|
||||
sem: this.formData.sem,
|
||||
verband: this.formData.verband,
|
||||
gruppe: this.formData.gruppe,
|
||||
};
|
||||
|
||||
//ensure logic: no value after a null value in route
|
||||
if (params.sem == null) {
|
||||
params.verband = null;
|
||||
params.gruppe = null;
|
||||
}
|
||||
if (params.verband == null) {
|
||||
params.gruppe = null;
|
||||
}
|
||||
|
||||
//delete all null values to avoid null in router
|
||||
Object.keys(params).forEach(
|
||||
(key) => params[key] == null && delete params[key],
|
||||
);
|
||||
|
||||
this.$router.push({
|
||||
name: "StgOrgLvPlan",
|
||||
params,
|
||||
});
|
||||
|
||||
this.$refs['calendar']?.resetEventLoader();
|
||||
},
|
||||
loadListSem(){
|
||||
if(!this.listSem)
|
||||
this.listSem = [...Array(this.maxSemester).keys()].map(i => i + 1);
|
||||
},
|
||||
loadListVerband() {
|
||||
this.$api
|
||||
.call(ApiLvPlan.getLehrverband(this.formData.stgkz, this.formData.sem, this.formData.verband))
|
||||
.then(result => {
|
||||
const data = result.data;
|
||||
const mappedData = data.map((item) => item.verband);
|
||||
this.listVerband = [
|
||||
...new Set(
|
||||
mappedData.filter(
|
||||
(v) =>
|
||||
v !== null &&
|
||||
v !== undefined &&
|
||||
String(v).trim() !== "",
|
||||
),
|
||||
),
|
||||
].sort();
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
loadListGroup() {
|
||||
this.$api
|
||||
.call(ApiLvPlan.getGruppe(this.formData.stgkz, this.formData.sem, this.formData.verband))
|
||||
.then(result => {
|
||||
const data = result.data;
|
||||
const mappedData = data.map((item) => item.gruppe);
|
||||
this.listGroup = [
|
||||
...new Set(
|
||||
mappedData.filter(
|
||||
(v) =>
|
||||
v !== null &&
|
||||
v !== undefined &&
|
||||
String(v).trim() !== "",
|
||||
),
|
||||
),
|
||||
].sort();
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
handleChangeDate(day, newMode) {
|
||||
return this.handleChangeMode(newMode, day);
|
||||
},
|
||||
handleChangeMode(newMode, day) {
|
||||
const mode = newMode[0].toUpperCase() + newMode.slice(1);
|
||||
const focus_date = day.toISODate();
|
||||
|
||||
this.$router.push({
|
||||
name: "StgOrgLvPlan",
|
||||
params: {
|
||||
mode,
|
||||
focus_date,
|
||||
stgkz: this.formData.stgkz,
|
||||
sem: this.formData.sem,
|
||||
verband: this.formData.verband,
|
||||
gruppe: this.formData.gruppe,
|
||||
},
|
||||
});
|
||||
},
|
||||
updateRange(rangeInterval) {
|
||||
this.$api
|
||||
.call(
|
||||
ApiLvPlan.studiensemesterDateInterval(
|
||||
rangeInterval.end.startOf("week").toISODate(),
|
||||
),
|
||||
)
|
||||
.then((res) => {
|
||||
this.studiensemester_kurzbz =
|
||||
res.data.studiensemester_kurzbz;
|
||||
this.studiensemester_start = res.data.start;
|
||||
this.studiensemester_ende = res.data.ende;
|
||||
});
|
||||
},
|
||||
getPromiseFunc(start, end) {
|
||||
return [
|
||||
this.$api.call(
|
||||
ApiLvPlan.eventsStgOrg(
|
||||
start,
|
||||
end,
|
||||
this.formData.stgkz,
|
||||
this.formData.sem,
|
||||
this.formData.verband,
|
||||
this.formData.gruppe,
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
async fetchAuthInfo() {
|
||||
const authInfoResponse = await this.$api.call(ApiAuthinfo.getAuthInfo());
|
||||
|
||||
const authInfo = authInfoResponse.data;
|
||||
this.uid = authInfo.uid;
|
||||
this.isMitarbeiter = authInfo.isMitarbeiter;
|
||||
this.isStudent = authInfo.isStudent;
|
||||
},
|
||||
async fetchViewData() {
|
||||
const viewDataResponse = await this.$api.call(ApiStgOrgLvPlan.getStgOrgLvPlanViewData());
|
||||
|
||||
const viewData = viewDataResponse.data;
|
||||
this.listStg = viewData.studiengaenge;
|
||||
},
|
||||
},
|
||||
async created(){
|
||||
await this.fetchAuthInfo();
|
||||
await this.fetchViewData();
|
||||
|
||||
if (this.propsViewData) {
|
||||
this.formData.stgkz = this.propsViewData.stgkz ? this.propsViewData.stgkz: null;
|
||||
this.formData.sem = this.propsViewData.sem ? this.propsViewData.sem: null;
|
||||
this.formData.verband = this.propsViewData.verband ? this.propsViewData.verband: null;
|
||||
this.formData.gruppe = this.propsViewData.gruppe ? this.propsViewData.gruppe: null;
|
||||
|
||||
//ensure loading dropdown arrays for version propsView
|
||||
if(!this.listVerband.length && this.formData.sem)
|
||||
{
|
||||
this.loadListVerband();
|
||||
}
|
||||
if(!this.listGroup.length && this.formData.verband)
|
||||
{
|
||||
this.loadListGroup();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.formData.stgkz) {
|
||||
this.loadLvPlan();
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="cis-lvplan-stg-org d-flex flex-column h-100">
|
||||
|
||||
<div class="mt-3">
|
||||
<form-form class="row row-cols-1 row-cols-md-2 row-cols-lg-4 row-cols-xl-5 g-3 mb-3">
|
||||
<form-input
|
||||
type="select"
|
||||
v-model="formData.stgkz"
|
||||
@change="loadListSem(formData.stgkz)"
|
||||
>
|
||||
<option :value="null" selected>{{ $p.t('LvPlan/chooseStg') }}</option>
|
||||
<option
|
||||
v-for="stg in listStg"
|
||||
:key="stg.studiengang_kz"
|
||||
:value="stg.studiengang_kz"
|
||||
>
|
||||
{{ stg.kurzbzlang }} ({{ stg.bezeichnung }})
|
||||
</option>
|
||||
</form-input>
|
||||
<form-input
|
||||
type="select"
|
||||
v-model="formData.sem"
|
||||
@change="loadListVerband()"
|
||||
@click="loadListSem(formData.stgkz)"
|
||||
>
|
||||
<option :value="null" selected>Semester</option>
|
||||
<option
|
||||
v-for="sem in listSem"
|
||||
:key="sem"
|
||||
:value="sem"
|
||||
>
|
||||
{{ sem }}
|
||||
</option>
|
||||
</form-input>
|
||||
<form-input
|
||||
type="select"
|
||||
v-model="formData.verband"
|
||||
@change="loadListGroup()"
|
||||
>
|
||||
<option :value="null" selected>{{ $p.t('lehre/verband') }} </option>
|
||||
<option
|
||||
v-for="verband in listVerband"
|
||||
:key="verband"
|
||||
:value="verband"
|
||||
>
|
||||
{{ verband }}
|
||||
</option>
|
||||
</form-input>
|
||||
<form-input
|
||||
type="select"
|
||||
v-model="formData.gruppe"
|
||||
>
|
||||
<option :value="null" selected>{{ $p.t('gruppenmanagement/gruppe') }}</option>
|
||||
<option
|
||||
v-for="group in listGroup"
|
||||
:key="group"
|
||||
:value="group"
|
||||
>
|
||||
{{ group }}
|
||||
</option>
|
||||
</form-input>
|
||||
<button type="button" class="btn btn-secondary" @click="loadLvPlan">{{ $p.t('LvPlan/loadLvPlan') }}</button>
|
||||
|
||||
</form-form>
|
||||
</div>
|
||||
|
||||
<template v-if="timezone">
|
||||
<fhc-calendar
|
||||
v-if="propsViewData && propsViewData.stgkz"
|
||||
ref="calendar"
|
||||
v-model:lv="formData"
|
||||
:timezone="timezone"
|
||||
:get-promise-func="getPromiseFunc"
|
||||
:date="currentDay"
|
||||
:mode="currentMode"
|
||||
@update:date="handleChangeDate"
|
||||
@update:mode="handleChangeMode"
|
||||
@update:range="updateRange"
|
||||
class="responsive-calendar"
|
||||
>
|
||||
<div
|
||||
v-if="downloadLinks"
|
||||
class="d-flex gap-1 justify-items-start"
|
||||
>
|
||||
<div v-for="{ title, icon, link } in downloadLinks">
|
||||
<a
|
||||
:href="link"
|
||||
:aria-label="title"
|
||||
class="py-1 btn btn-outline-secondary"
|
||||
>
|
||||
<div class="d-flex flex-column">
|
||||
<i aria-hidden="true" :class="icon"></i>
|
||||
<span style="font-size:.5rem">{{ title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</fhc-calendar>
|
||||
</template>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -30,6 +30,7 @@ export default {
|
||||
menuOpen:true,
|
||||
};
|
||||
},
|
||||
inject: ["isNarrow", "isMobile"],
|
||||
provide(){
|
||||
return{
|
||||
setActiveEntry: this.setActiveEntry,
|
||||
@@ -58,7 +59,7 @@ export default {
|
||||
},
|
||||
site_url(){
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchMenu() {
|
||||
@@ -112,45 +113,101 @@ export default {
|
||||
});
|
||||
},
|
||||
template: /*html*/`
|
||||
<button id="nav-main-btn" class="navbar-toggler rounded-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#nav-main" aria-controls="nav-main" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<fhc-searchbar ref="searchbar" id="nav-search" class="fhc-searchbar w-100 py-1 py-lg-2" :searchoptions="searchbaroptions" :searchfunction="searchfunction"></fhc-searchbar>
|
||||
<div id="nav-logo" class="d-none d-lg-block">
|
||||
<div class="d-flex h-100 justify-content-between">
|
||||
<a :href="rootUrl">
|
||||
<img :src="logoUrl" alt="Corporate Identity Logo">
|
||||
</a>
|
||||
<theme-switch></theme-switch>
|
||||
<div id="cis-header-bar" class="d-flex flex-row flex-grow-1">
|
||||
<div id="nav-logo" class="d-none d-lg-block">
|
||||
<div class="d-flex h-100 justify-content-between">
|
||||
<a :href="rootUrl">
|
||||
<img :src="logoUrl" alt="Corporate Identity Logo">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="nav-user">
|
||||
<button id="nav-user-btn" class="btn btn-link rounded-0" type="button" data-bs-toggle="collapse" data-bs-target="#nav-user-menu" aria-expanded="false" aria-controls="nav-user-menu">
|
||||
<img :src="avatarUrl" :alt="$p.t('profilUpdate/profilBild')" class="bg-dark avatar rounded-circle border border-dark"/>
|
||||
</button>
|
||||
<ul ref="navUserDropdown"
|
||||
@[\`shown.bs.collapse\`]="handleShowNavUser"
|
||||
@[\`hide.bs.collapse\`]="handleHideNavUser"
|
||||
id="nav-user-menu" class="top-100 end-0 collapse list-unstyled" aria-labelledby="nav-user-btn">
|
||||
<li><a class="fhc-dark-bg btn rounded-0 d-block" :href="site_url + '/Cis/Profil'" id="menu-profil">Profil</a></li>
|
||||
<li >
|
||||
<cis-sprachen @languageChanged="fetchMenu"></cis-sprachen>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider m-0 "></li>
|
||||
<li ><a class="fhc-dark-bg btn rounded-0 d-block" :href="logoutUrl">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<nav id="nav-main" class="offcanvas offcanvas-start" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false">
|
||||
<div id="nav-main-sticky">
|
||||
<div id="nav-main-toggle" class="position-static d-none d-lg-block ">
|
||||
<button :aria-label="menuCollapseAriaLabel" type="button" @click="menuOpen = !menuOpen" class="btn text-light rounded-0 p-1 d-flex align-items-center" data-bs-toggle="collapse" data-bs-target=".nav-menu-collapse" aria-expanded="true" aria-controls="nav-sprachen nav-main-menu">
|
||||
<i aria-hidden="true" class="fa fa-arrow-circle-left fhc-text"></i>
|
||||
|
||||
<div
|
||||
v-if="isNarrow"
|
||||
:class="{'collapse multi-collapse collapse-horizontal show': isMobile}"
|
||||
id="navbar-toggler-collapsible"
|
||||
>
|
||||
<div class="d-flex flex-row align-items-center h-100" style="width: 35px">
|
||||
<button id="nav-main-btn" class="navbar-toggler rounded-0 px-2 border-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#nav-main" aria-controls="nav-main" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="offcanvas-body p-0">
|
||||
<div id="nav-main-menu" class="nav-menu-collapse collapse collapse-horizontal show">
|
||||
<div>
|
||||
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeEntry" v-for="entry in entries" :key="entry.content_id" :entry="entry" />
|
||||
</div>
|
||||
|
||||
<fhc-searchbar
|
||||
:searchoptions="searchbaroptions"
|
||||
:searchfunction="searchfunction"
|
||||
ref="searchbar"
|
||||
id="nav-search"
|
||||
class="fhc-searchbar flex-grow-1 py-1 py-lg-2"
|
||||
>
|
||||
<template #collapseToggler="{ isSearchShownInMobileView }">
|
||||
<span
|
||||
v-if="isMobile"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target=".multi-collapse"
|
||||
aria-controls="searchbar-collapsible navbar-toggler-collapsible options-collapsible"
|
||||
aria-expanded="false"
|
||||
class="d-flex flex-row align-items-center pe-1"
|
||||
style="color: white"
|
||||
>
|
||||
<i v-if="isSearchShownInMobileView" class="fa-solid fa-chevron-left ps-3"></i>
|
||||
<i v-else class="fa-solid fa-magnifying-glass ps-2"></i>
|
||||
</span>
|
||||
</template>
|
||||
</fhc-searchbar>
|
||||
|
||||
<div
|
||||
id="options-collapsible"
|
||||
:class="{'collapse multi-collapse collapse-horizontal show': isMobile}"
|
||||
>
|
||||
<div :style="!isMobile ? '' : 'width: 105px'" class="d-flex flex-row ps-3 justify-content-end">
|
||||
<span class="d-flex flex-row align-items-center">
|
||||
<theme-switch></theme-switch>
|
||||
</span>
|
||||
<div id="nav-user">
|
||||
<button id="nav-user-btn" class="btn btn-link rounded-0" type="button" data-bs-toggle="collapse" data-bs-target="#nav-user-menu" aria-expanded="false" aria-controls="nav-user-menu">
|
||||
<img :src="avatarUrl" :alt="$p.t('profilUpdate/profilBild')" class="bg-dark avatar rounded-circle border border-dark"/>
|
||||
</button>
|
||||
<ul ref="navUserDropdown"
|
||||
@[\`shown.bs.collapse\`]="handleShowNavUser"
|
||||
@[\`hide.bs.collapse\`]="handleHideNavUser"
|
||||
id="nav-user-menu" class="top-100 end-0 collapse list-unstyled" aria-labelledby="nav-user-btn">
|
||||
<li><a class="fhc-dark-bg btn rounded-0 d-block" :href="site_url + '/Cis/Profil'" id="menu-profil">Profil</a></li>
|
||||
<li >
|
||||
<cis-sprachen @languageChanged="fetchMenu"></cis-sprachen>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider m-0 "></li>
|
||||
<li ><a class="fhc-dark-bg btn rounded-0 d-block" :href="logoutUrl">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav id="nav-main" class="offcanvas offcanvas-start" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false">
|
||||
<div id="nav-main-sticky">
|
||||
<div class="d-flex flex-row h-100">
|
||||
<div class="offcanvas-body p-0">
|
||||
<div id="nav-main-menu" class="nav-menu-collapse collapse collapse-horizontal show">
|
||||
<div class="flex-grow-1">
|
||||
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeEntry" v-for="entry in entries" :key="entry.content_id" :entry="entry" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="nav-main-toggle" class="d-none d-lg-block">
|
||||
<div
|
||||
@click="menuOpen = !menuOpen"
|
||||
:aria-label="menuCollapseAriaLabel"
|
||||
type="button"
|
||||
class="h-100 d-flex align-items-center px-2"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target=".nav-menu-collapse"
|
||||
aria-expanded="true"
|
||||
aria-controls="nav-sprachen nav-main-menu"
|
||||
>
|
||||
<i aria-hidden="true" class="fa-solid fa-chevron-left fhc-text"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,41 +68,8 @@ export default {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
link() {
|
||||
if (this.entry.template_kurzbz == 'redirect') {
|
||||
if (!this.entry.content)
|
||||
return '';
|
||||
let xmlDoc = (new DOMParser()).parseFromString(this.entry.content,"text/xml");
|
||||
let url = xmlDoc.getElementsByTagName('url')[0];
|
||||
|
||||
if (!url)
|
||||
return '';
|
||||
// TODO(chris): replace get params
|
||||
url = url.childNodes[0].nodeValue + "";
|
||||
if (url.includes("../cms/news.php")) {
|
||||
let news_regex = new RegExp("^\.\./cms/news\.php");
|
||||
url = url.replace(news_regex, FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/CisVue/Cms/news');
|
||||
}
|
||||
else if (url.includes("../cms/content.php?")) {
|
||||
let content_regex = new RegExp("^\.\./cms/content.php\\?content_id=([0-9]+)");
|
||||
let content_regex_result = content_regex.exec(url);
|
||||
// content_regex_result[1] will be the first matched group
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/CisVue/Cms/content/' + content_regex_result[1];
|
||||
}
|
||||
else if(url.includes("../index.ci.php")){
|
||||
let index_regex = new RegExp("^\.\./index\.ci\.php");
|
||||
url = url.replace(index_regex, FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router);
|
||||
}
|
||||
else if (url.includes("../")) {
|
||||
let relative_regex = new RegExp("^\.\./");
|
||||
url = url.replace(relative_regex, FHC_JS_DATA_STORAGE_OBJECT.app_root);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/CisVue/Cms/content/' + this.entry.content_id;
|
||||
},
|
||||
hasFullLink() {
|
||||
return this.link.startsWith(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router)
|
||||
return this.entry.url.startsWith(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router)
|
||||
},
|
||||
target() {
|
||||
if (this.entry.template_kurzbz == 'redirect') {
|
||||
@@ -145,7 +112,7 @@ export default {
|
||||
this.addUrlCount(count);
|
||||
},
|
||||
checkActiveUrl(url){
|
||||
this.getUrlMatchPoints(url,this.link);
|
||||
this.getUrlMatchPoints(url,this.entry.url);
|
||||
|
||||
let url_hash_spaceSymbol_regex = new RegExp("%20","gi");
|
||||
let url_hash_sharpSymbol_regex = new RegExp("^#");
|
||||
@@ -155,7 +122,7 @@ export default {
|
||||
// if the url hash contains the titel of the menu
|
||||
// or if the url equals the link of a menu
|
||||
// then set the menu active
|
||||
if (url_hash == this.entry.titel || url.href == this.link) {
|
||||
if (url_hash == this.entry.titel || url.href == this.entry.url) {
|
||||
this.setActiveEntry(this.entry.content_id);
|
||||
}
|
||||
},
|
||||
@@ -204,7 +171,7 @@ export default {
|
||||
<template v-if="hasChilds">
|
||||
<div class="btn-group w-100">
|
||||
<a :target="target"
|
||||
:href="(entry.menu_open && hasFullLink)?link:null"
|
||||
:href="(entry.menu_open && hasFullLink) ? entry.url : null"
|
||||
@click="toggleCollapse"
|
||||
:class="{
|
||||
'btn btn-default rounded-0 text-start': true,
|
||||
@@ -227,7 +194,7 @@ export default {
|
||||
</ul>
|
||||
</template>
|
||||
<a v-else
|
||||
:href="link"
|
||||
:href="entry.url"
|
||||
:target="target"
|
||||
:class="{
|
||||
'btn btn-default rounded-0 w-100 text-start': true,
|
||||
|
||||
@@ -100,7 +100,7 @@ export default {
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="event?.lehrfach_bez ">{{event?.lehrfach_bez + (event?.stg_kurzbzlang?' / ' + event?.stg_kurzbzlang:'')}}</span>
|
||||
<span v-else>Lehrveranstaltungs Übersicht</span>
|
||||
<span v-else>{{$p.t('lehre','lvUebersicht')}}</span>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,8 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
|
||||
|
||||
import ApiLvPlan from '../../../api/factory/lvPlan.js';
|
||||
|
||||
export const DEFAULT_MODE_RAUMINFO = 'Week'
|
||||
export const DEFAULT_MODE_RAUMINFO_MOBILE = 'List';
|
||||
export const DEFAULT_MODE_RAUMINFO_DESKTOP = 'Week';
|
||||
|
||||
export default {
|
||||
name: "RoomInformation",
|
||||
@@ -13,12 +14,14 @@ export default {
|
||||
viewData: Object, // NOTE(chris): this is inherited from router-view
|
||||
propsViewData: Object
|
||||
},
|
||||
inject: ["isMobile"],
|
||||
computed: {
|
||||
currentDay() {
|
||||
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
|
||||
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
|
||||
},
|
||||
currentMode() {
|
||||
return this.propsViewData?.mode || DEFAULT_MODE_RAUMINFO;
|
||||
const defaultMode = this.isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP;
|
||||
return this.propsViewData?.mode || defaultMode;
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
@@ -51,7 +54,6 @@ export default {
|
||||
<hr>
|
||||
<fhc-calendar
|
||||
ref="calendar"
|
||||
:timezone="viewData.timezone"
|
||||
:get-promise-func="getPromiseFunc"
|
||||
:date="currentDay"
|
||||
:mode="currentMode"
|
||||
|
||||
@@ -36,8 +36,9 @@ export default {
|
||||
return this.lvs.filter(lv => lv.studiengang_kz == studiengang.studiengang_kz && lv.semester == studiengang.semester);
|
||||
}
|
||||
},
|
||||
template: `<div class="mylv-semester" v-if="ready">
|
||||
<mylv-semester-studiengang v-for="studiengang in studiengaenge" :key="studiengang.studiengang_kz" v-bind="studiengang" :lvs="lvsForStudiengang(studiengang)"/>
|
||||
template: `
|
||||
<div class="mylv-semester" v-if="ready">
|
||||
<mylv-semester-studiengang v-for="studiengang in studiengaenge" :key="studiengang.studiengang_kz" v-bind="studiengang" :lvs="lvsForStudiengang(studiengang)" :semesterInfo="$props.semester" />
|
||||
</div>
|
||||
<div class="mylv-semester text-center" v-else>
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import MylvSemesterStudiengangLv from "./Studiengang/Lv.js";
|
||||
import MylvSemesterStudiengangAverageGrade from "./Studiengang/AverageGrade.js";
|
||||
import Phrasen from "../../../../mixins/Phrasen.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MylvSemesterStudiengangLv
|
||||
MylvSemesterStudiengangLv,
|
||||
MylvSemesterStudiengangAverageGrade
|
||||
},
|
||||
mixins: [
|
||||
Phrasen
|
||||
@@ -12,6 +14,7 @@ export default {
|
||||
bezeichnung: String,
|
||||
kuerzel: String,
|
||||
semester: [String,Number],
|
||||
semesterInfo: [String,Number],
|
||||
lvs: Array,
|
||||
sg_bezeichnung_eng: String
|
||||
},
|
||||
@@ -29,9 +32,10 @@ export default {
|
||||
methods: {
|
||||
note(lv) {
|
||||
return lv.benotung ? lv.znote || lv.lvnote || null : null;
|
||||
}
|
||||
},
|
||||
},
|
||||
template: `<div class="card mb-3">
|
||||
|
||||
<div class="card-body">
|
||||
<h4 class="card-title mb-3">{{$p.user_language.value === 'English' ? sg_bezeichnung_eng : bezeichnung}} - {{kuerzel}}
|
||||
<small>{{semester}}.{{$p.t('lehre/semester')}}</small>
|
||||
@@ -41,6 +45,7 @@ export default {
|
||||
<mylv-semester-studiengang-lv v-bind="lv" class="text-center h-100"></mylv-semester-studiengang-lv>
|
||||
</div>
|
||||
</div>
|
||||
<mylv-semester-studiengang-average-grade :semesterInfo="$props.semesterInfo" />
|
||||
</div>
|
||||
</div>`
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import Phrasen from "../../../../../mixins/Phrasen.js";
|
||||
import ApiLehre from "../../../../../api/factory/lehre.js";
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
Phrasen
|
||||
],
|
||||
props: {
|
||||
semesterInfo: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
gradeAverage: null,
|
||||
gradeWeightedAverage: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchAverageGrade() {
|
||||
this.gradeAverage = null;
|
||||
this.gradeWeightedAverage = null;
|
||||
if (!this.$props.semesterInfo) return;
|
||||
|
||||
let gradeAverageResponse = await this.$api.call(
|
||||
ApiLehre.getSemesterAverageGrade(this.$props.semesterInfo),
|
||||
);
|
||||
const gradeAverageResponseData = gradeAverageResponse.data;
|
||||
this.gradeAverage =
|
||||
gradeAverageResponseData.average_grade?.toFixed(2);
|
||||
this.gradeWeightedAverage =
|
||||
gradeAverageResponseData.weighted_average_grade?.toFixed(2);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
semesterInfo() {
|
||||
this.fetchAverageGrade();
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.fetchAverageGrade();
|
||||
},
|
||||
template: /*html*/`
|
||||
<div class="card mylv-semester-studiengang-grades">
|
||||
|
||||
<div class="card-header text-center">
|
||||
<h6>{{$p.t('lehre/notenstatistik')}}</h6>
|
||||
</div>
|
||||
|
||||
<div v-if="gradeAverage && gradeWeightedAverage">
|
||||
<table class="card-body table w-auto mx-auto">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-end">
|
||||
{{$p.t('lehre/headerAverage')}}
|
||||
</td>
|
||||
<td class="text-start">
|
||||
{{ gradeAverage }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-end">
|
||||
{{$p.t('lehre/headerWeightedAverage')}}
|
||||
</td>
|
||||
<td class="text-start">
|
||||
{{ gradeWeightedAverage }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="card-body text-center">
|
||||
<p>{{$p.t('lehre/info_noGradesYet')}}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="gradeAverage && gradeWeightedAverage" class="card-footer d-flex align-items-start text-muted small">
|
||||
<i class="fa fa-circle-info me-2 mt-1"></i>
|
||||
<div>
|
||||
<strong>{{$p.t('ui', 'hinweis')}}</strong><br>
|
||||
* {{$p.t('lehre/noticeAverage')}}
|
||||
<br>
|
||||
** {{$p.t('lehre/noticeWeightedAverage')}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -24,9 +24,9 @@ export default {
|
||||
farbe: String,
|
||||
lvinfo: Boolean,
|
||||
benotung: Boolean,
|
||||
lvnote: String,
|
||||
lvnote: [String, Number],
|
||||
lvnotebez: Array,
|
||||
znote: String,
|
||||
znote: [String, Number],
|
||||
znotebez: Array,
|
||||
studiengang_kuerzel: String,
|
||||
semester: [String, Number],
|
||||
@@ -37,6 +37,11 @@ export default {
|
||||
positiv: Boolean,
|
||||
note_index: String
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
studium_studiensemester: Vue.computed(() => this.studien_semester),
|
||||
}
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
pruefungenData: null,
|
||||
@@ -73,11 +78,11 @@ export default {
|
||||
},
|
||||
grade() {
|
||||
const languageIndex = this.$p.user_language.value === 'English' ? 1 : 0
|
||||
// no more showing of grade LV, if grade Zeugnis is not existing yet
|
||||
if(this.benotung && this.znotebez?.length) {
|
||||
return this.znotebez[languageIndex]
|
||||
} else if(this.benotung && this.lvnotebez?.length) {
|
||||
return this.lvnotebez[languageIndex]
|
||||
} else return null
|
||||
}
|
||||
else return null
|
||||
},
|
||||
LvHasPruefungenInformation(){
|
||||
return this.pruefungenData && this.pruefungenData.length > 0;
|
||||
@@ -119,7 +124,6 @@ export default {
|
||||
}
|
||||
},
|
||||
openPruefungen() {
|
||||
// early return if the pruefungenData is empty or not set
|
||||
if (!this.LvHasPruefungenInformation) return;
|
||||
|
||||
LvPruefungen.popup({
|
||||
@@ -205,21 +209,14 @@ export default {
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="!emptyMenu" class="card-footer">
|
||||
<div class="row">
|
||||
<!-- template for the LV if there are multiple pruefungen -->
|
||||
<template v-if="LvHasPruefungenInformation">
|
||||
<a href="#" class="col-auto text-start text-decoration-none" @click.prevent="openPruefungen">
|
||||
<i class="fa fa-check text-success" v-if="positiv"></i>
|
||||
<span class="ps-1" :style="'color:'+gradeColor">{{ grade || $p.t('lehre/noGrades') }}</span>
|
||||
</a>
|
||||
</template>
|
||||
<!-- template for the LV with no pruefungen -->
|
||||
<template v-else>
|
||||
<span class="col-auto text-start text-decoration-none" >
|
||||
<i class="fa fa-check text-success" v-if="positiv"></i>
|
||||
<span class="ps-1" :style="'color:'+gradeColor">{{ grade || $p.t('lehre/noGrades') }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<div
|
||||
@click.prevent="openPruefungen()"
|
||||
:type="LvHasPruefungenInformation ? 'button' : ''"
|
||||
class="d-flex flex-row align-items-center gap-1"
|
||||
>
|
||||
<i class="fa fa-check text-success" v-if="positiv"></i>
|
||||
<span :style="'color:'+gradeColor">{{ grade || $p.t('lehre/noGrades') }}</span>
|
||||
<i v-if="LvHasPruefungenInformation" class="fa fa-circle-info ms-1"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
@@ -96,34 +96,35 @@ export default {
|
||||
|
||||
},
|
||||
template: `
|
||||
|
||||
<h2>{{$p.t('lehre/myLV')}}</h2>
|
||||
<hr>
|
||||
<div class="mylv-student" v-if="ready">
|
||||
<div v-if="currentSemester" class="row justify-content-center mb-3">
|
||||
<div class="col-auto d-none">
|
||||
<label class="col-form-label">{{$p.t('lehre/studiensemester')}}</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="input-group">
|
||||
<button :aria-label="$p.t('lehre','previousStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','previousStudSemester')}" class="btn btn-outline-secondary" type="button" :disabled="currentIsFirst" @click="prevSem">
|
||||
<i class="fa fa-caret-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<select ref="studiensemester" v-model="currentSemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="updateRouter($event.target.value)">
|
||||
<option v-for="semester in studiensemester" :key="semester.studiensemester_kurzbz">{{semester.studiensemester_kurzbz}}</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" :aria-label="$p.t('lehre','nextStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','nextStudSemester')}" type="button" :disabled="currentIsLast" @click="nextSem">
|
||||
<i class="fa fa-caret-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div>
|
||||
<h2>{{$p.t('lehre/myLV')}}</h2>
|
||||
<hr>
|
||||
<div class="mylv-student" v-if="ready">
|
||||
<div v-if="currentSemester" class="row justify-content-center mb-3">
|
||||
<div class="col-auto d-none">
|
||||
<label class="col-form-label">{{$p.t('lehre/studiensemester')}}</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="input-group">
|
||||
<button :aria-label="$p.t('lehre','previousStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','previousStudSemester')}" class="btn btn-outline-secondary" type="button" :disabled="currentIsFirst" @click="prevSem">
|
||||
<i class="fa fa-caret-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<select ref="studiensemester" v-model="currentSemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="updateRouter($event.target.value)">
|
||||
<option v-for="semester in studiensemester" :key="semester.studiensemester_kurzbz">{{semester.studiensemester_kurzbz}}</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" :aria-label="$p.t('lehre','nextStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','nextStudSemester')}" type="button" :disabled="currentIsLast" @click="nextSem">
|
||||
<i class="fa fa-caret-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-danger" role="alert" v-else>
|
||||
{{$p.t('lehre/noLvFound')}}
|
||||
</div>
|
||||
<mylv-semester v-bind="current"/>
|
||||
</div>
|
||||
<div class="alert alert-danger" role="alert" v-else>
|
||||
{{$p.t('lehre/noLvFound')}}
|
||||
<div class="mylv-student text-center" v-else>
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
<mylv-semester v-bind="current"/>
|
||||
</div>
|
||||
<div class="mylv-student text-center" v-else>
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>`
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import QuickLinks from "./ProfilComponents/QuickLinks.js";
|
||||
import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
|
||||
import RoleInformation from "./ProfilComponents/RoleInformation.js";
|
||||
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
|
||||
import CalendarSync from "./ProfilComponents/CalendarSync.js";
|
||||
|
||||
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
|
||||
import { dateFilter } from '../../../tabulator/filters/Dates.js';
|
||||
@@ -26,6 +27,7 @@ export default {
|
||||
ProfilEmails,
|
||||
RoleInformation,
|
||||
ProfilInformation,
|
||||
CalendarSync,
|
||||
},
|
||||
|
||||
inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"],
|
||||
@@ -34,7 +36,6 @@ export default {
|
||||
return {
|
||||
showModal: false,
|
||||
editDataFilter: null,
|
||||
preloadedPhrasen:{},
|
||||
// tabulator options
|
||||
funktionen_table_options: {
|
||||
persistenceID: "filterTableMaProfilFunktionen",
|
||||
@@ -46,6 +47,8 @@ export default {
|
||||
responsiveLayout: "collapse",
|
||||
responsiveLayoutCollapseUseFormatters: false,
|
||||
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
|
||||
responsiveLayoutCollapseStartOpen: false,
|
||||
locale: true,
|
||||
columns: [
|
||||
{
|
||||
title:
|
||||
@@ -56,24 +59,30 @@ export default {
|
||||
formatter: "responsiveCollapse",
|
||||
maxWidth: 40,
|
||||
headerClick: this.collapseFunction,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.bezeichnungPhrase),
|
||||
title: "placeholder",
|
||||
titlePhrase: "ui/bezeichnung",
|
||||
field: "Bezeichnung",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.organisationseinheitPhrase),
|
||||
title: "placeholder",
|
||||
titlePhrase: "lehre/organisationseinheit",
|
||||
field: "Organisationseinheit",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 1,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.gueltigVonPhrase),
|
||||
title: "placeholder",
|
||||
titlePhrase: "global/gueltigVon",
|
||||
field: "Gültig_von",
|
||||
headerFilterFunc: 'dates',
|
||||
headerFilter: dateFilter,
|
||||
@@ -81,10 +90,12 @@ export default {
|
||||
minWidth: 200,
|
||||
visible: true,
|
||||
formatter:"datetime",
|
||||
formatterParams: this.datetimeFormatterParams()
|
||||
formatterParams: this.datetimeFormatterParams(),
|
||||
responsive: 4,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.gueltigBisPhrase),
|
||||
title: "placeholder",
|
||||
titlePhrase: "global/gueltigBis",
|
||||
field: "Gültig_bis",
|
||||
headerFilterFunc: 'dates',
|
||||
headerFilter: dateFilter,
|
||||
@@ -92,18 +103,20 @@ export default {
|
||||
minWidth: 200,
|
||||
visible: true,
|
||||
formatter:"datetime",
|
||||
formatterParams: this.datetimeFormatterParams()
|
||||
formatterParams: this.datetimeFormatterParams(),
|
||||
responsive: 3,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.wochenstundenPhrase),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/wochenstunden",
|
||||
field: "Wochenstunden",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
betriebsmittel_table_options: {
|
||||
persistenceID: "filterTableMaProfilBetriebsmittel",
|
||||
persistence: {
|
||||
@@ -115,6 +128,8 @@ export default {
|
||||
responsiveLayoutCollapseUseFormatters: false,
|
||||
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
|
||||
data: [{betriebsmittel: "", Nummer: "", Ausgegeben_am: ""}],
|
||||
responsiveLayoutCollapseStartOpen: false,
|
||||
locale: true,
|
||||
columns: [
|
||||
{
|
||||
title:
|
||||
@@ -125,32 +140,39 @@ export default {
|
||||
formatter: "responsiveCollapse",
|
||||
maxWidth: 40,
|
||||
headerClick: this.collapseFunction,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.entlehnteBetriebsmittelPhrase),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/entlehnteBetriebsmittel",
|
||||
field: "betriebsmittel",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.inventarnummerPhrase),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/inventarnummer",
|
||||
field: "Nummer",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 2,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.ausgabedatumPhrase),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/ausgabedatum",
|
||||
field: "Ausgegeben_am",
|
||||
headerFilterFunc: 'dates',
|
||||
headerFilter: dateFilter,
|
||||
minWidth: 200,
|
||||
visible: true,
|
||||
formatter:"datetime",
|
||||
formatterParams: this.datetimeFormatterParams()
|
||||
formatterParams: this.datetimeFormatterParams(),
|
||||
responsive: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -160,15 +182,14 @@ export default {
|
||||
props: {
|
||||
data: Object,
|
||||
editData: Object,
|
||||
calendarSyncUrls: Array,
|
||||
},
|
||||
|
||||
methods: {
|
||||
betriebsmittelTableBuilt: function () {
|
||||
this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns)
|
||||
this.$refs.betriebsmittelTable.tabulator.setData(this.data.mittel);
|
||||
},
|
||||
funktionenTableBuilt: function () {
|
||||
this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns)
|
||||
this.$refs.funktionenTable.tabulator.setData(this.data.funktionen);
|
||||
},
|
||||
hideEditProfilModal: function () {
|
||||
@@ -218,10 +239,6 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
setTableColumnTitles() { // reevaluates computed phrasen
|
||||
if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns)
|
||||
if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns)
|
||||
},
|
||||
datetimeFormatterParams: function() {
|
||||
const params = {
|
||||
inputFormat:"yyyy-MM-dd",
|
||||
@@ -296,24 +313,16 @@ export default {
|
||||
}
|
||||
};
|
||||
},
|
||||
quickLinks() {
|
||||
let quickLinks = [];
|
||||
//
|
||||
return quickLinks;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
// preload phrasen
|
||||
this.$p.loadCategory(["ui","lehre","global","profil"]).then(() => {
|
||||
this.preloadedPhrasen.bezeichnungPhrase = this.$p.t('ui/bezeichnung');
|
||||
this.preloadedPhrasen.organisationseinheitPhrase = this.$p.t('lehre/organisationseinheit');
|
||||
this.preloadedPhrasen.gueltigVonPhrase = this.$p.t('global/gueltigVon');
|
||||
this.preloadedPhrasen.gueltigBisPhrase = this.$p.t('global/gueltigBis');
|
||||
this.preloadedPhrasen.wochenstundenPhrase = this.$p.t('profil/wochenstunden');
|
||||
this.preloadedPhrasen.entlehnteBetriebsmittelPhrase = this.$p.t('profil/entlehnteBetriebsmittel');
|
||||
this.preloadedPhrasen.inventarnummerPhrase = this.$p.t('profil/inventarnummer');
|
||||
this.preloadedPhrasen.ausgabedatumPhrase = this.$p.t('profil/ausgabedatum');
|
||||
this.preloadedPhrasen.loaded=true;
|
||||
});
|
||||
//? sorts the profil Updates: pending -> accepted -> rejected
|
||||
this.data.profilUpdates?.sort(this.sortProfilUpdates);
|
||||
|
||||
},
|
||||
watch: {
|
||||
'data.funktionen'(newVal) {
|
||||
@@ -322,22 +331,13 @@ export default {
|
||||
'data.mittel'(newVal) {
|
||||
if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setData(newVal);
|
||||
},
|
||||
'language.value'(newVal) {
|
||||
this.setTableColumnTitles()
|
||||
}
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="container-fluid text-break fhc-form" >
|
||||
<edit-profil v-if="showModal" ref="editModal" :isMitarbeiter="true" @hideBsModal="hideEditProfilModal" :value="JSON.parse(JSON.stringify(filteredEditData))" :titel="$p.t('profil','profilBearbeiten')"></edit-profil>
|
||||
<div class="row">
|
||||
<div class="d-md-none col-12 ">
|
||||
<!--TODO: uncomment when implemented
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true"></quick-links>
|
||||
</div>
|
||||
</div>-->
|
||||
<!-- Bearbeiten Button -->
|
||||
<div class="d-md-none col-12">
|
||||
<!-- Bearbeiten Button -->
|
||||
<div v-if="isEditable" class="row mb-3 ">
|
||||
<div class="col">
|
||||
<button @click="()=>showEditProfilModal()" type="button" class="text-start card w-100 btn btn-outline-secondary" >
|
||||
@@ -350,9 +350,9 @@ export default {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- MOBILE PROFIL UPDATES -->
|
||||
<div v-if="data.profilUpdates" class="row mb-3">
|
||||
<div class="col">
|
||||
<!-- MOBILE PROFIL UPDATES -->
|
||||
<fetch-profil-updates v-if="data.profilUpdates && data.profilUpdates.length" @fetchUpdates="fetchProfilUpdates" :data="data.profilUpdates" ></fetch-profil-updates>
|
||||
</div>
|
||||
</div>
|
||||
@@ -365,30 +365,42 @@ export default {
|
||||
<!-- ROW WITH THE PROFIL INFORMATION -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-12 col-xl-6 ">
|
||||
<!-- PROFIL INFORMATION -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- PROFIL INFORMATION -->
|
||||
<profil-information @showEditProfilModal="showEditProfilModal" :title="$p.t('profil','mitarbeiterIn')" :data="profilInformation" :fotoStatus="fotoStatus"></profil-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- QUICK LINKS, MOBILE VIEW (HIDDEN IF VIEWPORT >= MD BREAKPOINT) -->
|
||||
<div v-if="quickLinks.length" class="row mb-4 d-md-none">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil/quickLinks')" :links="quickLinks" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- CALENDAR SYNC OPTIONS, MOBILE VIEW (HIDDEN IF VIEWPORT >= MD BREAKPOINT) -->
|
||||
<div class="row mb-4 d-md-none">
|
||||
<div class="col">
|
||||
<calendar-sync :uid="$props.data.username" :calendarSyncUrls="$props.calendarSyncUrls"></calendar-sync>
|
||||
</div>
|
||||
</div>
|
||||
<!-- MITARBEITER INFO -->
|
||||
<div class="row mb-4">
|
||||
<div class=" col-lg-12">
|
||||
<!-- MITARBEITER INFO -->
|
||||
<role-information :title="$p.t('profil','mitarbeiterInformation')" :data="roleInformation"></role-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
|
||||
</div>
|
||||
<div class="col-xl-6 col-lg-12 ">
|
||||
<!-- EMAILS -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- EMAILS -->
|
||||
<profil-emails :title="this.$p.t('person','email')" :data="data.emails" ></profil-emails>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PRIVATE KONTAKTE-->
|
||||
<div class="row mb-4 ">
|
||||
<div class="col">
|
||||
<!-- PRIVATE KONTAKTE-->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
@@ -410,9 +422,9 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PRIVATE ADRESSEN-->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- PRIVATE ADRESSEN-->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
@@ -437,10 +449,9 @@ export default {
|
||||
</div>
|
||||
</div >
|
||||
<div class="row">
|
||||
<!-- FUNKTIONEN TABELLE -->
|
||||
<div class="col-12 mb-4" >
|
||||
<!-- FUNKTIONEN TABELLE -->
|
||||
<core-filter-cmpt
|
||||
v-if="preloadedPhrasen.loaded"
|
||||
@tableBuilt="funktionenTableBuilt"
|
||||
:title="$p.t('person','funktionen')"
|
||||
ref="funktionenTable"
|
||||
@@ -449,33 +460,22 @@ export default {
|
||||
:sideMenu="false"
|
||||
/>
|
||||
</div>
|
||||
<!-- BETRIEBSMITTEL TABELLE -->
|
||||
<div class="col-12 mb-4" >
|
||||
<!-- BETRIEBSMITTEL TABELLE -->
|
||||
<core-filter-cmpt
|
||||
v-if="preloadedPhrasen.loaded"
|
||||
@tableBuilt="betriebsmittelTableBuilt"
|
||||
:title="$p.t('profil','entlehnteBetriebsmittel')"
|
||||
ref="betriebsmittelTable"
|
||||
:tabulator-options="betriebsmittel_table_options"
|
||||
tableOnly
|
||||
:sideMenu="false"
|
||||
:isUsingPresets="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF SIDE PANEL -->
|
||||
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
|
||||
<!--TODO: uncomment when implemented
|
||||
<div class="row d-none d-md-block mb-3">
|
||||
|
||||
<div class="col">
|
||||
|
||||
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>-->
|
||||
<!-- Bearbeiten Button -->
|
||||
<div class="row d-none d-md-block ">
|
||||
<div class="col mb-3">
|
||||
@@ -489,21 +489,33 @@ export default {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- QUICK LINKS, HIDDEN IF VIEWPORT < MD BREAKPOINT -->
|
||||
<div v-if="quickLinks.length" class="row mb-3 d-none d-md-block">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil/quickLinks')" :links="quickLinks" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- CALENDAR SYNC OPTIONS, HIDDEN IF VIEWPORT < MD BREAKPOINT -->
|
||||
<div class="row mb-3 d-none d-md-block">
|
||||
<div class="col">
|
||||
<calendar-sync :uid="$props.data.username" :calendarSyncUrls="$props.calendarSyncUrls"></calendar-sync>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PROFIL UPDATES -->
|
||||
<div v-if="data.profilUpdates" class="row d-none d-md-block mb-3">
|
||||
<div class="col mb-3">
|
||||
<!-- PROFIL UPDATES -->
|
||||
<fetch-profil-updates v-if="data.profilUpdates && data.profilUpdates.length" @fetchUpdates="fetchProfilUpdates" :data="data.profilUpdates"></fetch-profil-updates>
|
||||
</div>
|
||||
</div>
|
||||
<!-- AUSWEIS STATUS -->
|
||||
<div class="row mb-3" >
|
||||
<div class="col-12">
|
||||
<!-- AUSWEIS STATUS -->
|
||||
<ausweis-status :data="data.zutrittsdatum"></ausweis-status>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- MAILVERTEILER -->
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<!-- MAILVERTEILER -->
|
||||
<mailverteiler :data="data?.mailverteiler" :title="$p.t('profil','mailverteiler')"></mailverteiler>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,92 +1,102 @@
|
||||
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
|
||||
import { CoreFilterCmpt } from "../../../components/filter/Filter.js";
|
||||
import Mailverteiler from "./ProfilComponents/Mailverteiler.js";
|
||||
import QuickLinks from "./ProfilComponents/QuickLinks.js";
|
||||
import RoleInformation from "./ProfilComponents/RoleInformation.js";
|
||||
import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
|
||||
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
|
||||
import QuickLinks from "./ProfilComponents/QuickLinks.js";
|
||||
|
||||
import { dateFilter } from '../../../tabulator/filters/Dates.js';
|
||||
import { dateFilter } from "../../../tabulator/filters/Dates.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CoreFilterCmpt,
|
||||
Mailverteiler,
|
||||
QuickLinks,
|
||||
RoleInformation,
|
||||
ProfilEmails,
|
||||
ProfilInformation,
|
||||
QuickLinks,
|
||||
},
|
||||
inject: ["collapseFunction", "language"],
|
||||
data() {
|
||||
return {
|
||||
collapseIconFunktionen: true,
|
||||
preloadedPhrasen:{},
|
||||
funktionen_table_options: {
|
||||
persistenceID: "filterTableMaViewProfilFunktionen",
|
||||
persistence: {
|
||||
columns: false
|
||||
columns: false,
|
||||
},
|
||||
minHeight: 300,
|
||||
layout: "fitColumns",
|
||||
responsiveLayout: "collapse",
|
||||
responsiveLayoutCollapseUseFormatters: false,
|
||||
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
|
||||
locale: true,
|
||||
columns: [
|
||||
//? option when wanting to hide the collapsed list
|
||||
|
||||
{
|
||||
title:
|
||||
"<i id='collapseIconFunktionen' role='button' class='fa-solid fa-angle-down '></i>",
|
||||
title: "<i id='collapseIconFunktionen' role='button' class='fa-solid fa-angle-down '></i>",
|
||||
field: "collapse",
|
||||
headerSort: false,
|
||||
headerFilter: false,
|
||||
formatter: "responsiveCollapse",
|
||||
maxWidth: 40,
|
||||
headerClick: this.collapseFunction,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('ui/bezeichnung')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "ui/bezeichnung",
|
||||
field: "Bezeichnung",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('lehre/organisationseinheit')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "lehre/organisationseinheit",
|
||||
field: "Organisationseinheit",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 1,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('global/gueltigVon')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "global/gueltigVon",
|
||||
field: "Gültig_von",
|
||||
headerFilterFunc: 'dates',
|
||||
headerFilterFunc: "dates",
|
||||
headerFilter: dateFilter,
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
visible: true,
|
||||
formatter:"datetime",
|
||||
formatterParams: this.datetimeFormatterParams()
|
||||
formatter: "datetime",
|
||||
formatterParams: this.datetimeFormatterParams(),
|
||||
responsive: 4,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('global/gueltigBis')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "global/gueltigBis",
|
||||
field: "Gültig_bis",
|
||||
headerFilterFunc: 'dates',
|
||||
headerFilterFunc: "dates",
|
||||
headerFilter: dateFilter,
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
visible: true,
|
||||
formatter:"datetime",
|
||||
formatterParams: this.datetimeFormatterParams()
|
||||
formatter: "datetime",
|
||||
formatterParams: this.datetimeFormatterParams(),
|
||||
responsive: 3,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/wochenstunden')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/wochenstunden",
|
||||
field: "Wochenstunden",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -94,47 +104,56 @@ export default {
|
||||
},
|
||||
|
||||
//? this is the prop passed to the dynamic component with the custom data of the view
|
||||
props: ["data"],
|
||||
props: ["data", "permissions"],
|
||||
methods: {
|
||||
funktionenTableBuilt: function () {
|
||||
this.$refs.funktionenTable.tabulator.setData(this.data.funktionen);
|
||||
},
|
||||
datetimeFormatterParams: function() {
|
||||
datetimeFormatterParams: function () {
|
||||
const params = {
|
||||
inputFormat:"yyyy-MM-dd",
|
||||
outputFormat:"dd.MM.yyyy",
|
||||
invalidPlaceholder:"(invalid date)",
|
||||
timezone:FHC_JS_DATA_STORAGE_OBJECT.timezone
|
||||
inputFormat: "yyyy-MM-dd",
|
||||
outputFormat: "dd.MM.yyyy",
|
||||
invalidPlaceholder: "(invalid date)",
|
||||
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
|
||||
};
|
||||
return params;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'data.funktionen'(newVal) {
|
||||
if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setData(newVal);
|
||||
"data.funktionen"(newVal) {
|
||||
if (this.$refs.funktionenTable)
|
||||
this.$refs.funktionenTable.tabulator.setData(newVal);
|
||||
},
|
||||
"language.value"(newVal) {
|
||||
// reevaluates computed phrasen
|
||||
if (this.$refs.funktionenTable)
|
||||
this.$refs.funktionenTable.tabulator.setColumns(
|
||||
this.funktionen_table_options.columns,
|
||||
);
|
||||
},
|
||||
'language.value'(newVal) { // reevaluates computed phrasen
|
||||
if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getTelefonValue() {
|
||||
if(this.data.standort_telefon?.kontakt) {
|
||||
return this.data.standort_telefon.kontakt + " " + this.data.telefonklappe
|
||||
} else if(this.data.standort_telefon) {
|
||||
return this.data.standort_telefon + " " + this.data.telefonklappe
|
||||
if (this.data.standort_telefon?.kontakt) {
|
||||
return (
|
||||
this.data.standort_telefon.kontakt +
|
||||
" " +
|
||||
this.data.telefonklappe
|
||||
);
|
||||
} else if (this.data.standort_telefon) {
|
||||
return (
|
||||
this.data.standort_telefon + " " + this.data.telefonklappe
|
||||
);
|
||||
} else {
|
||||
return this.data.telefonklappe
|
||||
return this.data.telefonklappe;
|
||||
}
|
||||
},
|
||||
fotoStatus() {
|
||||
return this.data?.fotoStatus ?? null;
|
||||
},
|
||||
|
||||
personEmails() {
|
||||
return this.data?.emails ? this.data.emails : [];
|
||||
},
|
||||
|
||||
profilInformation() {
|
||||
if (!this.data) {
|
||||
return {};
|
||||
@@ -151,44 +170,52 @@ export default {
|
||||
foto: this.data.foto,
|
||||
};
|
||||
},
|
||||
|
||||
roleInformation() {
|
||||
if (!this.data) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
geburtsdatum: {
|
||||
label: `${this.$p.t('profil','Geburtsdatum')}`,
|
||||
value: this.data.gebdatum
|
||||
label: `${this.$p.t("profil", "Geburtsdatum")}`,
|
||||
value: this.data.gebdatum,
|
||||
},
|
||||
geburtsort: {
|
||||
label: `${this.$p.t('profil','Geburtsort')}`,
|
||||
value: this.data.gebort
|
||||
label: `${this.$p.t("profil", "Geburtsort")}`,
|
||||
value: this.data.gebort,
|
||||
},
|
||||
personenkennzeichen: {
|
||||
label: `${this.$p.t('profil','Kurzzeichen')}`,
|
||||
value: this.data.kurzbz
|
||||
label: `${this.$p.t("profil", "Kurzzeichen")}`,
|
||||
value: this.data.kurzbz,
|
||||
},
|
||||
telefon: {
|
||||
label: `${this.$p.t('profil','Telefon')}`,
|
||||
value: this.getTelefonValue
|
||||
label: `${this.$p.t("profil", "Telefon")}`,
|
||||
value: this.getTelefonValue,
|
||||
},
|
||||
office: {
|
||||
label: `${this.$p.t('profil','Büro')}`,
|
||||
value: this.data.ort_kurzbz
|
||||
}
|
||||
label: `${this.$p.t("profil", "Büro")}`,
|
||||
value: this.data.ort_kurzbz,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
created(){
|
||||
this.$p.loadCategory(["ui", "lehre", "global", "profil"]).then(() => {
|
||||
this.preloadedPhrasen.bezeichnungPhrase = this.$p.t('ui/bezeichnung');
|
||||
this.preloadedPhrasen.organisationseinheitPhrase = this.$p.t('lehre/organisationseinheit');
|
||||
this.preloadedPhrasen.gueltigVonPhrase = this.$p.t('global/gueltigVon');
|
||||
this.preloadedPhrasen.gueltigBisPhrase = this.$p.t('global/gueltigBis');
|
||||
this.preloadedPhrasen.wochenstundenPhrase = this.$p.t('profil/wochenstunden');
|
||||
this.preloadedPhrasen.loaded = true;
|
||||
});
|
||||
quickLinks() {
|
||||
let quickLinks = [];
|
||||
if (
|
||||
this.$props.permissions &&
|
||||
this.$props.permissions["basis/other_lv_plan"]
|
||||
) {
|
||||
quickLinks.push({
|
||||
icon: "fa-calendar-days",
|
||||
phrase: "lehre/stundenplan",
|
||||
action: () => {
|
||||
this.$router.push({
|
||||
name: "OtherLvPlan",
|
||||
params: { otherUid: this.$props.data.username },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
return quickLinks;
|
||||
},
|
||||
},
|
||||
|
||||
template: /*html*/ `
|
||||
@@ -196,15 +223,6 @@ export default {
|
||||
<div class="container-fluid text-break fhc-form" >
|
||||
<!-- ROW -->
|
||||
<div class="row">
|
||||
<!-- HIDDEN QUICK LINKS -->
|
||||
<!-- TODO: uncomment when implemented
|
||||
<div class="d-md-none col-12 ">
|
||||
|
||||
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true" ></quick-links>
|
||||
|
||||
</div>
|
||||
-->
|
||||
<!-- END OF HIDDEN QUCK LINKS -->
|
||||
<!-- MAIN PANNEL -->
|
||||
<div class="col-sm-12 col-md-8 col-xxl-9 ">
|
||||
<!-- ROW WITH PROFIL IMAGE AND INFORMATION -->
|
||||
@@ -213,27 +231,33 @@ export default {
|
||||
<div class="row mb-4">
|
||||
<!-- FIRST KAESTCHEN -->
|
||||
<div class="col-lg-12 col-xl-6 ">
|
||||
<!-- Profil Informationen -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- Profil Informationen -->
|
||||
<profil-information :title="$p.t('profil','mitarbeiterIn')" :data="profilInformation" :fotoStatus="fotoStatus"></profil-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- QUICK LINKS, MOBILE VIEW (HIDDEN IF VIEWPORT >= MD BREAKPOINT) -->
|
||||
<div v-if="quickLinks.length" class="row mb-4 d-md-none">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil/quickLinks')" :links="quickLinks" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
|
||||
<!-- END OF PROFIL INFORMATION ROW -->
|
||||
<!-- INFORMATION CONTENT END -->
|
||||
</div>
|
||||
<div class="col-xl-6 col-lg-12 ">
|
||||
<!-- EMAILS -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- EMAILS -->
|
||||
<profil-emails :title="this.$p.t('person','email')" :data="personEmails"></profil-emails>
|
||||
</div>
|
||||
</div>
|
||||
<!-- SECOND ROW OF SECOND COLUMN IN MAIN CONTENT -->
|
||||
<!-- roleInformation -->
|
||||
<div class="row mb-4">
|
||||
<div class=" col-lg-12">
|
||||
<!-- roleInformation -->
|
||||
<role-information :data="roleInformation" :title="$p.t('profil','mitarbeiterInformation')"></role-information>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,12 +266,12 @@ export default {
|
||||
</div>
|
||||
<!-- START OF THE SECOND PROFIL INFORMATION ROW -->
|
||||
<!-- ROW WITH PROFIL IMAGE AND INFORMATION END -->
|
||||
</div >
|
||||
</div>
|
||||
<!-- SECOND ROW UNDER THE PROFIL IMAGE AND INFORMATION WITH THE TABLES -->
|
||||
<div class="row">
|
||||
<!-- FIRST TABLE -->
|
||||
<div class="col-12 mb-4" >
|
||||
<core-filter-cmpt v-if="preloadedPhrasen.loaded" @tableBuilt="funktionenTableBuilt" :title="$p.t('person','funktionen')" ref="funktionenTable" :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" />
|
||||
<core-filter-cmpt @tableBuilt="funktionenTableBuilt" :title="$p.t('person','funktionen')" ref="funktionenTable" :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" />
|
||||
</div>
|
||||
<!-- END OF THE ROW WITH THE TABLES UNDER THE PROFIL INFORMATION -->
|
||||
</div>
|
||||
@@ -255,27 +279,23 @@ export default {
|
||||
</div>
|
||||
<!-- START OF SIDE PANEL -->
|
||||
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
|
||||
<!-- VISIBLE UNTIL VIEWPORT MD -->
|
||||
<!--TODO: uncomment when implemented
|
||||
<div class="row d-none d-md-block mb-3">
|
||||
<div class="col">
|
||||
|
||||
<quick-links :title="$p.t('profil','quickLinks')" ></quick-links>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<!-- MAILVERTEILER -->
|
||||
<mailverteiler :data="data?.mailverteiler" :title="$p.t('profil','mailverteiler')"></mailverteiler>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END OF SIDE PANEL -->
|
||||
</div>
|
||||
<!-- QUICK LINKS, HIDDEN IF VIEWPORT < MD BREAKPOINT -->
|
||||
<div v-if="quickLinks.length" class="row mb-3 d-none d-md-block">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil/quickLinks')" :links="quickLinks" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- MAILVERTEILER -->
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<mailverteiler :data="data?.mailverteiler" :title="$p.t('profil','mailverteiler')"></mailverteiler>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END OF SIDE PANEL -->
|
||||
</div>
|
||||
<!-- END OF CONTAINER ROW-->
|
||||
</div>
|
||||
<!-- END OF CONTAINER -->
|
||||
</div>
|
||||
<!-- END OF CONTAINER -->
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@ import ViewStudentProfil from "./StudentViewProfil.js";
|
||||
import ViewMitarbeiterProfil from "./MitarbeiterViewProfil.js";
|
||||
import Loading from "../../Loader.js";
|
||||
|
||||
import ApiProfil from '../../../api/factory/profil.js';
|
||||
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
|
||||
import ApiProfil from "../../../api/factory/profil.js";
|
||||
import ApiProfilUpdate from "../../../api/factory/profilUpdate.js";
|
||||
|
||||
Vue.$collapseFormatter = function (data) {
|
||||
//data - an array of objects containing the column title and value for each cell
|
||||
@@ -35,7 +35,7 @@ Vue.$collapseFormatter = function (data) {
|
||||
};
|
||||
|
||||
export const Profil = {
|
||||
name: 'Profil',
|
||||
name: "Profil",
|
||||
components: {
|
||||
StudentProfil,
|
||||
MitarbeiterProfil,
|
||||
@@ -46,11 +46,8 @@ export const Profil = {
|
||||
props: {
|
||||
uid: {
|
||||
type: String,
|
||||
required:false,
|
||||
required: false,
|
||||
},
|
||||
viewData: {
|
||||
type: Object,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -62,17 +59,19 @@ export const Profil = {
|
||||
data: null,
|
||||
// notfound is null by default, but contains an UID if no user exists with that UID
|
||||
notFoundUID: null,
|
||||
isEditable: this.viewData.editable ?? false,
|
||||
isEditable: false,
|
||||
authPermissions: null,
|
||||
calendarSyncUrls: [],
|
||||
};
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
isEditable: Vue.computed(()=>this.isEditable),
|
||||
isEditable: Vue.computed(() => this.isEditable),
|
||||
profilUpdateStates: Vue.computed(() =>
|
||||
this.profilUpdateStates ? this.profilUpdateStates : false
|
||||
this.profilUpdateStates ? this.profilUpdateStates : false,
|
||||
),
|
||||
profilUpdateTopic: Vue.computed(() =>
|
||||
this.profilUpdateTopic ? this.profilUpdateTopic : false
|
||||
this.profilUpdateTopic ? this.profilUpdateTopic : false,
|
||||
),
|
||||
setLoading: (newValue) => {
|
||||
this.loading = newValue;
|
||||
@@ -130,8 +129,12 @@ export const Profil = {
|
||||
//? if they have the same status the insert date is used for ordering
|
||||
if (ele1.status === ele2.status) {
|
||||
result =
|
||||
new Date(ele2.insertamum.split(".").reverse().join("-")) -
|
||||
new Date(ele1.insertamum.split(".").reverse().join("-"));
|
||||
new Date(
|
||||
ele2.insertamum.split(".").reverse().join("-"),
|
||||
) -
|
||||
new Date(
|
||||
ele1.insertamum.split(".").reverse().join("-"),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
@@ -139,6 +142,8 @@ export const Profil = {
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
await this.fetchViewData();
|
||||
|
||||
// fetch profilUpdateStates to provide them to children components
|
||||
await this.$api
|
||||
.call(ApiProfilUpdate.getStatus())
|
||||
@@ -157,20 +162,20 @@ export const Profil = {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
|
||||
this.$api
|
||||
.call(ApiProfil.profilViewData(this.$route.params.uid??null))
|
||||
.then((response) => response.data).then(data=>{
|
||||
this.view = data?.profil_data.view;
|
||||
this.data = data?.profil_data.data;
|
||||
this.isEditable = data?.editable ?? false;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
async fetchViewData() {
|
||||
let viewDataResult = await this.$api.call(
|
||||
ApiProfil.getProfilViewData(this.$route.params.uid ?? null),
|
||||
);
|
||||
|
||||
const data = viewDataResult.data;
|
||||
if (!data) return;
|
||||
|
||||
this.view = data.profil_data.view;
|
||||
this.isEditable = data.profil_data.editable;
|
||||
this.data = data.profil_data.data;
|
||||
this.calendarSyncUrls = data.calendar_sync_urls ?? [];
|
||||
this.authPermissions = data.permissions;
|
||||
},
|
||||
zustellAdressenCount() {
|
||||
if (!this.data || !this.data.adressen) {
|
||||
@@ -186,7 +191,7 @@ export const Profil = {
|
||||
})
|
||||
.map((adresse) => {
|
||||
return adresse.requested_change.adresse_id;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,8 +202,9 @@ export const Profil = {
|
||||
.every((adresse) =>
|
||||
this.data.profilUpdates.some(
|
||||
(update) =>
|
||||
update.requested_change.adresse_id == adresse.adresse_id
|
||||
)
|
||||
update.requested_change.adresse_id ==
|
||||
adresse.adresse_id,
|
||||
),
|
||||
)
|
||||
) {
|
||||
adressenArray = adressenArray.concat(
|
||||
@@ -208,12 +214,11 @@ export const Profil = {
|
||||
})
|
||||
.map((adr) => {
|
||||
return adr.adresse_id;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return [...new Set(adressenArray)];
|
||||
|
||||
},
|
||||
zustellKontakteCount() {
|
||||
if (!this.data || !this.data.kontakte) {
|
||||
@@ -226,14 +231,17 @@ export const Profil = {
|
||||
kontakteArray = kontakteArray.concat(
|
||||
this.data.profilUpdates
|
||||
.filter((update) => {
|
||||
return update.status === 'Pending' && update.requested_change.zustellung;
|
||||
return (
|
||||
update.status === "Pending" &&
|
||||
update.requested_change.zustellung
|
||||
);
|
||||
})
|
||||
.map((kontant) => {
|
||||
return {
|
||||
kontakt_id: kontant.requested_change.kontakt_id,
|
||||
kontakttyp: kontant.requested_change.kontakttyp
|
||||
};
|
||||
})
|
||||
kontakt_id: kontant.requested_change.kontakt_id,
|
||||
kontakttyp: kontant.requested_change.kontakttyp,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -244,8 +252,10 @@ export const Profil = {
|
||||
.every((kontakt) =>
|
||||
this.data.profilUpdates.some(
|
||||
(update) =>
|
||||
update.status === 'Pending' && update.requested_change.kontakt_id == kontakt.kontakt_id
|
||||
)
|
||||
update.status === "Pending" &&
|
||||
update.requested_change.kontakt_id ==
|
||||
kontakt.kontakt_id,
|
||||
),
|
||||
)
|
||||
) {
|
||||
kontakteArray = kontakteArray.concat(
|
||||
@@ -255,10 +265,10 @@ export const Profil = {
|
||||
})
|
||||
.map((kon) => {
|
||||
return {
|
||||
kontakt_id: kon.kontakt_id,
|
||||
kontakttyp: kon.kontakttyp
|
||||
};
|
||||
})
|
||||
kontakt_id: kon.kontakt_id,
|
||||
kontakttyp: kon.kontakttyp,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -266,7 +276,6 @@ export const Profil = {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
||||
filteredEditData() {
|
||||
if (!this.data) {
|
||||
return;
|
||||
@@ -330,8 +339,12 @@ export const Profil = {
|
||||
// excludes all contacts that are already used in pending profil update requests
|
||||
return !this.data.profilUpdates?.some(
|
||||
(update) =>
|
||||
update.status === this.profilUpdateStates["Pending"] &&
|
||||
update.requested_change?.kontakt_id === item.kontakt_id
|
||||
update.status ===
|
||||
this.profilUpdateStates[
|
||||
"Pending"
|
||||
] &&
|
||||
update.requested_change?.kontakt_id ===
|
||||
item.kontakt_id,
|
||||
);
|
||||
})
|
||||
.map((kontakt) => {
|
||||
@@ -347,12 +360,18 @@ export const Profil = {
|
||||
topic: this.profilUpdateTopic?.["Private Adressen"],
|
||||
data: this.data.adressen
|
||||
?.filter((item) => {
|
||||
return !this.data.profilUpdates?.some((update) => {
|
||||
return (
|
||||
update.status === this.profilUpdateStates["Pending"] &&
|
||||
update.requested_change?.adresse_id == item.adresse_id
|
||||
);
|
||||
});
|
||||
return !this.data.profilUpdates?.some(
|
||||
(update) => {
|
||||
return (
|
||||
update.status ===
|
||||
this.profilUpdateStates[
|
||||
"Pending"
|
||||
] &&
|
||||
update.requested_change
|
||||
?.adresse_id == item.adresse_id
|
||||
);
|
||||
},
|
||||
);
|
||||
})
|
||||
.map((adresse) => {
|
||||
return {
|
||||
@@ -374,23 +393,28 @@ export const Profil = {
|
||||
this.$refs.loadingModalRef.hide();
|
||||
}
|
||||
},
|
||||
uid (newVal, oldVal) {
|
||||
this.load()
|
||||
}
|
||||
uid(newVal, oldVal) {
|
||||
this.load();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.load()
|
||||
this.load();
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="pb-4">
|
||||
<div v-if="notFoundUID">
|
||||
<h3>Es wurde keine Person mit der UID {{this.notFoundUID}} gefunden</h3>
|
||||
</div>
|
||||
<div v-else>
|
||||
<loading ref="loadingModalRef" :timeout="0"></loading>
|
||||
<component :is="view" :data="data" :editData="filteredEditData" ></component>
|
||||
<component
|
||||
:is="view"
|
||||
:data="data"
|
||||
:editData="filteredEditData"
|
||||
:permissions="authPermissions"
|
||||
:calendarSyncUrls="calendarSyncUrls"></component>
|
||||
</div>
|
||||
</div>`,
|
||||
}
|
||||
};
|
||||
|
||||
export default Profil
|
||||
export default Profil;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
export default {
|
||||
name: "CalendarSync",
|
||||
props: { uid: String, calendarSyncUrls: Array },
|
||||
data() {
|
||||
return {
|
||||
syncInstructionsUrlWithoutParam:
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root +
|
||||
"cms/content.php?content_id=",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
copyUrlToClipboard(url) {
|
||||
navigator.clipboard.writeText(url);
|
||||
this.$fhcAlert.alertSuccess(
|
||||
this.$p.t("profil/calendar_sync_clipboard_copy_confirmation"),
|
||||
);
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
{{ $p.t("profil/calendar_sync") }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<a
|
||||
target="_blank"
|
||||
:href="syncInstructionsUrlWithoutParam + $p.t('DMS-Link/lvplanSyncFAQ')"
|
||||
class="fhc-link-color d-flex flex-row gap-2 align-items-center"
|
||||
>
|
||||
<span>
|
||||
<i class="fa-solid fa-up-right-from-square ms-2"></i>
|
||||
</span>
|
||||
<span>
|
||||
{{ $p.t("profil/calendar_sync_instructions") }}
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
v-for="syncUrl in $props.calendarSyncUrls"
|
||||
:key="syncUrl.identifier"
|
||||
@click.prevent="copyUrlToClipboard(syncUrl.url)"
|
||||
@contextmenu.prevent="copyUrlToClipboard(syncUrl.url)"
|
||||
href="#"
|
||||
class="fhc-link-color d-flex flex-row gap-2 align-items-center"
|
||||
>
|
||||
<span>
|
||||
<i class="fa-regular fa-copy ms-2 text-decoration-none"></i>
|
||||
</span>
|
||||
{{ $p.t(syncUrl.labelPhrase) }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
};
|
||||
@@ -67,6 +67,21 @@ export default {
|
||||
return res;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copyFullNameWithTitleToClipboard() {
|
||||
let fragments = [
|
||||
this.$props.data.Titel,
|
||||
this.$props.data.Vorname,
|
||||
this.$props.data.Nachname,
|
||||
this.$props.data.Postnomen,
|
||||
];
|
||||
fragments = fragments.filter((fragment) => !!fragment);
|
||||
navigator.clipboard.writeText(fragments.join(" "));
|
||||
this.$fhcAlert.alertSuccess(
|
||||
this.$p.t("profil/name_title_clipboard_copy_confirmation"),
|
||||
);
|
||||
},
|
||||
},
|
||||
template: /*html*/ `
|
||||
|
||||
<div class="card h-100">
|
||||
@@ -124,6 +139,16 @@ export default {
|
||||
<span class="form-underline-content">{{wert?wert:'-'}} </span>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
@click="copyFullNameWithTitleToClipboard()"
|
||||
href="#"
|
||||
class="col-12 fhc-link-color d-flex flex-row gap-2 align-items-center justify-content-center mt-3"
|
||||
>
|
||||
<span>
|
||||
<i class="fa-regular fa-copy ms-2 text-decoration-none"></i>
|
||||
</span>
|
||||
<span>{{ $p.t("profil/name_title_copy") }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,53 +1,31 @@
|
||||
export default {
|
||||
//TODO: To be implemented
|
||||
props: {
|
||||
data: {
|
||||
type: String,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
mobile: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hideCollapse: function () {
|
||||
this.collapseOpen = false;
|
||||
},
|
||||
showCollapse: function () {
|
||||
this.collapseOpen = true;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
collapseOpen: false,
|
||||
};
|
||||
},
|
||||
template: /*html*/ `
|
||||
name: "QuickLinks",
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
links: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="card">
|
||||
<template v-if="mobile">
|
||||
<button class="btn btn-outline-primary" data-bs-toggle="collapse" data-bs-target="#quickLinks" :aria-expanded="collapseOpen" aria-controls="quickLinks" >
|
||||
{{title}}
|
||||
<i class="fa " :class="collapseOpen?'fa-chevron-up':'fa-chevron-down'"></i>
|
||||
</button>
|
||||
<div @[\`show.bs.collapse\`]="collapseOpen=true;" @[\`hide.bs.collapse\`]="collapseOpen=false;" class="mt-1 collapse" id="quickLinks">
|
||||
<div class="list-group">
|
||||
<a href="#" class="list-group-item list-group-item-action">{{$p.t('profil','zeitwuensche')}}</a>
|
||||
<a href="#" class="list-group-item list-group-item-action">{{$p.t('profil','lehrveranstaltungen')}}</a>
|
||||
<a href="#" class="list-group-item list-group-item-action ">{{$p.t('profil','zeitsperren')}}</a>
|
||||
<div class="card-header">
|
||||
{{title}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-row align-items-center gap-3 flex-wrap">
|
||||
<div v-for="link in links" @click="link.action()" type="button" class="d-flex flex-row gap-2 px-3 py-2 border fhc-primary">
|
||||
<div><i class="fa" :class="link.icon"></i></div>
|
||||
{{ $p.t(link.phrase) }}
|
||||
<div><i class="fa fa-arrow-up-right-from-square" style="color:var(--fhc-light) !important"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="card-header">{{title}}</div>
|
||||
<div class="card-body">
|
||||
<a style="text-decoration:none" class="my-1 d-block" href="#">{{$p.t('profil','zeitwuensche')}}</a>
|
||||
<a style="text-decoration:none" class="my-1 d-block" href="#">{{$p.t('profil','lehrveranstaltungen')}}</a>
|
||||
<a style="text-decoration:none" class="my-1 d-block" href="#">{{$p.t('profil','zeitsperren')}}</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>`,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
|
||||
import Mailverteiler from "./ProfilComponents/Mailverteiler.js";
|
||||
import AusweisStatus from "./ProfilComponents/FhAusweisStatus.js";
|
||||
import QuickLinks from "./ProfilComponents/QuickLinks.js";
|
||||
import Adresse from "./ProfilComponents/Adresse.js";
|
||||
import Kontakt from "./ProfilComponents/Kontakt.js";
|
||||
import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
|
||||
@@ -9,6 +8,8 @@ import RoleInformation from "./ProfilComponents/RoleInformation.js";
|
||||
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
|
||||
import FetchProfilUpdates from "./ProfilComponents/FetchProfilUpdates.js";
|
||||
import EditProfil from "./ProfilModal/EditProfil.js";
|
||||
import QuickLinks from "./ProfilComponents/QuickLinks.js";
|
||||
import CalendarSync from "./ProfilComponents/CalendarSync.js";
|
||||
|
||||
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
|
||||
import { dateFilter } from '../../../tabulator/filters/Dates.js';
|
||||
@@ -18,7 +19,6 @@ export default {
|
||||
CoreFilterCmpt,
|
||||
Mailverteiler,
|
||||
AusweisStatus,
|
||||
QuickLinks,
|
||||
Adresse,
|
||||
Kontakt,
|
||||
ProfilEmails,
|
||||
@@ -26,14 +26,15 @@ export default {
|
||||
ProfilInformation,
|
||||
FetchProfilUpdates,
|
||||
EditProfil,
|
||||
QuickLinks,
|
||||
CalendarSync,
|
||||
},
|
||||
inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"],
|
||||
inject: ["sortProfilUpdates", "collapseFunction", "isEditable"],
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
collapseIconBetriebsmittel: true,
|
||||
editDataFilter: null,
|
||||
preloadedPhrasen:{},
|
||||
// tabulator options
|
||||
zutrittsgruppen_table_options: {
|
||||
persistenceID: "filterTableStudentProfilZutrittsgruppen",
|
||||
@@ -43,9 +44,11 @@ export default {
|
||||
minHeight: 200,
|
||||
layout: "fitColumns",
|
||||
columns: [{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.zutrittsGruppenPhrase),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/zutrittsGruppen",
|
||||
field: "bezeichnung"
|
||||
}],
|
||||
}
|
||||
],
|
||||
},
|
||||
betriebsmittel_table_options: {
|
||||
persistenceID: "filterTableStudentProfilBetriebsmittel",
|
||||
@@ -57,6 +60,8 @@ export default {
|
||||
responsiveLayout: "collapse",
|
||||
responsiveLayoutCollapseUseFormatters: false,
|
||||
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
|
||||
responsiveLayoutCollapseStartOpen: false,
|
||||
locale: true,
|
||||
columns: [
|
||||
{
|
||||
title:
|
||||
@@ -67,31 +72,38 @@ export default {
|
||||
formatter: "responsiveCollapse",
|
||||
maxWidth: 40,
|
||||
headerClick: this.collapseFunction,
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(()=>this.preloadedPhrasen.entlehnteBetriebsmittelPhrase),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/entlehnteBetriebsmittel",
|
||||
field: "betriebsmittel",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() =>this.preloadedPhrasen.inventarnummerPhrase) ,
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/inventarnummer",
|
||||
field: "Nummer",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
visible: true,
|
||||
responsive: 2,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() =>this.preloadedPhrasen.ausgabedatum) ,
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/ausgabedatum",
|
||||
field: "Ausgegeben_am",
|
||||
headerFilterFunc: 'dates',
|
||||
headerFilter: dateFilter,
|
||||
minWidth: 200,
|
||||
visible: true,
|
||||
formatter:"datetime",
|
||||
formatterParams: this.datetimeFormatterParams()
|
||||
formatterParams: this.datetimeFormatterParams(),
|
||||
responsive: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -101,6 +113,7 @@ export default {
|
||||
props: {
|
||||
data: Object,
|
||||
editData: Object,
|
||||
calendarSyncUrls: Array,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
@@ -110,11 +123,9 @@ export default {
|
||||
methods: {
|
||||
|
||||
betriebsmittelTableBuilt: function () {
|
||||
this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns)
|
||||
this.$refs.betriebsmittelTable.tabulator.setData(this.data.mittel);
|
||||
},
|
||||
zutrittsgruppenTableBuilt: function () {
|
||||
this.$refs.zutrittsgruppenTable.tabulator.setColumns(this.zutrittsgruppen_table_options.columns)
|
||||
this.$refs.zutrittsgruppenTable.tabulator.setData(
|
||||
this.data.zuttritsgruppen
|
||||
);
|
||||
@@ -222,6 +233,10 @@ export default {
|
||||
label: `${this.$p.t('person','personenkennzeichen')}`,
|
||||
value: this.data.personenkennzeichen
|
||||
},
|
||||
matrikelnummer: {
|
||||
label: this.$p.t('person/matrikelnummer'),
|
||||
value: this.data.matrikelnummer
|
||||
},
|
||||
studiengang: {
|
||||
label: `${this.$p.t('lehre','studiengang')}`,
|
||||
value: this.data.studiengang
|
||||
@@ -240,40 +255,23 @@ export default {
|
||||
}
|
||||
};
|
||||
},
|
||||
quickLinks() {
|
||||
let quickLinks = [];
|
||||
//
|
||||
return quickLinks;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// preload phrasen
|
||||
this.$p.loadCategory('profil').then(() => {
|
||||
this.preloadedPhrasen.zutrittsGruppenPhrase = this.$p.t('profil/zutrittsGruppen');
|
||||
this.preloadedPhrasen.entlehnteBetriebsmittelPhrase = this.$p.t('profil/entlehnteBetriebsmittel');
|
||||
this.preloadedPhrasen.inventarnummerPhrase = this.$p.t('profil/inventarnummer');
|
||||
this.preloadedPhrasen.ausgabedatum = this.$p.t('profil/ausgabedatum');
|
||||
this.preloadedPhrasen.loaded = true;
|
||||
});
|
||||
//? sorts the profil Updates: pending -> accepted -> rejected
|
||||
this.data.profilUpdates?.sort(this.sortProfilUpdates);
|
||||
},
|
||||
watch: {
|
||||
'language.value'(newVal) {
|
||||
if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns)
|
||||
if(this.$refs.zutrittsgruppenTable) this.$refs.zutrittsgruppenTable.tabulator.setColumns(this.zutrittsgruppen_table_options.columns)
|
||||
}
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="container-fluid text-break fhc-form">
|
||||
<edit-profil v-if="showModal" ref="editModal" @hideBsModal="hideEditProfilModal"
|
||||
:value="JSON.parse(JSON.stringify(filteredEditData))" :titel="$p.t('profil','profilBearbeiten')"></edit-profil>
|
||||
<!-- ROW -->
|
||||
<div class="row">
|
||||
<!-- HIDDEN QUICK LINKS -->
|
||||
<div class="d-md-none col-12 ">
|
||||
<!--TODO: uncomment when implemented
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true"></quick-links>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
<!-- Bearbeiten Button -->
|
||||
<div v-if="isEditable" class="row ">
|
||||
<div class="col mb-3">
|
||||
@@ -287,14 +285,13 @@ export default {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- MOBILE PROFIL UPDATES -->
|
||||
<div v-if="data.profilUpdates" class="row mb-3">
|
||||
<div class="col">
|
||||
<!-- MOBILE PROFIL UPDATES -->
|
||||
<fetch-profil-updates v-if="data.profilUpdates && data.profilUpdates.length" @fetchUpdates="fetchProfilUpdates" :data="data.profilUpdates"></fetch-profil-updates>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END OF HIDDEN QUCK LINKS -->
|
||||
|
||||
<!-- MAIN PANNEL -->
|
||||
<div class="col-sm-12 col-md-8 col-xxl-9 ">
|
||||
@@ -303,30 +300,42 @@ export default {
|
||||
<!-- ROW WITH THE PROFIL INFORMATION -->
|
||||
<div class="row mb-4 ">
|
||||
<div class="col-lg-12 col-xl-6 ">
|
||||
<!-- PROFIL INFORMATION -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- PROFIL INFORMATION -->
|
||||
<profil-information @showEditProfilModal="showEditProfilModal" :title="$p.t('profil','studentIn')" :data="profilInformation" :fotoStatus="fotoStatus"></profil-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- QUICK LINKS, MOBILE VIEW (HIDDEN IF VIEWPORT >= MD BREAKPOINT) -->
|
||||
<div v-if="quickLinks.length" class="row mb-4 d-md-none">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil/quickLinks')" :links="quickLinks" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- CALENDAR SYNC OPTIONS, MOBILE VIEW (HIDDEN IF VIEWPORT >= MD BREAKPOINT) -->
|
||||
<div class="row mb-4 d-md-none">
|
||||
<div class="col">
|
||||
<calendar-sync :uid="$props.data.username" :calendarSyncUrls="$props.calendarSyncUrls"></calendar-sync>
|
||||
</div>
|
||||
</div>
|
||||
<!-- STUDENT INFO -->
|
||||
<div class="row mb-4">
|
||||
<div class=" col-lg-12">
|
||||
<!-- STUDENT INFO -->
|
||||
<role-information :title="$p.t('profil','studentInformation')" :data="roleInformation"></role-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
|
||||
</div>
|
||||
<div class="col-xl-6 col-lg-12 ">
|
||||
<!-- EMAILS -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- EMAILS -->
|
||||
<profil-emails :title="this.$p.t('person','email')" :data="data.emails" ></profil-emails>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PRIVATE KONTAKTE-->
|
||||
<div class="row mb-4 ">
|
||||
<div class="col">
|
||||
<!-- PRIVATE KONTAKTE-->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
@@ -349,9 +358,9 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PRIVATE ADRESSEN-->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- PRIVATE ADRESSEN-->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
@@ -379,7 +388,6 @@ export default {
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4" >
|
||||
<core-filter-cmpt
|
||||
v-if="preloadedPhrasen.loaded"
|
||||
@tableBuilt="betriebsmittelTableBuilt"
|
||||
:title="$p.t('profil','entlehnteBetriebsmittel')"
|
||||
ref="betriebsmittelTable"
|
||||
@@ -389,7 +397,6 @@ export default {
|
||||
</div>
|
||||
<div class="col-12 mb-4" >
|
||||
<core-filter-cmpt
|
||||
v-if="preloadedPhrasen.loaded"
|
||||
@tableBuilt="zutrittsgruppenTableBuilt"
|
||||
:title="$p.t('profil','zutrittsGruppen')"
|
||||
ref="zutrittsgruppenTable"
|
||||
@@ -403,12 +410,6 @@ export default {
|
||||
</div>
|
||||
<!-- START OF SIDE PANEL -->
|
||||
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
|
||||
<!--TODO: uncomment when implemented
|
||||
<div class="row d-none d-md-block mb-3">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
|
||||
</div>
|
||||
</div>-->
|
||||
<!-- Bearbeiten Button -->
|
||||
<div class="row d-none d-md-block">
|
||||
<div class="col mb-3">
|
||||
@@ -422,9 +423,21 @@ export default {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- QUICK LINKS, HIDDEN IF VIEWPORT < MD BREAKPOINT -->
|
||||
<div v-if="quickLinks.length" class="row mb-3 d-none d-md-block">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil/quickLinks')" :links="quickLinks" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- CALENDAR SYNC OPTIONS, HIDDEN IF VIEWPORT < MD BREAKPOINT -->
|
||||
<div class="row mb-3 d-none d-md-block">
|
||||
<div class="col">
|
||||
<calendar-sync :uid="$props.data.username" :calendarSyncUrls="$props.calendarSyncUrls"></calendar-sync>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PROFIL UPDATES -->
|
||||
<div v-if="data.profilUpdates" class="row d-none d-md-block mb-3">
|
||||
<div class="col mb-3">
|
||||
<!-- PROFIL UPDATES -->
|
||||
<fetch-profil-updates v-if="data.profilUpdates && data.profilUpdates.length" @fetchUpdates="fetchProfilUpdates" :data="data.profilUpdates"></fetch-profil-updates>
|
||||
</div>
|
||||
</div>
|
||||
@@ -434,13 +447,13 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF THE SECOND ROW IN THE SIDE PANEL -->
|
||||
<div class="row">
|
||||
<!-- MAILVERTEILER -->
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<!-- HIER SIND DIE MAILVERTEILER -->
|
||||
<mailverteiler :title="$p.t('profil','mailverteiler')" :data="data?.mailverteiler"></mailverteiler>
|
||||
</div>
|
||||
<!-- END OF THE SECOND ROW IN THE SIDE PANEL -->
|
||||
</div>
|
||||
<!-- END OF THE SECOND ROW IN THE SIDE PANEL -->
|
||||
<!-- END OF SIDE PANEL -->
|
||||
</div>
|
||||
<!-- END OF CONTAINER ROW-->
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
import QuickLinks from "./ProfilComponents/QuickLinks.js";
|
||||
import Mailverteiler from "./ProfilComponents/Mailverteiler.js";
|
||||
import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
|
||||
import RoleInformation from "./ProfilComponents/RoleInformation.js";
|
||||
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
|
||||
import QuickLinks from "./ProfilComponents/QuickLinks.js";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
components: {
|
||||
QuickLinks,
|
||||
Mailverteiler,
|
||||
ProfilEmails,
|
||||
RoleInformation,
|
||||
ProfilInformation,
|
||||
QuickLinks,
|
||||
},
|
||||
props: ["data", "permissions"],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
|
||||
props: ["data"],
|
||||
provide() {
|
||||
return {
|
||||
studiengang_kz: Vue.computed({ get: () => this.data.studiengang_kz }),
|
||||
}
|
||||
studiengang_kz: Vue.computed({
|
||||
get: () => this.data.studiengang_kz,
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
||||
methods: {},
|
||||
|
||||
computed: {
|
||||
fotoStatus() {
|
||||
return this.data?.fotoStatus ?? null;
|
||||
@@ -45,66 +43,71 @@ export default {
|
||||
foto: this.data.foto,
|
||||
};
|
||||
},
|
||||
|
||||
personEmails() {
|
||||
return this.data?.emails ? this.data.emails : [];
|
||||
},
|
||||
|
||||
roleInformation() {
|
||||
if (!this.data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
geburtsdatum: {
|
||||
label: `${this.$p.t('profil','Geburtsdatum')}`,
|
||||
value: this.data.gebdatum
|
||||
label: `${this.$p.t("profil", "Geburtsdatum")}`,
|
||||
value: this.data.gebdatum,
|
||||
},
|
||||
geburtsort: {
|
||||
label: `${this.$p.t('profil','Geburtsort')}`,
|
||||
value: this.data.gebort
|
||||
label: `${this.$p.t("profil", "Geburtsort")}`,
|
||||
value: this.data.gebort,
|
||||
},
|
||||
personenkennzeichen: {
|
||||
label: `${this.$p.t('person','personenkennzeichen')}`,
|
||||
value: this.data.personenkennzeichen
|
||||
label: `${this.$p.t("person", "personenkennzeichen")}`,
|
||||
value: this.data.personenkennzeichen,
|
||||
},
|
||||
matrikelnummer: {
|
||||
label: this.$p.t('person/matrikelnummer'),
|
||||
value: this.data.matrikelnummer
|
||||
},
|
||||
studiengang: {
|
||||
label: `${this.$p.t('lehre','studiengang')}`,
|
||||
value: this.data.studiengang
|
||||
label: `${this.$p.t("lehre", "studiengang")}`,
|
||||
value: this.data.studiengang,
|
||||
},
|
||||
semester: {
|
||||
label: `${this.$p.t('lehre','semester')}`,
|
||||
value: this.data.semester
|
||||
label: `${this.$p.t("lehre", "semester")}`,
|
||||
value: this.data.semester,
|
||||
},
|
||||
verband: {
|
||||
label: `${this.$p.t('lehre','lehrverband')}`,
|
||||
value: this.data.verband
|
||||
label: `${this.$p.t("lehre", "lehrverband")}`,
|
||||
value: this.data.verband,
|
||||
},
|
||||
gruppe: {
|
||||
label: `${this.$p.t('lehre','gruppe')}`,
|
||||
value: this.data.gruppe.trim()
|
||||
}
|
||||
label: `${this.$p.t("lehre", "gruppe")}`,
|
||||
value: this.data.gruppe.trim(),
|
||||
},
|
||||
};
|
||||
},
|
||||
quickLinks() {
|
||||
let quickLinks = [];
|
||||
if (this.$props.permissions && this.$props.permissions["basis/other_lv_plan"]) {
|
||||
quickLinks.push({
|
||||
icon: "fa-calendar-days",
|
||||
phrase: "lehre/stundenplan",
|
||||
action: () => {
|
||||
this.$router.push({
|
||||
name: "OtherLvPlan",
|
||||
params: { otherUid: this.$props.data.username },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
return quickLinks;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
},
|
||||
|
||||
template: /*html*/ `
|
||||
|
||||
<div class="container-fluid text-break fhc-form" >
|
||||
<!-- ROW -->
|
||||
<div class="row">
|
||||
<!-- HIDDEN QUICK LINKS -->
|
||||
<!-- uncomment when implemented
|
||||
<div class="d-md-none col-12 ">
|
||||
|
||||
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true"></quick-links>
|
||||
|
||||
</div>-->
|
||||
<!-- END OF HIDDEN QUCK LINKS -->
|
||||
<!-- MAIN PANNEL -->
|
||||
<div class="col-sm-12 col-md-8 col-xxl-9 ">
|
||||
<!-- ROW WITH PROFIL IMAGE AND INFORMATION -->
|
||||
@@ -112,12 +115,18 @@ export default {
|
||||
<!-- ROW WITH THE PROFIL INFORMATION -->
|
||||
<div class="row mb-4">
|
||||
<!-- FIRST KAESTCHEN -->
|
||||
<div class="col-lg-12 col-xl-6 ">
|
||||
<div class="col-lg-12 col-xl-6 ">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<profil-information :data="profilInformation" :title="$p.t('profil','studentIn')" :fotoStatus="fotoStatus"></profil-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- QUICK LINKS, MOBILE VIEW (HIDDEN IF VIEWPORT >= MD BREAKPOINT) -->
|
||||
<div v-if="quickLinks.length" class="row mb-4 d-md-none">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil/quickLinks')" :links="quickLinks" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
|
||||
<!-- END OF PROFIL INFORMATION ROW -->
|
||||
<!-- INFORMATION CONTENT END -->
|
||||
@@ -145,17 +154,13 @@ export default {
|
||||
</div>
|
||||
<!-- START OF SIDE PANEL -->
|
||||
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
|
||||
<!-- SRART OF QUICK LINKS IN THE SIDE PANEL -->
|
||||
<!-- START OF THE FIRDT ROW IN THE SIDE PANEL -->
|
||||
<!-- THESE QUCK LINKS ARE ONLY VISIBLE UNTIL VIEWPORT MD -->
|
||||
<!--TODO: uncomment when implemented
|
||||
<div class="row d-none d-md-block mb-3">
|
||||
<div class="col">
|
||||
|
||||
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
|
||||
|
||||
</div>
|
||||
</div>-->
|
||||
<!-- START OF THE FIRST ROW IN THE SIDE PANEL -->
|
||||
<!-- QUICK LINKS, HIDDEN IF VIEWPORT < MD BREAKPOINT -->
|
||||
<div v-if="quickLinks.length" class="row mb-3 d-none d-md-block">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil/quickLinks')" :links="quickLinks" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF THE SECOND ROW IN THE SIDE PANEL -->
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
|
||||
@@ -3,14 +3,12 @@ import VueDatePicker from '../../vueDatepicker.js.php';
|
||||
import ApiOrt from '../../../api/factory/ort.js'
|
||||
export const Raumsuche = {
|
||||
name: "Raumsuche",
|
||||
props: {
|
||||
|
||||
},
|
||||
components: {
|
||||
VueDatePicker,
|
||||
CoreFilterCmpt,
|
||||
InputNumber: primevue.inputnumber,
|
||||
},
|
||||
inject: ["isMobile"],
|
||||
data() {
|
||||
return {
|
||||
phrasenPromise: null,
|
||||
@@ -73,6 +71,11 @@ export const Raumsuche = {
|
||||
}
|
||||
]};
|
||||
},
|
||||
computed: {
|
||||
isDarkMode(){
|
||||
return this.$theme.theme_name.value == 'dark';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tableResolve(resolve) {
|
||||
this.tableBuiltResolve = resolve
|
||||
@@ -178,11 +181,6 @@ export const Raumsuche = {
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isDarkMode(){
|
||||
return this.$theme.theme_name.value == 'dark';
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.phrasenPromise = this.$p.loadCategory(['rauminfo', 'global'])
|
||||
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
|
||||
@@ -194,46 +192,50 @@ export const Raumsuche = {
|
||||
<h1 class="h3">{{$p.t('rauminfo/roomSearch')}}</h1>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-2">
|
||||
<div :class="{'pb-1': isMobile}" class="col-12 col-lg-2">
|
||||
<VueDatePicker
|
||||
:dark="isDarkMode"
|
||||
@contextmenu="(e) => {if (isMobile) {e.preventDefault();}}"
|
||||
v-model="datum"
|
||||
:dark="isDarkMode"
|
||||
:clearable="false"
|
||||
date-picker
|
||||
:enable-time-picker="false"
|
||||
:format="dateFormat"
|
||||
:text-input="datepickerTextInputOptions"
|
||||
:min-date="new Date()"
|
||||
auto-apply>
|
||||
</VueDatePicker>
|
||||
</div>
|
||||
<div class="col-12 col-lg-1">
|
||||
<VueDatePicker
|
||||
:dark="isDarkMode"
|
||||
v-model="von"
|
||||
:clearable="false"
|
||||
time-picker
|
||||
:format="timeFormat"
|
||||
:text-input="timepickerTextInputOptions"
|
||||
:is-24="true"
|
||||
date-picker
|
||||
auto-apply
|
||||
>
|
||||
</VueDatePicker>
|
||||
</div>
|
||||
<div class="col-12 col-lg-1">
|
||||
<div :class="{'pb-1': isMobile}" class="col-12 col-lg-1">
|
||||
<VueDatePicker
|
||||
@contextmenu="(e) => {if (isMobile) {e.preventDefault();}}"
|
||||
v-model="von"
|
||||
:dark="isDarkMode"
|
||||
v-model="bis"
|
||||
:clearable="false"
|
||||
time-picker
|
||||
:format="timeFormat"
|
||||
:text-input="timepickerTextInputOptions"
|
||||
:is-24="true"
|
||||
time-picker
|
||||
auto-apply
|
||||
>
|
||||
</VueDatePicker>
|
||||
</div>
|
||||
<div :class="{'pb-1': isMobile}" class="col-12 col-lg-1">
|
||||
<VueDatePicker
|
||||
@contextmenu="(e) => {if (isMobile) {e.preventDefault();}}"
|
||||
v-model="bis"
|
||||
:dark="isDarkMode"
|
||||
:clearable="false"
|
||||
:format="timeFormat"
|
||||
:text-input="timepickerTextInputOptions"
|
||||
:is-24="true"
|
||||
time-picker
|
||||
auto-apply>
|
||||
</VueDatePicker>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-3">
|
||||
<div :class="{'pb-1': isMobile}" class="col-12 col-lg-3">
|
||||
<select ref="raumtyp" id="raumtypSelect" v-model="selectedType" class="form-select"
|
||||
:aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setRoute($event.target.value)">
|
||||
<option :key="defaultType" selected :value="defaultType">{{defaultType.beschreibung}}</option>
|
||||
@@ -242,7 +244,7 @@ export const Raumsuche = {
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 col-lg-3">
|
||||
<div :class="{'pb-2': isMobile}" class="col-12 col-lg-3">
|
||||
<InputNumber v-model="anzahl"
|
||||
:prefix="$p.t('rauminfo/minCapacity') + ': '"
|
||||
inputId="anzahlInput" :min="1" :max="1000"
|
||||
|
||||
@@ -3,6 +3,11 @@ export default {
|
||||
event: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
timeSlotDisplayBehavior: {
|
||||
type: String,
|
||||
default: "default",
|
||||
// options: default, always, never
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
@@ -31,7 +36,7 @@ export default {
|
||||
this.event.lektor.slice(0, 3).map(lektor => lektor.kurzbz).join("\n")
|
||||
+ "\n" + this.$p.t('lehre/weitereLektoren', [this.event.lektor.length - 3])
|
||||
].join(": "));
|
||||
} else {;
|
||||
} else {
|
||||
tooltipArray.push([
|
||||
this.$p.t('lehre/lektor'),
|
||||
this.event.lektor.map(lektor => lektor.kurzbz).join("\n")
|
||||
@@ -50,7 +55,17 @@ export default {
|
||||
return luxon.Duration
|
||||
.fromISOTime(this.event.ende)
|
||||
.toISOTime({ suppressSeconds: true });
|
||||
}
|
||||
},
|
||||
timeSlotDisplayClasses() {
|
||||
switch (this.$props.timeSlotDisplayBehavior) {
|
||||
case "always":
|
||||
return "d-grid";
|
||||
case "never":
|
||||
return "d-none";
|
||||
default:
|
||||
return "d-none d-xl-grid";
|
||||
}
|
||||
},
|
||||
},
|
||||
template: /*html*/`
|
||||
<div
|
||||
@@ -58,8 +73,9 @@ export default {
|
||||
@wheel.stop
|
||||
>
|
||||
<div
|
||||
v-if="!event.allDayEvent && event?.beginn && event?.ende"
|
||||
class="event-time d-none d-xl-grid h-100"
|
||||
v-if="!event?.allDayEvent && event?.beginn && event?.ende"
|
||||
:class="timeSlotDisplayClasses"
|
||||
class="event-time h-100"
|
||||
>
|
||||
<span>{{ start }}</span>
|
||||
<span>{{ end }}</span>
|
||||
|
||||
@@ -51,6 +51,16 @@ export default {
|
||||
return numberPadding(this.event.ende.getHours()) + ":" + numberPadding(this.event.ende.getMinutes());
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
event: {
|
||||
async handler() {
|
||||
if (this.$props.event.type == "lehreinheit") {
|
||||
await this.getLvMenu();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
mehtodNumberPadding: function (number) {
|
||||
return numberPadding(number);
|
||||
@@ -58,21 +68,31 @@ export default {
|
||||
methodFormatDate: function (d) {
|
||||
return formatDate(d);
|
||||
},
|
||||
async getLvMenu() {
|
||||
const semesterResponse = await this.$api.call(
|
||||
ApiLvPlan.getLehreinheitStudiensemester(
|
||||
Array.isArray(this.event.lehreinheit_id)
|
||||
? this.event.lehreinheit_id[0]
|
||||
: this.event.lehreinheit_id,
|
||||
),
|
||||
);
|
||||
if (semesterResponse.meta.status !== "success") return;
|
||||
|
||||
const studiensemesterKurzbz = semesterResponse.data;
|
||||
const lvMenuResponse = await this.$api.call(
|
||||
ApiAddons.getLvMenu(
|
||||
this.event.lehrveranstaltung_id,
|
||||
studiensemesterKurzbz,
|
||||
),
|
||||
);
|
||||
if (lvMenuResponse.meta.status !== "success") return;
|
||||
|
||||
this.lvMenu = lvMenuResponse.data;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.event.type == 'lehreinheit') {
|
||||
this.$api
|
||||
.call(ApiLvPlan.getLehreinheitStudiensemester(Array.isArray(this.event.lehreinheit_id) ? this.event.lehreinheit_id[0] : this.event.lehreinheit_id))
|
||||
.then(res => res.data)
|
||||
.then(studiensemester_kurzbz => this.$api.call(
|
||||
ApiAddons.getLvMenu(
|
||||
this.event.lehrveranstaltung_id,
|
||||
studiensemester_kurzbz
|
||||
)
|
||||
))
|
||||
.then(res => {
|
||||
this.lvMenu = res.data;
|
||||
});
|
||||
async created() {
|
||||
if (this.$props.event.type == 'lehreinheit') {
|
||||
await this.getLvMenu();
|
||||
}
|
||||
},
|
||||
template: `
|
||||
@@ -142,7 +162,6 @@ export default {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<lv-menu :containerStyles="['p-0']" :rowStyles="['m-0']" v-if="lvMenu.length" :menu="lvMenu" />
|
||||
<lv-menu v-if="lvMenu.length && $route.name === 'MyLvPlan'" :containerStyles="['p-0']" :rowStyles="['m-0']" :menu="lvMenu" />
|
||||
</div>`,
|
||||
}
|
||||
@@ -3,6 +3,11 @@ export default {
|
||||
event: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
timeSlotDisplayBehavior: {
|
||||
type: String,
|
||||
default: "default",
|
||||
// options: default, always, never
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -50,21 +55,33 @@ export default {
|
||||
return luxon.Duration
|
||||
.fromISOTime(this.event.ende)
|
||||
.toISOTime({ suppressSeconds: true });
|
||||
}
|
||||
},
|
||||
timeSlotDisplayClasses() {
|
||||
switch (this.$props.timeSlotDisplayBehavior) {
|
||||
case "always":
|
||||
return "d-grid";
|
||||
case "never":
|
||||
return "d-none";
|
||||
default:
|
||||
return "d-none d-xl-grid";
|
||||
}
|
||||
},
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="cis-renderer-reservierungen-calendar-event calendar-event-default h-100 w-100 p-1"
|
||||
>
|
||||
<div
|
||||
v-if="!event.allDayEvent && event?.beginn && event?.ende"
|
||||
class="event-time d-grid h-100"
|
||||
v-if="!event?.allDayEvent && event?.beginn && event?.ende"
|
||||
:class="timeSlotDisplayClasses"
|
||||
class="event-time h-100"
|
||||
>
|
||||
<span>{{ start }}</span>
|
||||
<span>{{ end }}</span>
|
||||
</div>
|
||||
<div class="event-text" v-tooltip="tooltipString">
|
||||
<span class="event-topic">{{ event.topic }}</span>
|
||||
<span class="event-place">{{ event.ort_kurzbz }}</span>
|
||||
<span
|
||||
v-for="lektor in event.lektor.slice(0, 3)"
|
||||
class="event-lectors"
|
||||
@@ -77,7 +94,6 @@ export default {
|
||||
>
|
||||
... +{{ event.lektor.length - 3 }}
|
||||
</span>
|
||||
<span class="event-place">{{ event.ort_kurzbz }}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import LvUebersicht from "../Mylv/LvUebersicht.js";
|
||||
|
||||
import ApiCisStudium from '../../../api/factory/cis/studium.js';
|
||||
|
||||
export default {
|
||||
data(){
|
||||
@@ -26,6 +27,7 @@ export default {
|
||||
|
||||
}
|
||||
},
|
||||
name: "OverviewStudiengaenge",
|
||||
components: {
|
||||
LvUebersicht,
|
||||
},
|
||||
@@ -88,6 +90,27 @@ export default {
|
||||
studienordnung.selectedIndex = newSelectIndex;
|
||||
this.changeSelectedStudienPlan(studienordnung.value);
|
||||
},
|
||||
onStudiensemesterChange(event) {
|
||||
const value = event.target.value;
|
||||
this.setHash(value);
|
||||
this.changeSelectedStudienSemester(value);
|
||||
|
||||
},
|
||||
onStudiengangChange(event) {
|
||||
const value = event.target.value;
|
||||
this.setHash(value);
|
||||
this.changeSelectedStudienGang(value);
|
||||
},
|
||||
onSemesterChange(event) {
|
||||
const value = event.target.value;
|
||||
this.setHash(value);
|
||||
this.changeSelectedSemester(value);
|
||||
},
|
||||
onStudienordnungChange(event) {
|
||||
const value = event.target.value;
|
||||
this.setHash(value);
|
||||
this.changeSelectedStudienPlan(value);
|
||||
},
|
||||
|
||||
storeDataToLocalStorage(key,value){
|
||||
localStorage.setItem(key, value);
|
||||
@@ -97,28 +120,32 @@ export default {
|
||||
return value;
|
||||
},
|
||||
changeSelectedStudienSemester(studiensemester_kurzbz) {
|
||||
this.$fhcApi.factory.studium.getAllStudienSemester(studiensemester_kurzbz, this.selectedStudiengang, this.selectedSemester, this.selectedStudienordnung)
|
||||
return this.$api
|
||||
.call(ApiCisStudium.getAllStudienSemester(studiensemester_kurzbz, this.selectedStudiengang, this.selectedSemester, this.selectedStudienordnung))
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
this.extractPropertyValues(res);
|
||||
})
|
||||
},
|
||||
changeSelectedStudienGang(studiengang_kz) {
|
||||
this.$fhcApi.factory.studium.getAllStudienSemester(this.selectedStudiensemester, studiengang_kz, this.selectedSemester, this.selectedStudienordnung)
|
||||
return this.$api
|
||||
.call(ApiCisStudium.getAllStudienSemester(this.selectedStudiensemester, studiengang_kz, this.selectedSemester, this.selectedStudienordnung))
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
this.extractPropertyValues(res);
|
||||
})
|
||||
},
|
||||
changeSelectedSemester(semester) {
|
||||
this.$fhcApi.factory.studium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, semester, this.selectedStudienordnung)
|
||||
return this.$api
|
||||
.call(ApiCisStudium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, semester, this.selectedStudienordnung))
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
this.extractPropertyValues(res);
|
||||
})
|
||||
},
|
||||
changeSelectedStudienPlan(studienplan_id) {
|
||||
this.$fhcApi.factory.studium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, this.selectedSemester, studienplan_id)
|
||||
return this.$api
|
||||
.call(ApiCisStudium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, this.selectedSemester, studienplan_id))
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
this.extractPropertyValues(res);
|
||||
@@ -160,6 +187,7 @@ export default {
|
||||
this.studiengaenge = studiengang.all;
|
||||
this.selectedStudiengang = studiengang.preselected?.studiengang_kz;
|
||||
|
||||
|
||||
this.semester = semester.all;
|
||||
this.selectedSemester = semester?.preselected;
|
||||
|
||||
@@ -178,6 +206,8 @@ export default {
|
||||
});
|
||||
|
||||
this.lehrveranstaltungen.forEach((lehrveranstaltung)=>{
|
||||
// a lehrveranstaltung without a modul has no children
|
||||
if(!Array.isArray(lehrveranstaltung.lehrveranstaltungen)) return;
|
||||
lehrveranstaltung.lehrveranstaltungen.sort((lv1,lv2)=>{
|
||||
if (lv1.bezeichnung.toLowerCase() > lv2.bezeichnung.toLowerCase()) {
|
||||
return 1;
|
||||
@@ -195,7 +225,11 @@ export default {
|
||||
},
|
||||
studiengangTitel(studiengang) {
|
||||
if (!studiengang) return "";
|
||||
return `${studiengang?.kurzbzlang} (${studiengang?.bezeichnung})`;
|
||||
if(this.isGermanLanguage){
|
||||
return `${studiengang?.kurzbzlang} (${studiengang?.bezeichnung})`;
|
||||
}else{
|
||||
return `${studiengang?.kurzbzlang} (${studiengang?.english})`;
|
||||
}
|
||||
},
|
||||
studiensemesterTitel(studiensemester){
|
||||
if (!studiensemester) return "";
|
||||
@@ -213,9 +247,12 @@ export default {
|
||||
},
|
||||
|
||||
computed:{
|
||||
isGermanLanguage(){
|
||||
return this.$p.user_language.value == "German"
|
||||
},
|
||||
selectedLehrveranstaltungTitel(){
|
||||
const studiengang = this.studiengaenge.find((studiengang) => studiengang.studiengang_kz == this.selectedStudiengang);
|
||||
return `${this.selectedLehrveranstaltung?.bezeichnung} ${this.selectedLehrveranstaltung?.lehrform_kurzbz} / ${studiengang.kurzbzlang}-${this.selectedSemester} ${this.selectedLehrveranstaltung?.orgform_kurzbz} (${this.selectedStudiensemester})`;
|
||||
return `${this.isGermanLanguage ? this.selectedLehrveranstaltung?.bezeichnung : this.selectedLehrveranstaltung?.bezeichnung_english} ${this.selectedLehrveranstaltung?.lehrform_kurzbz} / ${studiengang.kurzbzlang}-${this.selectedSemester} ${this.selectedLehrveranstaltung?.orgform_kurzbz} (${this.selectedStudiensemester})`;
|
||||
},
|
||||
computedStudienOrdnung(){
|
||||
if(!this.studienOrdnung) return null;
|
||||
@@ -232,14 +269,14 @@ export default {
|
||||
let result = [];
|
||||
Object.entries(this.computedStudienOrdnung).forEach(([key,value])=>{
|
||||
result.push({
|
||||
bezeichnung: `Studienordnung: ${key}`,
|
||||
bezeichnung: `${this.$p.t('studium', 'studienordnung') }: ${key}`,
|
||||
disabled: true,
|
||||
});
|
||||
value.forEach((studienplan)=>{
|
||||
result.push({
|
||||
studienplan:studienplan,
|
||||
diabled: false,
|
||||
bezeichnung: `${studienplan?.bezeichnung}-${studienplan?.orgform_kurzbz} ( ${studienplan?.orgform_bezeichnung}, ${studienplan?.sprache} )`
|
||||
bezeichnung: `${studienplan?.bezeichnung}-${studienplan?.orgform_kurzbz} ( ${this.isGermanLanguage ? studienplan?.orgform_bezeichnung : studienplan?.orgform_bezeichnung_english}, ${studienplan?.sprache} )`
|
||||
|
||||
});
|
||||
})
|
||||
@@ -256,22 +293,23 @@ export default {
|
||||
const studienordnung = JSON.parse(this.getDataFromLocalStorage("studienordnung")) ?? undefined;
|
||||
|
||||
// only fetch default data if no data is stored in the local storage
|
||||
|
||||
this.$fhcApi.factory.studium.getAllStudienSemester(studiensemester, studiengang, semester, studienordnung)
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
this.extractPropertyValues(res);
|
||||
})
|
||||
|
||||
this.$api
|
||||
.call(ApiCisStudium.getAllStudienSemester(studiensemester, studiengang, semester, studienordnung))
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
this.extractPropertyValues(res);
|
||||
})
|
||||
|
||||
},
|
||||
template: `
|
||||
template: /*html*/`
|
||||
<div>
|
||||
<h2>Studium</h2>
|
||||
<h2>{{$p.t('studium','studium')}}</h2>
|
||||
<hr>
|
||||
<lv-uebersicht ref="lvUebersicht" :titel="selectedLehrveranstaltungTitel" :event="selectedLehrveranstaltung" :studiensemester="selectedStudiensemester" v-if="selectedLehrveranstaltung">
|
||||
<template #content>
|
||||
<div v-if="Array.isArray(selectedLehrveranstaltung.lektoren) && selectedLehrveranstaltung.lektoren.length>0" class="mb-4">
|
||||
<h4>Lektoren:</h4>
|
||||
<h4>{{$p.t('studium','lektoren')}}:</h4>
|
||||
<a :href="'mailto:'+lektor?.email" class="fhc-link-color mx-2" v-for="lektor in selectedLehrveranstaltung.lektoren">{{lektor.name}}</a>
|
||||
</div>
|
||||
<h4>Menu:</h4>
|
||||
@@ -279,13 +317,13 @@ export default {
|
||||
</lv-uebersicht>
|
||||
<div class="lvOptions">
|
||||
<div>
|
||||
<h6>Studiensemester:</h6>
|
||||
<h6>{{$p.t('studium','studiensemester')}}:</h6>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiensemester(1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')">
|
||||
<i class="fa fa-caret-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<select ref="studiensemester" v-model="selectedStudiensemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)">
|
||||
<option v-for="semester in studienSemester" @click="changeSelectedStudienSemester(semester.studiensemester_kurzbz)" :key="semester" :value="semester.studiensemester_kurzbz">{{studiensemesterTitel(semester.studiensemester_kurzbz) }}</option>
|
||||
<select ref="studiensemester" v-model="selectedStudiensemester" @change="onStudiensemesterChange" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')">
|
||||
<option v-for="semester in studienSemester" :key="semester" :value="semester.studiensemester_kurzbz">{{studiensemesterTitel(semester.studiensemester_kurzbz) }}</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiensemester(-1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')">
|
||||
<i class="fa fa-caret-right" aria-hidden="true"></i>
|
||||
@@ -294,13 +332,13 @@ export default {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>Studiengang:</h6>
|
||||
<h6>{{$p.t('lehre','studiengang')}}:</h6>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiengang(-1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')">
|
||||
<i class="fa fa-caret-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<select ref="studiengaenge" v-model="selectedStudiengang" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)">
|
||||
<option v-for="studiengang in studiengaenge" @click="changeSelectedStudienGang(studiengang.studiengang_kz)" :key="studiengang.studiengang_kz" :value="studiengang.studiengang_kz" >{{studiengangTitel(studiengang)}}</option>
|
||||
<select ref="studiengaenge" v-model="selectedStudiengang" class="form-select" @change="onStudiengangChange" :aria-label="$p.t('global/studiensemester_auswaehlen')">
|
||||
<option v-for="studiengang in studiengaenge" :key="studiengang.studiengang_kz" :value="studiengang.studiengang_kz" >{{studiengangTitel(studiengang)}}</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiengang(1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')">
|
||||
<i class="fa fa-caret-right" aria-hidden="true"></i>
|
||||
@@ -309,13 +347,13 @@ export default {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>Semester:</h6>
|
||||
<h6>{{$p.t('lehre','semester')}}:</h6>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeSemester(-1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')">
|
||||
<i class="fa fa-caret-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<select ref="semester" v-model="selectedSemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)">
|
||||
<option v-for="sem in semester" @click="changeSelectedSemester(sem)" :key="semester" :value="sem">{{sem}}. Semester</option>
|
||||
<select ref="semester" v-model="selectedSemester" class="form-select" @change="onSemesterChange" :aria-label="$p.t('global/studiensemester_auswaehlen')">
|
||||
<option v-for="sem in semester" :key="sem" :value="sem">{{sem}}. Semester</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeSemester(1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')">
|
||||
<i class="fa fa-caret-right" aria-hidden="true"></i>
|
||||
@@ -324,13 +362,13 @@ export default {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>Studienordnung:</h6>
|
||||
<h6>{{$p.t('studium','studienordnung')}}:</h6>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudienordnung(-1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')">
|
||||
<i class="fa fa-caret-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<select ref="studienordnung" v-model="selectedStudienordnung" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)">
|
||||
<option v-for="ordnung in computedStudienOrdnungSelectValues" :disabled="ordnung.disabled" @click="changeSelectedStudienPlan(ordnung?.studienplan?.studienplan_id)" :key="ordnung?.studienplan?.bezeichnung " :value="ordnung?.studienplan?.studienplan_id">{{ordnung.bezeichnung}}</option>
|
||||
<select ref="studienordnung" v-model="selectedStudienordnung" class="form-select" @change="onStudienordnungChange" :aria-label="$p.t('global/studiensemester_auswaehlen')">
|
||||
<option v-for="ordnung in computedStudienOrdnungSelectValues" :disabled="ordnung.disabled" :key="ordnung?.studienplan?.studienplan_id" :value="ordnung?.studienplan?.studienplan_id">{{ordnung.bezeichnung}}</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudienordnung(1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')">
|
||||
<i class="fa fa-caret-right" aria-hidden="true"></i>
|
||||
@@ -345,18 +383,28 @@ export default {
|
||||
<template v-for="lehrveranstaltung in lehrveranstaltungen" :key="lehrveranstaltung.lehrveranstaltung_id">
|
||||
<div class="card" v-if="Array.isArray(lehrveranstaltung.lehrveranstaltungen) && lehrveranstaltung.lehrveranstaltungen.length >0" >
|
||||
<div class="card-header">
|
||||
<h5 class=" card-title">{{lehrveranstaltung.bezeichnung}}</h5>
|
||||
<h5 class=" card-title">{{isGermanLanguage ? lehrveranstaltung.bezeichnung : lehrveranstaltung.bezeichnung_english }}</h5>
|
||||
<h6 class=" card-subtitle">{{lehrveranstaltung.lehrform_kurzbz}}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="d-flex list-group-item" v-for="lv in lehrveranstaltung.lehrveranstaltungen">
|
||||
<a class="fhc-link-color d-block me-auto" href="#" @click="openLvUebersicht(lv)">{{lv.bezeichnung}}</a>
|
||||
<a class="fhc-link-color d-block me-auto" href="#" @click="openLvUebersicht(lv)">{{isGermanLanguage ? lv.bezeichnung : lv.bezeichnung_english}}</a>
|
||||
<p>{{lv.lehrform_kurzbz}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" v-else-if="!Array.isArray(lehrveranstaltung.lehrveranstaltungen)">
|
||||
<div class="card-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="d-flex list-group-item">
|
||||
<a class="fhc-link-color d-block me-auto" href="#" @click="openLvUebersicht(lehrveranstaltung)">{{isGermanLanguage ? lehrveranstaltung.bezeichnung : lehrveranstaltung.bezeichnung_english}}</a>
|
||||
<p>{{lehrveranstaltung.lehrform_kurzbz}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import DashboardAdminWidgets from "./Admin/Widgets.js";
|
||||
import DashboardAdminPresets from "./Admin/Presets.js";
|
||||
|
||||
import ApiDashboardBoard from "../../api/factory/dashboard/board.js";
|
||||
import ApiDashboardWidget from "../../api/factory/dashboard/widget.js";
|
||||
|
||||
export default {
|
||||
name: 'DashboardAdmin',
|
||||
@@ -16,7 +15,7 @@ export default {
|
||||
provide() {
|
||||
return {
|
||||
adminMode: true,
|
||||
widgetsSetup: Vue.computed(() => this.dashboards[this.current] ? this.dashboards[this.current].widgetSetup : null)
|
||||
widgetsSetup: Vue.computed(() => this.dashboard ? this.dashboard.widgetSetup : null)
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -34,33 +33,32 @@ export default {
|
||||
methods: {
|
||||
dashboardAdd() {
|
||||
let _name = '';
|
||||
BsPrompt.popup('New Dashboard name').then(
|
||||
name => {
|
||||
_name = name;
|
||||
BsPrompt
|
||||
.popup('New Dashboard name')
|
||||
.then(dashboard_kurzbz => {
|
||||
const params = {
|
||||
dashboard_kurzbz: name
|
||||
dashboard_kurzbz
|
||||
};
|
||||
return this.$api
|
||||
.call(ApiDashboardBoard.add(params))
|
||||
.then(response =>{
|
||||
.then(response => {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
|
||||
|
||||
let newDashboard = {
|
||||
dashboard_id: response.data,
|
||||
dashboard_kurzbz: _name,
|
||||
dashboard_kurzbz,
|
||||
beschreibung: ''
|
||||
};
|
||||
this.dashboards.push(newDashboard);
|
||||
this.current = newDashboard.dashboard_id;
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
});
|
||||
});
|
||||
},
|
||||
dashboardUpdate(dashboard) {
|
||||
return this.$api
|
||||
this.$api
|
||||
.call(ApiDashboardBoard.update(dashboard))
|
||||
.then(response =>{
|
||||
|
||||
.then(response => {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
|
||||
|
||||
let old = this.dashboards.find(el => el.dashboard_id == dashboard.dashboard_id);
|
||||
@@ -70,73 +68,122 @@ export default {
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
dashboardDelete(dashboard_id) {
|
||||
return this.$api
|
||||
this.$api
|
||||
.call(ApiDashboardBoard.delete(dashboard_id))
|
||||
.then(response => {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
|
||||
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError)
|
||||
.finally(() => {
|
||||
this.current = -1;
|
||||
this.dashboards = this.dashboards.filter(el => el.dashboard_id != dashboard_id);
|
||||
});
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
assignWidgets(widgets) {
|
||||
this.widgets = widgets;
|
||||
/*while (this.widgets.length)
|
||||
this.widgets.pop();
|
||||
for (var i in widgets)
|
||||
this.widgets.push(widgets[i]);*/
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$api
|
||||
.call(ApiDashboardBoard.list())
|
||||
.then(result => {
|
||||
this.dashboards = result.data.retval;
|
||||
for (const dashboard of this.dashboards) {
|
||||
this.$api
|
||||
.call(ApiDashboardWidget.list(dashboard.dashboard_id))
|
||||
.then(res => {
|
||||
dashboard.widgetSetup = res.data;
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
}
|
||||
this.dashboards = result.data;
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
template: `<div class="dashboard-admin">
|
||||
|
||||
template: /* html */`
|
||||
<div class="dashboard-admin">
|
||||
<div class="input-group">
|
||||
<label for="dashboard-select" class="input-group-text">Dashboard:</label>
|
||||
<select id="dashboard-select" class="form-select" v-model="current">
|
||||
<option v-for="dashboard in dashboards" :key="dashboard.dashboard_id" :value="dashboard.dashboard_id">{{dashboard.dashboard_kurzbz}}</option>
|
||||
<label for="dashboard-select" class="input-group-text">
|
||||
Dashboard:
|
||||
</label>
|
||||
<select id="dashboard-select" v-model="current" class="form-select">
|
||||
<option
|
||||
v-for="dashboard in dashboards"
|
||||
:key="dashboard.dashboard_id"
|
||||
:value="dashboard.dashboard_id"
|
||||
>{{ dashboard.dashboard_kurzbz }}</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button" @click="dashboardAdd"><i class="fa-solid fa-plus"></i></button>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
@click="dashboardAdd"
|
||||
><i class="fa-solid fa-plus"></i></button>
|
||||
</div>
|
||||
|
||||
<div v-if="dashboard">
|
||||
<ul class="nav nav-tabs mt-3" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="edit-tab" data-bs-toggle="tab" data-bs-target="#edit" type="button" role="tab" aria-controls="edit" aria-selected="false">{{this.$p.t('ui', 'bearbeiten')}}</button>
|
||||
<button
|
||||
id="edit-tab"
|
||||
class="nav-link"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#edit"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="edit"
|
||||
aria-selected="false"
|
||||
>{{ this.$p.t('ui', 'bearbeiten') }}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="widgets-tab" data-bs-toggle="tab" data-bs-target="#widgets" type="button" role="tab" aria-controls="widgets" aria-selected="true">Widgets</button>
|
||||
<button
|
||||
id="widgets-tab"
|
||||
class="nav-link active"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#widgets"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="widgets"
|
||||
aria-selected="true"
|
||||
>Widgets</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="presets-tab" data-bs-toggle="tab" data-bs-target="#presets" type="button" role="tab" aria-controls="presets" aria-selected="false">Presets</button>
|
||||
<button
|
||||
class="nav-link"
|
||||
id="presets-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#presets"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="presets"
|
||||
aria-selected="false"
|
||||
>Presets</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content pt-3">
|
||||
<div class="tab-pane fade" id="edit" role="tabpanel" aria-labelledby="edit-tab">
|
||||
<dashboard-admin-edit v-bind="dashboard" :key="dashboard.dashboard_id" @change="dashboardUpdate($event)" @delete="dashboardDelete($event)"></dashboard-admin-edit>
|
||||
<div
|
||||
id="edit"
|
||||
class="tab-pane fade"
|
||||
role="tabpanel"
|
||||
aria-labelledby="edit-tab"
|
||||
>
|
||||
<dashboard-admin-edit
|
||||
v-bind="dashboard"
|
||||
@change="dashboardUpdate($event)"
|
||||
@delete="dashboardDelete($event)"
|
||||
></dashboard-admin-edit>
|
||||
</div>
|
||||
<div class="tab-pane fade show active" id="widgets" role="tabpanel" aria-labelledby="widgets-tab">
|
||||
<dashboard-admin-widgets :key="dashboard.dashboard_id" :dashboard_id="dashboard.dashboard_id" :widgets="widgets" @assign-widgets="assignWidgets"></dashboard-admin-widgets>
|
||||
<div
|
||||
id="widgets"
|
||||
class="tab-pane fade show active"
|
||||
role="tabpanel"
|
||||
aria-labelledby="widgets-tab"
|
||||
>
|
||||
<dashboard-admin-widgets
|
||||
:dashboard_id="dashboard.dashboard_id"
|
||||
:widgets="widgets"
|
||||
@assign-widgets="assignWidgets"
|
||||
></dashboard-admin-widgets>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="presets" role="tabpanel" aria-labelledby="presets-tab">
|
||||
<dashboard-admin-presets :dashboard="dashboard.dashboard_kurzbz" :widgets="widgets"></dashboard-admin-presets>
|
||||
<div
|
||||
id="presets"
|
||||
class="tab-pane fade"
|
||||
role="tabpanel"
|
||||
aria-labelledby="presets-tab"
|
||||
>
|
||||
<dashboard-admin-presets
|
||||
:dashboard="dashboard.dashboard_kurzbz"
|
||||
:widgets="widgets"
|
||||
></dashboard-admin-presets>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import BsConfirm from '../../Bootstrap/Confirm.js';
|
||||
|
||||
export default {
|
||||
emits: [
|
||||
"change",
|
||||
"delete"
|
||||
],
|
||||
props: {
|
||||
dashboard_id: Number,
|
||||
dashboard_kurzbz: String,
|
||||
beschreibung: String
|
||||
},
|
||||
emits: [
|
||||
"change",
|
||||
"delete"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
kurzbz: this.dashboard_kurzbz,
|
||||
@@ -18,22 +18,43 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
sendDelete() {
|
||||
BsConfirm.popup(this.$p.t('ui', 'confirm_delete') + " " + this.$p.t('ui', 'deleteInfo'))
|
||||
.then(() => this.$emit('delete', this.dashboard_id)).catch();
|
||||
BsConfirm
|
||||
.popup(this.$p.t('ui', 'confirm_delete') + " " + this.$p.t('ui', 'deleteInfo'))
|
||||
.then(() => this.$emit('delete', this.dashboard_id))
|
||||
.catch();
|
||||
}
|
||||
},
|
||||
template: `<div class="dashboard-admin-edit px-3">
|
||||
template: /* html */`
|
||||
<div class="dashboard-admin-edit px-3">
|
||||
<div class="mb-3">
|
||||
<label for="dashboard-admin-edit-kurzbz">Kurz Bezeichnung</label>
|
||||
<input id="dashboard-admin-edit-kurzbz" type="text" class="form-control" v-model="kurzbz">
|
||||
<label for="dashboard-admin-edit-kurzbz">{{ $p.t('dashboard/kurzbz') }}</label>
|
||||
<input
|
||||
id="dashboard-admin-edit-kurzbz"
|
||||
v-model="kurzbz"
|
||||
type="text"
|
||||
class="form-control"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dashboard-admin-edit-beschreibung">Beschreibung</label>
|
||||
<textarea id="dashboard-admin-edit-beschreibung" class="form-control" v-model="desc"></textarea>
|
||||
<label for="dashboard-admin-edit-beschreibung">{{ $p.t('global/beschreibung') }}</label>
|
||||
<textarea
|
||||
id="dashboard-admin-edit-beschreibung"
|
||||
v-model="desc"
|
||||
class="form-control"
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-danger" @click="sendDelete">{{this.$p.t('ui', 'loeschen')}}</button>
|
||||
<button class="btn btn-primary" @click="$emit('change', {dashboard_id,dashboard_kurzbz:kurzbz,beschreibung:desc})">{{this.$p.t('ui', 'btnAktualisieren')}}</button>
|
||||
<button class="btn btn-danger" @click="sendDelete">
|
||||
{{ this.$p.t('ui', 'loeschen') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="$emit('change', {
|
||||
dashboard_id,
|
||||
dashboard_kurzbz: kurzbz,
|
||||
beschreibung: desc
|
||||
})"
|
||||
>{{ this.$p.t('ui', 'btnAktualisieren') }}</button>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
@@ -12,20 +12,61 @@ export default {
|
||||
dashboard: String,
|
||||
widgets: Array
|
||||
},
|
||||
data: () => ({
|
||||
funktionen: {},
|
||||
sections: [],
|
||||
tmpLoading: ''
|
||||
}),
|
||||
data() {
|
||||
return {
|
||||
funktionen: {},
|
||||
sections: [],
|
||||
selectedFunktionen: [],
|
||||
abortController: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pickerWidgets() {
|
||||
return this.widgets.filter(widget => widget.allowed);
|
||||
},
|
||||
sizeLimits() {
|
||||
return Object.fromEntries(this.widgets.map(({ setup, widget_id: type }) => {
|
||||
const result = {}; // work on a copy
|
||||
if (setup.height === undefined)
|
||||
result.height = { min: 1, max: undefined };
|
||||
else if (Number.isInteger(setup.height))
|
||||
result.height = { min: setup.height, max: setup.height };
|
||||
else
|
||||
result.height = {
|
||||
min: setup.height.min ?? 1,
|
||||
max: setup.height.max
|
||||
};
|
||||
|
||||
if (setup.width === undefined)
|
||||
result.width = { min: 1, max: undefined };
|
||||
else if (Number.isInteger(setup.width))
|
||||
result.width = { min: setup.width, max: setup.width };
|
||||
else
|
||||
result.width = {
|
||||
min: setup.width.min ?? 1,
|
||||
max: setup.width.max
|
||||
};
|
||||
|
||||
return [type, result];
|
||||
}));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dashboard() {
|
||||
this.loadSections();
|
||||
this.loadFunktionen();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
widgetAdd(section_name, widget) {
|
||||
widgetAdd(widget, section_name) {
|
||||
this.$refs.widgetpicker.getWidget().then(widget_id => {
|
||||
widget.widget = widget_id;
|
||||
// NOTE(chris): min size
|
||||
widget.place = Object.fromEntries(Object.entries(widget.place).map(([key, value]) => {
|
||||
value.w = this.sizeLimits[widget_id].width.min;
|
||||
value.h = this.sizeLimits[widget_id].height.min;
|
||||
return [key, value];
|
||||
}));
|
||||
widget.id = 'loading_' + String((new Date()).valueOf());
|
||||
delete widget.custom;
|
||||
widget.preset = 1;
|
||||
@@ -36,6 +77,8 @@ export default {
|
||||
section.widgets.push(loading);
|
||||
});
|
||||
|
||||
delete widget.id;
|
||||
|
||||
const params = {
|
||||
dashboard: this.dashboard,
|
||||
funktion_kurzbz: section_name,
|
||||
@@ -64,22 +107,29 @@ export default {
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
widgetUpdate(section_name, payload) {
|
||||
payload = payload[section_name];
|
||||
widgetUpdate(payload, section_name) {
|
||||
for (var k in payload) {
|
||||
const section = this.sections.find(section => section.name == section_name);
|
||||
for (var wid in section.widgets) {
|
||||
if (section.widgets[wid].id == k) {
|
||||
payload[k] = ObjectUtils.mergeDeep(section.widgets[wid], payload[k]);
|
||||
const copy = ObjectUtils.mergeDeep(section.widgets[wid], payload[k]);
|
||||
if (payload[k].config)
|
||||
copy.config = payload[k].config;
|
||||
payload[k] = copy;
|
||||
// NOTE(chris): remove internal props
|
||||
for (var prop of ['_x', '_y', '_w', '_h', 'index', 'id'])
|
||||
for (var prop of ['_x', '_y', '_w', '_h', 'index', 'id', 'custom'])
|
||||
if (payload[k][prop])
|
||||
delete payload[k][prop];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (payload[k].place) {
|
||||
Object.values(payload[k].place).forEach(place => {
|
||||
if (place.pinned === false)
|
||||
delete place.pinned;
|
||||
});
|
||||
}
|
||||
payload[k].widgetid = k;
|
||||
delete payload[k].custom;
|
||||
}
|
||||
this.$api
|
||||
.call(Object.entries(payload).map(([key, widget]) => [
|
||||
@@ -106,7 +156,7 @@ export default {
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
widgetRemove(section_name, id) {
|
||||
widgetRemove(id, section_name) {
|
||||
const params = {
|
||||
db: this.dashboard,
|
||||
funktion_kurzbz: section_name,
|
||||
@@ -122,21 +172,22 @@ export default {
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
loadSections(evt) {
|
||||
let funktionen = Array.from(evt.target.querySelectorAll("option:checked"),e=>e.value);
|
||||
this.sections = [];
|
||||
this.tmpLoading = funktionen.join('###');
|
||||
|
||||
loadSections() {
|
||||
const params = {
|
||||
db: this.dashboard,
|
||||
funktionen
|
||||
funktionen: this.selectedFunktionen
|
||||
};
|
||||
|
||||
if (this.abortController)
|
||||
this.abortController.abort();
|
||||
this.abortController = new AbortController();
|
||||
const signal = this.abortController.signal;
|
||||
|
||||
this.sections = [];
|
||||
|
||||
return this.$api
|
||||
.call(ApiDashboardPreset.getBatch(params))
|
||||
.call(ApiDashboardPreset.getBatch(params), { signal })
|
||||
.then(result => {
|
||||
if (this.tmpLoading !== funktionen.join('###'))
|
||||
return; // NOTE(chris): prevent race condition
|
||||
for (var section in result.data) {
|
||||
let widgets = [];
|
||||
for (var wid in result.data[section]) {
|
||||
@@ -151,7 +202,6 @@ export default {
|
||||
}
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
|
||||
},
|
||||
loadFunktionen() {
|
||||
this.$api
|
||||
@@ -165,17 +215,17 @@ export default {
|
||||
created() {
|
||||
this.loadFunktionen();
|
||||
},
|
||||
watch: {
|
||||
dashboard() {
|
||||
// TODO(chris): this should be done without a watcher
|
||||
this.loadSections({target:this.$refs.funktionenList});
|
||||
this.loadFunktionen();
|
||||
}
|
||||
},
|
||||
template: `<div class="dashboard-admin-presets">
|
||||
template: /* html */`
|
||||
<div class="dashboard-admin-presets">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<select ref="funktionenList" style="height:30em" class="form-control" multiple @input="loadSections">
|
||||
<select
|
||||
v-model="selectedFunktionen"
|
||||
class="form-control"
|
||||
style="height:30em"
|
||||
multiple
|
||||
@change="loadSections"
|
||||
>
|
||||
<option
|
||||
v-for="funktion in funktionen"
|
||||
:key="funktion.funktion_kurzbz"
|
||||
@@ -185,9 +235,20 @@ export default {
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<dashboard-section v-for="section in sections" :key="section.name" :name="section.name" :widgets="section.widgets" @widget-add="widgetAdd" @widget-update="widgetUpdate" @widget-remove="widgetRemove"></dashboard-section>
|
||||
<dashboard-section
|
||||
v-for="section in sections"
|
||||
:key="section.name"
|
||||
:name="section.name"
|
||||
:widgets="section.widgets"
|
||||
@widget-add="widgetAdd"
|
||||
@widget-update="widgetUpdate"
|
||||
@widget-remove="widgetRemove"
|
||||
></dashboard-section>
|
||||
</div>
|
||||
</div>
|
||||
<dashboard-widget-picker ref="widgetpicker" :widgets="pickerWidgets"></dashboard-widget-picker>
|
||||
<dashboard-widget-picker
|
||||
ref="widgetpicker"
|
||||
:widgets="pickerWidgets"
|
||||
></dashboard-widget-picker>
|
||||
</div>`
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import ApiDashboardWidget from "../../../api/factory/dashboard/widget.js";
|
||||
|
||||
export default {
|
||||
emits: [
|
||||
"change",
|
||||
"assignWidgets"
|
||||
],
|
||||
props: {
|
||||
dashboard_id: Number,
|
||||
widgets: Array
|
||||
},
|
||||
emits: [
|
||||
"change",
|
||||
"assignWidgets"
|
||||
],
|
||||
methods: {
|
||||
sendChange(widget_id) {
|
||||
let allow = !this.widgets.find(el => el.widget_id == widget_id).allowed;
|
||||
@@ -29,11 +29,27 @@ export default {
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
template: `
|
||||
template: /* html */`
|
||||
<div class="dashboard-admin-widgets">
|
||||
<div v-for="widget in widgets" :key="widget.widget_id" class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" :id="'dashboard-admin-widgets-' + widget.widget_id" v-model="widget.allowed" @input.prevent="sendChange(widget.widget_id)">
|
||||
<label class="form-check-label" :for="'dashboard-admin-widgets-' + widget.widget_id">{{(widget.setup && widget.setup.name) || widget.widget_kurzbz}}</label>
|
||||
<div
|
||||
v-for="widget in widgets"
|
||||
:key="widget.widget_id"
|
||||
class="form-check form-switch"
|
||||
>
|
||||
<input
|
||||
:id="'dashboard-admin-widgets-' + widget.widget_id"
|
||||
v-model="widget.allowed"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
@input.prevent="sendChange(widget.widget_id)"
|
||||
>
|
||||
<label
|
||||
class="form-check-label"
|
||||
:for="'dashboard-admin-widgets-' + widget.widget_id"
|
||||
>
|
||||
{{ (widget.setup && widget.setup.name) || widget.widget_kurzbz }}
|
||||
</label>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import DashboardSection from "./Section.js";
|
||||
import DashboardWidgetPicker from "./Widget/Picker.js";
|
||||
import ObjectUtils from "../../helpers/ObjectUtils.js";
|
||||
|
||||
import ApiDashboard from '../../api/factory/cis/dashboard.js';
|
||||
import ApiDashboardWidget from '../../api/factory/dashboard/widget.js';
|
||||
import ApiDashboardUser from '../../api/factory/dashboard/user.js';
|
||||
|
||||
@@ -17,40 +18,76 @@ export default {
|
||||
required: true,
|
||||
default: 'CIS'
|
||||
},
|
||||
viewData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return value && value.name && value.timezone
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
widgets: [],
|
||||
originalWidgets: {},
|
||||
widgetsSetup: null,
|
||||
editMode: false
|
||||
editMode: false,
|
||||
timezone: null,
|
||||
userFirstName: null,
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
editMode: Vue.computed(()=>this.editMode),
|
||||
editMode: Vue.computed(() => this.editMode),
|
||||
widgetsSetup: Vue.computed(() => this.widgetsSetup),
|
||||
timezone: Vue.computed(() => this.viewData.timezone)
|
||||
timezone: this.timezone
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sizeLimits() {
|
||||
return Object.fromEntries(this.widgetsSetup.map(({ setup, widget_id: type }) => {
|
||||
const result = {}; // work on a copy
|
||||
if (setup.height === undefined)
|
||||
result.height = { min: 1, max: undefined };
|
||||
else if (Number.isInteger(setup.height))
|
||||
result.height = { min: setup.height, max: setup.height };
|
||||
else
|
||||
result.height = {
|
||||
min: setup.height.min ?? 1,
|
||||
max: setup.height.max
|
||||
};
|
||||
|
||||
if (setup.width === undefined)
|
||||
result.width = { min: 1, max: undefined };
|
||||
else if (Number.isInteger(setup.width))
|
||||
result.width = { min: setup.width, max: setup.width };
|
||||
else
|
||||
result.width = {
|
||||
min: setup.width.min ?? 1,
|
||||
max: setup.width.max
|
||||
};
|
||||
|
||||
return [type, result];
|
||||
}));
|
||||
},
|
||||
hiddenWidgets() {
|
||||
return this.widgets.filter(widget => widget.hidden);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
widgetAdd(section_name, widget) {
|
||||
// TODO(chris): remove section_name? (change order of params => get rid of it)
|
||||
widgetAdd(widget) {
|
||||
this.$refs.widgetpicker
|
||||
.getWidget()
|
||||
.then(widget_id => {
|
||||
if (widget_id.hidden)
|
||||
this.widgetUnhide(widget_id, widget.place);
|
||||
|
||||
widget.widget = widget_id;
|
||||
// NOTE(chris): min size
|
||||
widget.place = Object.fromEntries(Object.entries(widget.place).map(([key, value]) => {
|
||||
value.w = this.sizeLimits[widget_id].width.min;
|
||||
value.h = this.sizeLimits[widget_id].height.min;
|
||||
return [key, value];
|
||||
}));
|
||||
widget.id = 'loading_' + String((new Date()).valueOf());
|
||||
let loading = { ...widget };
|
||||
loading.loading = true;
|
||||
this.widgets.push(loading);
|
||||
|
||||
delete widget.id;
|
||||
|
||||
this.$api
|
||||
.call(ApiDashboardUser.addWidget(this.dashboard, widget))
|
||||
@@ -64,19 +101,27 @@ export default {
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
widgetUpdate(section_name, payload) {
|
||||
payload = payload[section_name];
|
||||
widgetUpdate(payload) {
|
||||
for (var k in payload) {
|
||||
for (var wid in this.widgets) {
|
||||
if (this.widgets[wid].id == k) {
|
||||
payload[k] = ObjectUtils.mergeDeep(this.widgets[wid], payload[k]);
|
||||
const copy = ObjectUtils.mergeDeep(this.widgets[wid], payload[k]);
|
||||
if (payload[k].config)
|
||||
copy.config = payload[k].config;
|
||||
payload[k] = copy;
|
||||
// NOTE(chris): remove internal props
|
||||
for (var prop of ['_x','_y','_w','_h','index','id','preset'])
|
||||
for (var prop of ['_x', '_y', '_w', '_h', 'index', 'id', 'preset'])
|
||||
if (payload[k][prop])
|
||||
delete payload[k][prop];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (payload[k].place) {
|
||||
Object.values(payload[k].place).forEach(place => {
|
||||
if (place.pinned === false)
|
||||
delete place.pinned;
|
||||
});
|
||||
}
|
||||
payload[k].widgetid = k;
|
||||
}
|
||||
this.$api
|
||||
@@ -92,17 +137,6 @@ export default {
|
||||
this.widgets.forEach((widget, i) => {
|
||||
if (failed.includes(widget.id)) {
|
||||
this.widgets[i] = structuredClone(ObjectUtils.deepToRaw(this.originalWidgets[widget.id]));
|
||||
/** NOTE(chris): if you wanna hide or unhide a
|
||||
* preset and it fails: switch around the hidden
|
||||
* value to revert it properly (checkboxes can't
|
||||
* really handle it otherwise)
|
||||
*/
|
||||
if (payload[widget.id].hidden !== undefined) {
|
||||
this.widgets[i].hidden = payload[widget.id].hidden;
|
||||
this.$nextTick(() => {
|
||||
this.widgets[i] = structuredClone(ObjectUtils.deepToRaw(this.originalWidgets[widget.id]));
|
||||
});
|
||||
}
|
||||
} else if (payload[widget.id]) {
|
||||
payload[widget.id].id = widget.id;
|
||||
payload[widget.id].index = widget.index;
|
||||
@@ -113,18 +147,51 @@ export default {
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
widgetRemove(section_name, id) {
|
||||
widgetRemove(id) {
|
||||
this.$api
|
||||
.call(ApiDashboardUser.removeWidget(this.dashboard, id))
|
||||
.then(() => {
|
||||
this.widgets = this.widgets.filter(widget => widget.id != id);
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
widgetUnhide(orig, place) {
|
||||
place = Object.fromEntries(Object.entries(place).map(([key, value]) => {
|
||||
value.w = this.sizeLimits[orig.widget].width.min;
|
||||
value.h = this.sizeLimits[orig.widget].height.min;
|
||||
// NOTE(chris): unpin pinned widgets because they got a new placement
|
||||
value.pinned = false;
|
||||
return [key, value];
|
||||
}));
|
||||
// NOTE(chris): clear placement in other screensizes
|
||||
Object.keys(orig.place).forEach(key => {
|
||||
if (!place[key]) {
|
||||
place[key] = {
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
pinned: false
|
||||
};
|
||||
}
|
||||
});
|
||||
let payload = {};
|
||||
payload[orig.id] = {
|
||||
hidden: false,
|
||||
place
|
||||
};
|
||||
return this.widgetUpdate(payload);
|
||||
},
|
||||
async fetchViewData() {
|
||||
let viewDataResult = await this.$api.call(ApiDashboard.getViewData());
|
||||
const viewData = viewDataResult.data;
|
||||
this.timezone = viewData?.timezone;
|
||||
this.userFirstName = viewData?.name;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
async created() {
|
||||
this.$p.loadCategory('dashboard');
|
||||
|
||||
await this.fetchViewData();
|
||||
|
||||
this.$api
|
||||
.call(ApiDashboardWidget.listAllowed(this.dashboard))
|
||||
.then(res => {
|
||||
@@ -138,8 +205,8 @@ export default {
|
||||
const widgets = [];
|
||||
const remove = [];
|
||||
|
||||
for (var wid in res.data.general.widgets) {
|
||||
let widget = res.data.general.widgets[wid];
|
||||
for (var wid in res.data) {
|
||||
let widget = res.data[wid];
|
||||
widget.id = wid;
|
||||
if (widget.custom || widget.preset) {
|
||||
widgets.push(widget);
|
||||
@@ -149,19 +216,42 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
remove.forEach(wid => this.widgetRemove('general', wid));
|
||||
remove.forEach(wid => this.widgetRemove(wid));
|
||||
|
||||
this.widgets = widgets;
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
template: `
|
||||
template: /* html */`
|
||||
<div class="core-dashboard">
|
||||
<h3>
|
||||
{{ $p.t('global/personalGreeting', [ viewData?.name ]) }}
|
||||
<button style="margin-left: 8px;" class="btn" @click="editMode = !editMode" aria-label="edit dashboard" v-tooltip="{showDelay:1000,value:'edit dashboard'}"><i class="fa-solid fa-gear" aria-hidden="true"></i></button>
|
||||
</h3>
|
||||
<dashboard-section :seperator="0" name="general" :widgets="widgets" @widgetAdd="widgetAdd" @widgetUpdate="widgetUpdate" @widgetRemove="widgetRemove"></dashboard-section>
|
||||
<dashboard-widget-picker ref="widgetpicker" :widgets="widgetsSetup"></dashboard-widget-picker>
|
||||
<header class="d-flex justify-content-between align-items-baseline">
|
||||
<h3>
|
||||
{{ userFirstName ? $p.t('global/personalGreeting', [ userFirstName ]) : '' }}
|
||||
</h3>
|
||||
<div class="form-check form-check-reverse form-switch">
|
||||
<label class="form-check-label" for="switchEditMode">
|
||||
{{ $p.t('dashboard/edit') }}
|
||||
</label>
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="switchEditMode"
|
||||
v-model="editMode"
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
<dashboard-section
|
||||
name="general"
|
||||
:widgets="widgets"
|
||||
@widget-add="widgetAdd"
|
||||
@widget-update="widgetUpdate"
|
||||
@widget-remove="widgetRemove"
|
||||
></dashboard-section>
|
||||
<dashboard-widget-picker
|
||||
ref="widgetpicker"
|
||||
:widgets="widgetsSetup"
|
||||
:hidden-widgets="hiddenWidgets"
|
||||
></dashboard-widget-picker>
|
||||
</div>`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import BsModal from "../Bootstrap/Modal.js";
|
||||
import { useCachedWidgetLoader } from "../../composables/Dashboard/CachedWidgetLoader.js";
|
||||
import HeightTransition from "../Tranistion/HeightTransition.js";
|
||||
|
||||
import { enableDragDropTouch } from "../../../../vendor/drag-drop-touch-js/dragdroptouch/dist/drag-drop-touch.esm.min.js";
|
||||
|
||||
if (!document.dragDropTouchActive) {
|
||||
enableDragDropTouch();
|
||||
document.dragDropTouchActive = true;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Item',
|
||||
components: {
|
||||
@@ -11,18 +17,14 @@ export default {
|
||||
data: () => ({
|
||||
component: "",
|
||||
arguments: null,
|
||||
target: false,
|
||||
widget: null,
|
||||
tmpConfig: {},
|
||||
isLoading: false,
|
||||
hasConfig: false,
|
||||
sharedData: null,
|
||||
sharedData: null
|
||||
}),
|
||||
emits: [
|
||||
"change",
|
||||
"remove",
|
||||
"dragstart",
|
||||
"resizestart",
|
||||
"configOpened",
|
||||
"configClosed",
|
||||
"pinItem",
|
||||
@@ -30,66 +32,101 @@ export default {
|
||||
],
|
||||
props: [
|
||||
"id",
|
||||
"widgetID",
|
||||
"config",
|
||||
"width",
|
||||
"height",
|
||||
"custom",
|
||||
"hidden",
|
||||
"editMode",
|
||||
"loading",
|
||||
"loading", // widget got added and is waiting for backend to save in db
|
||||
"item_data",
|
||||
"place",
|
||||
"setup",
|
||||
"dragstate",
|
||||
"resizeOverlay",
|
||||
"additionalRow"
|
||||
"widgetTemplate",
|
||||
"source"
|
||||
],
|
||||
computed: {
|
||||
maxHeight(){
|
||||
return this.setup?.height?.max;
|
||||
},
|
||||
maxWidth(){
|
||||
if (Object.prototype.toString.call(this.setup?.width) == "[object Number]"){
|
||||
return this.setup?.width;
|
||||
sourceInfoTooltip() {
|
||||
switch (this.source) {
|
||||
case null:
|
||||
return '';
|
||||
case 'general':
|
||||
return this.$p.t('dashboard', 'widgetFromGeneralSection');
|
||||
case 'custom':
|
||||
return this.$p.t('dashboard', 'widgetFromCustomSection');
|
||||
default:
|
||||
return this.$p.t('dashboard', 'widgetFromFunktionSection', [this.source]);
|
||||
}
|
||||
return this.setup?.width?.max;
|
||||
},
|
||||
minHeight() {
|
||||
return this.setup?.height?.min;
|
||||
isResizeableHorizontal() {
|
||||
if (this.widgetTemplate.setup.width === undefined)
|
||||
return true;
|
||||
|
||||
if (Object.prototype.toString.call(this.widgetTemplate.setup.width) == "[object Number]")
|
||||
return false;
|
||||
|
||||
if (this.widgetTemplate.setup.width.min === undefined) {
|
||||
if (this.widgetTemplate.setup.width.max === undefined)
|
||||
return true;
|
||||
return this.widgetTemplate.setup.width.max > 1;
|
||||
}
|
||||
|
||||
if (this.widgetTemplate.setup.width.max === undefined)
|
||||
return true;
|
||||
|
||||
return this.widgetTemplate.setup.width.max > this.widgetTemplate.setup.width.min;
|
||||
},
|
||||
minWidth() {
|
||||
return this.setup?.width?.min;
|
||||
isResizeableVertical() {
|
||||
if (this.widgetTemplate.setup.height === undefined)
|
||||
return true;
|
||||
|
||||
if (Object.prototype.toString.call(this.widgetTemplate.setup.height) == "[object Number]")
|
||||
return false;
|
||||
|
||||
if (this.widgetTemplate.setup.height.min === undefined) {
|
||||
if (this.widgetTemplate.setup.height.max === undefined)
|
||||
return true;
|
||||
return this.widgetTemplate.setup.height.max > 1;
|
||||
}
|
||||
|
||||
if (this.widgetTemplate.setup.height.max === undefined)
|
||||
return true;
|
||||
|
||||
return this.widgetTemplate.setup.height.max > this.widgetTemplate.setup.height.min;
|
||||
},
|
||||
isResizeable(){
|
||||
return this.maxWidth >1 || this.maxHeight >1;
|
||||
isResizeable() {
|
||||
return this.isResizeableVertical || this.isResizeableHorizontal;
|
||||
},
|
||||
isPinned(){
|
||||
resizeClasses() {
|
||||
const classes = {
|
||||
icon: 'fa-up-right-and-down-left-from-center mirror-x',
|
||||
button: 'cursor-nw-resize'
|
||||
};
|
||||
if (!this.isResizeableHorizontal) {
|
||||
classes.icon = 'fa-up-down pe-2';
|
||||
classes.button = 'cursor-ns-resize';
|
||||
} else if (!this.isResizeableVertical) {
|
||||
classes.icon = 'fa-left-right pe-2';
|
||||
classes.button = 'cursor-ew-resize';
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
isPinned() {
|
||||
return this.place?.pinned ? true : false;
|
||||
},
|
||||
ready() {
|
||||
return this.component && this.arguments !== null;
|
||||
},
|
||||
visible: {
|
||||
get() {
|
||||
return !this.hidden;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('remove', this.hidden);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
unpin(){
|
||||
unpin() {
|
||||
// Unpinning is only possible in edit mode
|
||||
if(!this.editMode)
|
||||
if (!this.editMode)
|
||||
return;
|
||||
let result = { item: this.item_data, x: this.item_data.x, y: this.item_data.y };
|
||||
let result = { item: this.item_data, pinned: false };
|
||||
this.$emit('unPinItem', [result]);
|
||||
},
|
||||
pinItem(){
|
||||
let result = { item: this.item_data, x: this.item_data.x, y: this.item_data.y};
|
||||
this.$emit('pinItem',[result]);
|
||||
pinItem() {
|
||||
let result = { item: this.item_data, pinned: true };
|
||||
this.$emit('pinItem', [result]);
|
||||
},
|
||||
getWidgetC4Link(widget) {
|
||||
return (FHC_JS_DATA_STORAGE_OBJECT.app_root +
|
||||
@@ -101,22 +138,6 @@ export default {
|
||||
handleHideBsModal() {
|
||||
this.$emit('configClosed')
|
||||
},
|
||||
mouseDown(e) {
|
||||
this.target = e.target;
|
||||
},
|
||||
startDrag(e) {
|
||||
if (this.$refs.dragHandle.contains(this.target)) {
|
||||
this.$emit("dragstart", e);
|
||||
} else if (
|
||||
this.isResizeable &&
|
||||
this.$refs.resizeHandle.contains(this.target)
|
||||
) {
|
||||
if (this.isResizeable) this.$emit("resizestart", e);
|
||||
else e.preventDefault();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
openConfig() {
|
||||
this.tmpConfig = { ...this.arguments };
|
||||
this.$refs.config.show();
|
||||
@@ -135,110 +156,228 @@ export default {
|
||||
},
|
||||
sendChangeConfig(config) {
|
||||
for (var k in config) {
|
||||
if (this.widget.arguments[k] == config[k]) {
|
||||
delete config[k];
|
||||
if (this.widgetTemplate.arguments[k] == config[k]) {
|
||||
delete config[k];
|
||||
}
|
||||
}
|
||||
this.$emit("change", config);
|
||||
},
|
||||
async initializeComponent() {
|
||||
if (
|
||||
this.widgetTemplate
|
||||
&& this.widgetTemplate.setup
|
||||
&& this.widgetTemplate.widget_id
|
||||
&& this.widgetTemplate.arguments
|
||||
) {
|
||||
let component = (await import(this.widgetTemplate.setup.file)).default;
|
||||
this.$options.components["widget" + this.widgetTemplate.widget_id] = component;
|
||||
this.component = "widget" + this.widgetTemplate.widget_id;
|
||||
this.arguments = { ...this.widgetTemplate.arguments, ...this.config };
|
||||
this.tmpConfig = { ...this.arguments };
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
config() {
|
||||
this.arguments = { ...this.widget?.arguments, ...this.config };
|
||||
this.arguments = { ...this.widgetTemplate?.arguments, ...this.config };
|
||||
this.tmpConfig = { ...this.arguments };
|
||||
this.$refs.config && this.$refs.config.hide();
|
||||
this.isLoading = false;
|
||||
},
|
||||
widgetTemplate() {
|
||||
this.initializeComponent();
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { actions } = useCachedWidgetLoader();
|
||||
return {
|
||||
loadWidget: actions.load
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
this.widget = await this.loadWidget(this.id);
|
||||
let component = (await import(this.widget.setup.file)).default;
|
||||
this.$options.components["widget" + this.widget.widget_id] = component;
|
||||
this.component = "widget" + this.widget.widget_id;
|
||||
this.arguments = { ...this.widget.arguments, ...this.config };
|
||||
this.tmpConfig = { ...this.arguments };
|
||||
created() {
|
||||
this.initializeComponent();
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div v-if="loading">
|
||||
<div class="d-flex justify-content-center align-items-center h-100">
|
||||
<article
|
||||
class="dashboard-item card overflow-hidden h-100 position-relative"
|
||||
:class="{
|
||||
[arguments?.className]: arguments && arguments.className
|
||||
}"
|
||||
>
|
||||
<div v-if="loading" class="d-flex justify-content-center align-items-center h-100">
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!hidden || editMode" :id="widgetID" class="dashboard-item card overflow-hidden h-100 position-relative" :class="{'hiddenWidget':hidden, 'draggedItem':dragstate, 'dashboard-item-overlay':resizeOverlay, [arguments?.className]:arguments && arguments.className}">
|
||||
<div v-show="!dragstate" class="h-100 card border-0">
|
||||
<div v-if="widget" class="card-header d-flex ps-0 pe-2 align-items-center">
|
||||
<Transition>
|
||||
<span type="button" v-if="editMode && !isPinned" drag-action="move" class="col-auto mx-2 px-2 cursor-move" aria-label="move widget" v-tooltip="{showDelay:1000, value:'move widget'}"><i class="fa-solid fa-grip-vertical" aria-hidden="true"></i></span>
|
||||
</Transition>
|
||||
<span class="col mx-2 px-2">{{ widget.setup.name }}</span>
|
||||
<template v-if="isPinned">
|
||||
<div type="button" role="button" v-if="editMode" pinned="true" @click="unpin" title="unpin item" aria-label="unpin item" class="pin cursor-pointer col-auto me-2">
|
||||
<i class="fa-solid fa-thumbtack " aria-hidden="true"></i>
|
||||
</div>
|
||||
<div v-else class="col-auto me-2">
|
||||
<i class="fa-solid fa-thumbtack "></i>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div type="button" role="button" v-if="editMode" class="col-auto me-2 pin" @click="pinItem" aria-label="pin item" title="pin item">
|
||||
<i class="fa-solid fa-thumbtack" aria-hidden="true" style="color:lightgray;"></i>
|
||||
</div>
|
||||
</template>
|
||||
<a type="button" v-if="widget.setup.cis4link" :href="getWidgetC4Link(widget)" aria-label="widget link" v-tooltip="{showDelay:1000, value:'widget link'}" class="col-auto ms-auto ">
|
||||
<i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a type="button" v-if="hasConfig" class="col-auto px-1" href="#" @click.prevent="openConfig" aria-label="configure widget" v-tooltip="{showDelay:1000,value:'configure widget'}"><i class="fa-solid fa-gear" aria-hidden="true"></i></a>
|
||||
<a type="button" v-if="custom && editMode" class="col-auto px-1" aria-label="delete widget" v-tooltip="{showDelay:1000,value:'delete widget'}" href="#" @click.prevent="$emit('remove')">
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
<Transition>
|
||||
<div v-if="!custom && editMode" class="col-auto px-1 form-switch">
|
||||
<input class="form-check-input ms-0" type="checkbox" role="switch" aria-label="toggle widget" id="flexSwitchCheckChecked" v-model="visible" :value="true">
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
<div v-if="ready" class="card-body overflow-hidden p-0">
|
||||
<component :is="component" v-model:shared-data="sharedData" :config="arguments" :width="width" :height="height" @setConfig="setConfig" @change="changeConfigManually"></component>
|
||||
</div>
|
||||
<div v-else class="card-body overflow-hidden text-center d-flex flex-column justify-content-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></div>
|
||||
<bs-modal v-if="hasConfig" ref="config" @hideBsModal="handleHideBsModal" @showBsModal="handleShowBsModal">
|
||||
<template v-slot:title>
|
||||
{{ widget ? 'Config for ' + widget.setup.name : '' }}
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
<component v-if="ready && !isLoading" :is="component" v-model:shared-data="sharedData" :config="tmpConfig" @change="changeConfig" :configMode="true"></component>
|
||||
<div v-else class="text-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></div>
|
||||
</template>
|
||||
<template v-if="!widget?.setup?.hideFooter" v-slot:footer>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" @click="changeConfig">Save changes</button>
|
||||
</template>
|
||||
</bs-modal>
|
||||
<height-transition>
|
||||
<div v-if="editMode && isResizeable && !isPinned " class="card-footer d-flex justify-content-end p-0">
|
||||
<template v-if="maxWidth < 2">
|
||||
<span type="button" drag-action="resize" class="col-auto px-1 cursor-ns-resize" aria-label="resize widget" v-tooltip="{showDelay:1000, value:'resize widget'}">
|
||||
<i class="fa-solid fa-up-down pe-2" aria-hidden="true"></i>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="maxHeight < 2">
|
||||
<span type="button" drag-action="resize" class="col-auto px-1 cursor-ew-resize" aria-label="resize widget" v-tooltip="{showDelay:1000, value:'resize widget'}">
|
||||
<i class="fa-solid fa-left-right pe-2" aria-hidden="true"></i>
|
||||
<template v-else>
|
||||
<header
|
||||
v-if="widgetTemplate"
|
||||
class="card-header d-flex ps-0 pe-2 align-items-center"
|
||||
>
|
||||
<!-- move handle -->
|
||||
<Transition>
|
||||
<span
|
||||
v-if="editMode && !isPinned"
|
||||
type="button"
|
||||
drag-action="move"
|
||||
class="col-auto mx-2 px-2 cursor-move"
|
||||
draggable="true"
|
||||
aria-hidden="true"
|
||||
:aria-label="$p.t('dashboard/widget_move')"
|
||||
v-tooltip="{ showDelay: 1000, value: $p.t('dashboard/widget_move') }"
|
||||
>
|
||||
<i class="fa-solid fa-grip-vertical" aria-hidden="true"></i>
|
||||
</span>
|
||||
</Transition>
|
||||
<!-- TITLE -->
|
||||
<h4 class="col mb-0 mx-2 px-2 fs-6 lh-base">
|
||||
{{ widgetTemplate.setup.name }}
|
||||
</h4>
|
||||
<!-- source info -->
|
||||
<div
|
||||
v-if="source"
|
||||
class="col-auto me-2"
|
||||
:aria-label="sourceInfoTooltip"
|
||||
v-tooltip="{ class: 'w-100', value: sourceInfoTooltip }"
|
||||
>
|
||||
<i class="fa-solid fa-circle-info" aria-hidden="true"></i>
|
||||
</div>
|
||||
<!-- pin button -->
|
||||
<template v-if="isPinned">
|
||||
<div
|
||||
v-if="editMode"
|
||||
type="button"
|
||||
role="button"
|
||||
class="pin cursor-pointer col-auto me-2"
|
||||
:title="$p.t('dashboard/widget_unpin')"
|
||||
aria-hidden="true"
|
||||
:aria-label="$p.t('dashboard/widget_unpin')"
|
||||
pinned="true"
|
||||
@click="unpin"
|
||||
>
|
||||
<i class="fa-solid fa-thumbtack" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div v-else class="col-auto me-2" aria-hidden="true">
|
||||
<i class="fa-solid fa-thumbtack"></i>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span type="button" drag-action="resize" class="col-auto px-1 cursor-nw-resize" aria-label="resize widget" v-tooltip="{showDelay:1000, value:'resize widget'}">
|
||||
<i class="fa-solid fa-up-right-and-down-left-from-center mirror-x" aria-hidden="true"></i>
|
||||
</span>
|
||||
<div
|
||||
v-if="editMode"
|
||||
type="button"
|
||||
role="button"
|
||||
class="col-auto me-2 pin"
|
||||
:title="$p.t('dashboard/widget_pin')"
|
||||
aria-hidden="true"
|
||||
:aria-label="$p.t('dashboard/widget_pin')"
|
||||
@click="pinItem"
|
||||
>
|
||||
<i class="fa-solid fa-thumbtack" aria-hidden="true" style="color:lightgray;"></i>
|
||||
</div>
|
||||
</template>
|
||||
<!-- widget link -->
|
||||
<a
|
||||
v-if="widgetTemplate.setup.cis4link"
|
||||
:href="getWidgetC4Link(widgetTemplate)"
|
||||
class="col-auto ms-auto"
|
||||
:aria-label="$p.t('dashboard/widget_link')"
|
||||
v-tooltip="{ showDelay: 1000, value: $p.t('dashboard/widget_link') }"
|
||||
>
|
||||
<i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true"></i>
|
||||
</a>
|
||||
<!-- config button -->
|
||||
<a
|
||||
v-if="hasConfig"
|
||||
href="#"
|
||||
class="col-auto px-1"
|
||||
:aria-label="$p.t('dashboard/widget_configure')"
|
||||
v-tooltip="{ showDelay: 1000, value: $p.t('dashboard/widget_configure') }"
|
||||
@click.prevent="openConfig"
|
||||
>
|
||||
<i class="fa-solid fa-gear" aria-hidden="true"></i>
|
||||
</a>
|
||||
<!-- delete/hide button -->
|
||||
<a
|
||||
v-if="editMode"
|
||||
href="#"
|
||||
class="col-auto px-1"
|
||||
:aria-label="$p.t('dashboard/widget_delete')"
|
||||
v-tooltip="{ showDelay: 1000, value: $p.t('dashboard/widget_delete') }"
|
||||
@click.prevent="$emit('remove')"
|
||||
>
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
</header>
|
||||
<!-- TODO Manu rename/remove-->
|
||||
<div v-if="ready" class="card-body overflow-hidden p-0">
|
||||
<component
|
||||
:is="component"
|
||||
v-model:shared-data="sharedData"
|
||||
:config="arguments"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:widget-id="item_data.id"
|
||||
:item_data="item_data"
|
||||
@setConfig="setConfig"
|
||||
@change="changeConfigManually"
|
||||
></component>
|
||||
</div>
|
||||
</height-transition>
|
||||
</div>
|
||||
</div>`,
|
||||
<div
|
||||
v-else
|
||||
class="card-body overflow-hidden text-center d-flex flex-column justify-content-center"
|
||||
>
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
<bs-modal
|
||||
v-if="hasConfig"
|
||||
ref="config"
|
||||
@hideBsModal="handleHideBsModal"
|
||||
@showBsModal="handleShowBsModal"
|
||||
>
|
||||
<template v-slot:title>
|
||||
{{ widgetTemplate ? $p.t('dashboard/widget_config_title', widgetTemplate.setup) : '' }}
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
<component
|
||||
:is="component"
|
||||
v-if="ready && !isLoading"
|
||||
v-model:shared-data="sharedData"
|
||||
:config="tmpConfig"
|
||||
@change="changeConfig"
|
||||
:configMode="true"
|
||||
></component>
|
||||
<div v-else class="text-center">
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="!widgetTemplate?.setup?.hideFooter" v-slot:footer>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
data-bs-dismiss="modal"
|
||||
>{{ $p.t('ui/schliessen') }}</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="changeConfig"
|
||||
>{{ $p.t('ui/speichern') }}</button>
|
||||
</template>
|
||||
</bs-modal>
|
||||
<height-transition>
|
||||
<footer
|
||||
v-if="editMode && isResizeable && !isPinned"
|
||||
class="card-footer d-flex justify-content-end p-0"
|
||||
>
|
||||
<span
|
||||
type="button"
|
||||
drag-action="resize"
|
||||
class="col-auto px-1"
|
||||
:class="resizeClasses.button"
|
||||
draggable="true"
|
||||
aria-hidden="true"
|
||||
:aria-label="$p.t('dashboard/widget_resize')"
|
||||
v-tooltip="{ showDelay: 1000, value: $p.t('dashboard/widget_resize') }"
|
||||
>
|
||||
<i
|
||||
class="fa-solid"
|
||||
:class="resizeClasses.icon"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</span>
|
||||
</footer>
|
||||
</height-transition>
|
||||
</template>
|
||||
</article>`,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import BsConfirm from "../Bootstrap/Confirm.js";
|
||||
import DropGrid from '../Drop/Grid.js'
|
||||
import DashboardItem from "./Item.js";
|
||||
import { useCachedWidgetLoader } from "../../composables/Dashboard/CachedWidgetLoader.js";
|
||||
import WidgetIcon from "./Widget/WidgetIcon.js"
|
||||
|
||||
import dragClick from '../../directives/dragClick.js';
|
||||
|
||||
import ObjectUtils from "../../helpers/ObjectUtils.js";
|
||||
|
||||
export default {
|
||||
name: 'Section',
|
||||
components: {
|
||||
@@ -11,8 +14,11 @@ export default {
|
||||
DashboardItem,
|
||||
WidgetIcon,
|
||||
},
|
||||
directives: {
|
||||
dragClick
|
||||
},
|
||||
inject: {
|
||||
widgetsSetup:{
|
||||
widgetsSetup: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
@@ -39,9 +45,8 @@ export default {
|
||||
configOpened: false,
|
||||
gridWidth: 1,
|
||||
gridHeight: null,
|
||||
draggedItem:null,
|
||||
additionalRow:false,
|
||||
}
|
||||
additionalRow: false
|
||||
};
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
@@ -49,22 +54,40 @@ export default {
|
||||
this.editModeIsActive
|
||||
),
|
||||
sectionName: Vue.computed(() => this.name),
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
computedWidgetsSetup(){
|
||||
if(!this.widgetsSetup) return {};
|
||||
return this.widgetsSetup.reduce((acc, setup)=>{
|
||||
acc[setup.widget_id] = setup.setup;
|
||||
sectionNameTranslation() {
|
||||
switch (this.name) {
|
||||
case "general":
|
||||
return this.$p.t('dashboard', this.name);
|
||||
case "custom":
|
||||
return this.$p.t('dashboard', this.name);
|
||||
default:
|
||||
return this.name;
|
||||
}
|
||||
},
|
||||
showSectionInformation() {
|
||||
switch (this.name) {
|
||||
case "general":
|
||||
return this.$p.t('dashboard', 'dashboardGeneralSectionDescription');
|
||||
case "custom":
|
||||
return this.$p.t('dashboard', 'dashboardCustomSectionDescription');
|
||||
default:
|
||||
return this.$p.t('dashboard', 'dashboardSectionDescription', [this.name]);
|
||||
}
|
||||
},
|
||||
indexedWidgetsTemplates() {
|
||||
if (!this.widgetsSetup)
|
||||
return {};
|
||||
return this.widgetsSetup.reduce((acc, setup) => {
|
||||
acc[setup.widget_id] = setup;
|
||||
return acc;
|
||||
},{})
|
||||
}, {});
|
||||
},
|
||||
editModeIsActive() {
|
||||
return (this.editMode || this.adminMode) && !this.configOpened
|
||||
},
|
||||
getSectionStyle() {
|
||||
return 'margin-bottom: 8px;';
|
||||
},
|
||||
items() {
|
||||
// reuses the nearest placement of the widget from another viewport
|
||||
/* const computeNearestPlace = (item, gridWidth) =>{
|
||||
@@ -85,76 +108,43 @@ export default {
|
||||
if(!item?.widgetid && item?.id){
|
||||
item.widgetid = item.id;
|
||||
}
|
||||
return { ...item, reorder: false, ...(item.place[this.gridWidth] || { reorder: true, ...{ x: 0, y: 0, w: 1, h: 1 } })};
|
||||
|
||||
let weight = 5;
|
||||
if (!item.source)
|
||||
weight = 6;
|
||||
else if (item.source == 'general')
|
||||
weight = 4;
|
||||
|
||||
let placement = item.place[this.gridWidth];
|
||||
if (!placement) {
|
||||
weight -= 3;
|
||||
placement = {};
|
||||
}
|
||||
|
||||
return { ...item, ...placement, weight };
|
||||
});
|
||||
return placedItems;
|
||||
|
||||
},
|
||||
|
||||
|
||||
return placedItems.filter(item => !item.hidden);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
items() {
|
||||
this.additionalRow = false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sectionNameTranslation(){
|
||||
switch(this.name){
|
||||
case "general":
|
||||
return this.$p.t('dashboard',this.name);
|
||||
break;
|
||||
case "custom":
|
||||
return this.$p.t('dashboard',this.name);
|
||||
break;
|
||||
default:
|
||||
return this.name;
|
||||
break;
|
||||
}
|
||||
},
|
||||
showSectionInformation(){
|
||||
if (this.name == "general"){
|
||||
return this.$p.t('dashboard', 'dashboardGeneralSectionDescription');
|
||||
}
|
||||
else if(this.name == "custom"){
|
||||
return this.$p.t('dashboard', 'dashboardCustomSectionDescription');
|
||||
}
|
||||
else{
|
||||
return this.$p.t('dashboard', 'dashboardSectionDescription', [this.name]);
|
||||
}
|
||||
},
|
||||
handleConfigOpened() {
|
||||
this.configOpened = true
|
||||
},
|
||||
handleConfigClosed() {
|
||||
this.configOpened = false
|
||||
},
|
||||
checkResizeLimit(item, w, h) {
|
||||
// NOTE(chris): widgets needs to be loaded for this to work
|
||||
let widget = this.widgetState[item.widget];
|
||||
if (widget) {
|
||||
let minmaxW = { ...widget.setup.width };
|
||||
if (minmaxW.max)
|
||||
minmaxW.min = minmaxW.min || 1;
|
||||
else
|
||||
minmaxW = { min: minmaxW, max: minmaxW };
|
||||
if (w < minmaxW.min)
|
||||
w = minmaxW.min;
|
||||
if (w > minmaxW.max)
|
||||
w = minmaxW.max;
|
||||
|
||||
let minmaxH = { ...widget.setup.height };
|
||||
if (minmaxH.max)
|
||||
minmaxH.min = minmaxH.min || 1;
|
||||
else
|
||||
minmaxH = { min: minmaxH, max: minmaxH };
|
||||
if (h < minmaxH.min)
|
||||
h = minmaxH.min;
|
||||
if (h > minmaxH.max)
|
||||
h = minmaxH.max;
|
||||
}
|
||||
return [w, h];
|
||||
},
|
||||
removeWidget(item, revert) {
|
||||
removeWidget(item) {
|
||||
if (item.custom) {
|
||||
BsConfirm.popup(this.$p.t('dashboard', 'alert_deleteWidget')).then(() => this.$emit('widgetRemove', this.name, item.id));
|
||||
BsConfirm.popup(this.$p.t('dashboard', 'alert_deleteWidget')).then(() => this.$emit('widgetRemove', item.id, this.name));
|
||||
} else {
|
||||
let update = {};
|
||||
update[item.id] = { hidden: !revert };
|
||||
update[item.id] = { hidden: true };
|
||||
this.updatePreset(update);
|
||||
}
|
||||
},
|
||||
@@ -163,49 +153,42 @@ export default {
|
||||
payload[item.id] = { config };
|
||||
this.updatePreset(payload);
|
||||
},
|
||||
updatePositions(updated, pinned=false) {
|
||||
updatePositions(updated) {
|
||||
let result = {};
|
||||
updated.forEach(update => {
|
||||
|
||||
let item = {...update.item};
|
||||
if (!item.placeholder) {
|
||||
if (!item.place[this.gridWidth])
|
||||
item.place[this.gridWidth] = {x: 0, y: 0, w: 1, h: 1};
|
||||
delete item.x;
|
||||
delete item.y;
|
||||
delete item.w;
|
||||
delete item.h;
|
||||
delete item.place[this.gridWidth].pinned;
|
||||
if (update.x !== undefined)
|
||||
item.place[this.gridWidth].x = update.x;
|
||||
if (update.y !== undefined)
|
||||
item.place[this.gridWidth].y = update.y;
|
||||
if (update.w !== undefined)
|
||||
item.place[this.gridWidth].w = update.w;
|
||||
if (update.h !== undefined)
|
||||
item.place[this.gridWidth].h = update.h;
|
||||
if (pinned){
|
||||
item.place[this.gridWidth].pinned = true;
|
||||
}
|
||||
let item = structuredClone(ObjectUtils.deepToRaw(update.item));
|
||||
|
||||
result[item.id] = item;
|
||||
if (!item.placeholder) {
|
||||
if (!item.place[this.gridWidth])
|
||||
item.place[this.gridWidth] = { x: 0, y: 0, w: 1, h: 1 };
|
||||
|
||||
delete item.x;
|
||||
delete item.y;
|
||||
delete item.w;
|
||||
delete item.h;
|
||||
delete item.pinned;
|
||||
delete item.weight;
|
||||
|
||||
if (update.x !== undefined)
|
||||
item.place[this.gridWidth].x = update.x;
|
||||
if (update.y !== undefined)
|
||||
item.place[this.gridWidth].y = update.y;
|
||||
if (update.w !== undefined)
|
||||
item.place[this.gridWidth].w = update.w;
|
||||
if (update.h !== undefined)
|
||||
item.place[this.gridWidth].h = update.h;
|
||||
if (update.pinned !== undefined)
|
||||
item.place[this.gridWidth].pinned = update.pinned;
|
||||
|
||||
result[item.id] = item;
|
||||
}
|
||||
});
|
||||
this.updatePreset(result);
|
||||
},
|
||||
updatePreset(update) {
|
||||
let payload = {};
|
||||
payload[this.name] = update;
|
||||
this.$emit('widgetUpdate', this.name, payload);
|
||||
this.$emit('widgetUpdate', update, this.name);
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { state: widgetState } = useCachedWidgetLoader();
|
||||
|
||||
return {
|
||||
widgetState
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
let self = this;
|
||||
let cont = self.$refs.container;
|
||||
@@ -215,44 +198,61 @@ export default {
|
||||
self.gridWidth = parseInt(window.getComputedStyle(cont).getPropertyValue('--fhc-dashboard-grid-size'));
|
||||
});
|
||||
},
|
||||
template: `
|
||||
<div class="dashboard-section position-relative pb-3 border-bottom" ref="container" :style="getSectionStyle">
|
||||
<h4 v-if="editModeIsActive" class=" mb-2">
|
||||
<i v-tooltip="showSectionInformation(name)" class="fa-solid fa-circle-info section-info" ></i>
|
||||
{{sectionNameTranslation()}}:
|
||||
</h4>
|
||||
<button v-tooltip="$p.t('dashboard','addLine')" v-if="!additionalRow && editModeIsActive" @click="additionalRow=true" class="btn btn-outline-secondary rounded-circle newGridRow d-flex justify-content-center align-items-center">+</button>
|
||||
<drop-grid v-model:cols="gridWidth" v-model:additionalRow="additionalRow" :items="items" :itemsSetup="computedWidgetsSetup" :active="editModeIsActive" :resize-limit="checkResizeLimit" :margin-for-extra-row=".01" @draggedItem="draggedItem=$event" @rearrange-items="updatePositions" @gridHeight="gridHeight=$event" >
|
||||
template: /* html */`
|
||||
<section
|
||||
class="dashboard-section position-relative pb-3 mb-3 border-bottom"
|
||||
ref="container"
|
||||
:class="{ 'edit-active': editModeIsActive }"
|
||||
>
|
||||
<h3 v-if="adminMode" class="h4">
|
||||
<i v-tooltip="showSectionInformation" class="fa-solid fa-circle-info section-info"></i>
|
||||
{{ sectionNameTranslation }}:
|
||||
</h3>
|
||||
<button
|
||||
v-tooltip="$p.t('dashboard/addLine')"
|
||||
v-if="!additionalRow && editModeIsActive"
|
||||
class="btn btn-outline-secondary rounded-circle newGridRow d-flex justify-content-center align-items-center"
|
||||
@click="additionalRow=true"
|
||||
v-drag-click="() => additionalRow=true"
|
||||
>+</button>
|
||||
<drop-grid
|
||||
v-model:cols="gridWidth"
|
||||
:additional-row="additionalRow"
|
||||
:items="items"
|
||||
:items-setup="indexedWidgetsTemplates"
|
||||
:active="editModeIsActive"
|
||||
@rearrange-items="updatePositions"
|
||||
@grid-height="gridHeight=$event"
|
||||
>
|
||||
<template #default="item">
|
||||
<div v-if="item.placeholder" class="empty-tile-hover" @pointerdown="$emit('widgetAdd', name, { widget: 1, config: {}, place: {[gridWidth]: {x:item.x,y:item.y,w:1,h:1}}, custom: 1 })"></div>
|
||||
<div
|
||||
v-if="item.placeholder"
|
||||
class="empty-tile-hover"
|
||||
@click="$emit('widgetAdd', { config: [], place: {[gridWidth]: { x: item.x, y: item.y, w: 1, h: 1 } }, custom: 1 }, name)"
|
||||
></div>
|
||||
<dashboard-item
|
||||
v-else
|
||||
:id="item.widget"
|
||||
:dragstate="item.blank || (item.widgetid && item.widgetid == draggedItem?.data.widgetid)"
|
||||
:resizeOverlay="item.resizeOverlay"
|
||||
:widgetID="item.id"
|
||||
:width="item.w"
|
||||
:height="item.h"
|
||||
:item_data="{config:item.config, custom:item.custom, h:item.h, w:item.w,id:item.id,reorder:item.reorder,place:item.place,widget:item.widget,widgetid:item.widgetid,x:item.x,y:item.y}"
|
||||
:item_data="{config:item.config, custom:item.custom, h:item.h, w:item.w,id:item.id,place:item.place,widget:item.widget,widgetid:item.widgetid,x:item.x,y:item.y}"
|
||||
:loading="item.loading"
|
||||
:config="item.config"
|
||||
:custom="item.custom"
|
||||
:hidden="item.hidden"
|
||||
:editMode="editModeIsActive"
|
||||
:place="item.place[gridWidth]"
|
||||
:setup="computedWidgetsSetup[item.widget]"
|
||||
:widget-template="indexedWidgetsTemplates[item.widget]"
|
||||
:source="adminMode ? null : item.source || 'custom'"
|
||||
@change="saveConfig($event, item)"
|
||||
@remove="removeWidget(item, $event)"
|
||||
@remove="removeWidget(item)"
|
||||
@config-opened="handleConfigOpened"
|
||||
@config-closed="handleConfigClosed"
|
||||
@pinItem="updatePositions($event,true)"
|
||||
@unPinItem="updatePositions">
|
||||
</dashboard-item>
|
||||
|
||||
@pin-item="updatePositions"
|
||||
@un-pin-item="updatePositions"
|
||||
></dashboard-item>
|
||||
</template>
|
||||
|
||||
</drop-grid>
|
||||
</div>`
|
||||
</section>`
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user