Compare commits

..

7 Commits

52 changed files with 1902 additions and 2336 deletions
+1 -1
View File
@@ -8,4 +8,4 @@ if (! defined('BASEPATH')) exit('No direct script access allowed');
$config['theme_name']='default';
$config['theme_css']= "public/css/theme/default.css";
$config['theme_logo']= "public/images/logo_fh-complete_300x46.png";
$config['theme_modes']=['light','dark'];
$config['theme_modes']=['light','dark','contrast'];
+1 -1
View File
@@ -40,7 +40,7 @@ class Auth extends FHC_Controller
if ($this->form_validation->run())
{
redirect($this->authlib->getLandingPage('/Cis4'));
redirect($this->authlib->getLandingPage('/CisVue/Dashboard'));
}
else
{
@@ -0,0 +1,43 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
*
*/
class Dashboard extends Auth_Controller
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct(
array(
'index' => 'dashboard/benutzer:r'
)
);
}
// -----------------------------------------------------------------------------------------------------------------
// Public methods
/**
* @return void
*/
public function index()
{
$this->load->model('person/Person_model','PersonModel');
$personData = getData($this->PersonModel->getByUid(getAuthUID()))[0];
$viewData = array(
'uid' => getAuthUID(),
'name' => $personData->vorname,
'person_id' => $personData->person_id
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData]);
}
}
@@ -40,32 +40,11 @@ class Board extends FHCAPI_Controller
public function list()
{
$this->DashboardModel->addSelect('dashboard_id');
$this->DashboardModel->addSelect('dashboard_kurzbz');
$this->DashboardModel->addSelect('tbl_dashboard.beschreibung');
$this->DashboardModel->addSelect("(
SELECT json_agg(w.*)
FROM dashboard.tbl_widget w
JOIN dashboard.tbl_dashboard_widget dw
USING(widget_id)
WHERE dw.dashboard_id=tbl_dashboard.dashboard_id
) AS \"widgetSetup\"");
$result = $this->DashboardModel->load();
$data = $this->getDataOrTerminateWithError($result);
$data = array_map(function ($dashboard) {
$tmpSetups = json_decode($dashboard->widgetSetup);
$tmpSetups = array_map(function ($widget) {
$widget->setup->file = absoluteJsImportUrl($widget->setup->file);
return $widget;
}, $tmpSetups);
$dashboard->widgetSetup = $tmpSetups;
return $dashboard;
}, $data);
$this->terminateWithSuccess($data);
$this->terminateWithSuccess($result);
}
public function create()
@@ -103,7 +82,7 @@ class Board extends FHCAPI_Controller
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($data);
$this->terminateWithSuccess($result);
}
public function delete()
@@ -137,6 +116,6 @@ class Board extends FHCAPI_Controller
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($data);
$this->terminateWithSuccess($result);
}
}
@@ -120,7 +120,10 @@ class Preset extends FHCAPI_Controller
$conf = $this->dashboardlib->getPreset($db, $funktion);
if ($conf) {
$preset = json_decode($conf->preset, true);
$result[$funktion] = $preset;
if (!isset($preset[$funktion]) || !isset($preset[$funktion]['widgets']))
$result[$funktion] = [];
else
$result[$funktion] = $preset[$funktion]['widgets'];
} else {
$result[$funktion] = [];
}
@@ -151,7 +154,7 @@ class Preset extends FHCAPI_Controller
$preset_decoded = json_decode($preset->preset, true);
$preset_decoded[$widget['widgetid']] = $widget;
$this->dashboardlib->addWidgetsToWidgets($preset_decoded, $dashboard_kurzbz, $funktion_kurzbz, [$widget]);
$preset->preset = json_encode($preset_decoded);
@@ -183,10 +186,8 @@ class Preset extends FHCAPI_Controller
$preset_decoded = json_decode($preset->preset, true);
if (!isset($preset_decoded[$widgetid]))
if (!$this->dashboardlib->removeWidgetFromWidgets($preset_decoded, $funktion_kurzbz, $widgetid))
show_404();
unset($preset_decoded[$widgetid]);
$preset->preset = json_encode($preset_decoded);
@@ -48,9 +48,25 @@ class User extends FHCAPI_Controller
$uid = $this->authlib->getAuthObj()->username;
$mergedconfig = $this->dashboardlib->getMergedUserConfig($dashboard->dashboard_id, $uid);
/*$mergedconfig = $this->dashboardlib->getMergedConfig($dashboard->dashboard_id, $uid);
$this->terminateWithSuccess($mergedconfig);
$this->terminateWithSuccess([
'general' => call_user_func_array(
'array_merge_recursive',
$mergedconfig
)
]);*/
$defaultconfig = $this->dashboardlib->getDefaultConfig($dashboard->dashboard_id);
$userconfig = $this->dashboardlib->getUserConfig($dashboard->dashboard_id, $uid);
$defaultconfig_squashed = $defaultconfig ? call_user_func_array('array_replace_recursive', $defaultconfig) : [];
$userconfig_squashed = $userconfig ? call_user_func_array('array_replace_recursive', $userconfig) : [];
$mergedconfig = array_replace_recursive($defaultconfig_squashed, $userconfig_squashed);
$this->terminateWithSuccess([
DashboardLib::SECTION_IF_FUNKTION_KURZBZ_IS_NULL => $mergedconfig
]);
}
public function addWidget()
@@ -70,15 +86,26 @@ class User extends FHCAPI_Controller
if (!isset($widget['widgetid']))
$widget['widgetid'] = $this->dashboardlib->generateWidgetId($dashboard_kurzbz);
if (isset($widget['source']))
unset($widget['source']);
$override = $this->dashboardlib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid);
$override_decoded = json_decode($override->override, true);
$override_decoded[$widget['widgetid']] = $widget;
if (!isset($override_decoded['general']) || !is_array($override_decoded['general']))
$override_decoded['general'] = [];
if (!isset($override_decoded['general']['widgets']))
$override_decoded['general']['widgets'] = [];
$override_decoded['general']['widgets'][$widget['widgetid']] = $widget;
// NOTE(chris): remove doubles in other funktionen
foreach ($override_decoded as $funktion => $array) {
if ($funktion == 'general')
continue;
if (isset($array['widgets']) && isset($array['widgets'][$widget['widgetid']]))
unset($override_decoded[$funktion]['widgets'][$widget['widgetid']]);
}
$override->override = json_encode($override_decoded);
$result = $this->dashboardlib->insertOrUpdateOverride($override);
@@ -108,10 +135,18 @@ class User extends FHCAPI_Controller
$override_decoded = json_decode($override->override, true);
if (!isset($override_decoded[$widget_id]))
show_404();
unset($override_decoded[$widget_id]);
foreach (array_keys($override_decoded) as $k) {
if (!isset($override_decoded[$k]["widgets"])) {
unset($override_decoded[$k]);
continue;
}
if (isset($override_decoded[$k]["widgets"][$widget_id])) {
unset($override_decoded[$k]["widgets"][$widget_id]);
}
if (!$override_decoded[$k]["widgets"]) {
unset($override_decoded[$k]);
}
}
$override->override = json_encode($override_decoded);
@@ -37,9 +37,7 @@ class DashboardLib
public function getDashboardByKurzbz($dashboard_kurzbz)
{
$result = $this->_ci->DashboardModel->loadWhere([
'dashboard_kurzbz' => $dashboard_kurzbz
]);
$result = $this->_ci->DashboardModel->getDashboardByKurzbz($dashboard_kurzbz);
if (hasData($result))
{
@@ -49,21 +47,17 @@ class DashboardLib
return null;
}
public function getMergedUserConfig($dashboard_id, $uid)
public function getMergedConfig($dashboard_id, $uid)
{
$defaultconfig = $this->getUserBaseConfig($dashboard_id);
$userconfig = $this->getUserOverrideConfig($dashboard_id, $uid);
$defaultconfig = $this->getDefaultConfig($dashboard_id);
$userconfig = $this->getUserConfig($dashboard_id, $uid);
$sourceconfig = array_map(function ($value) {
return ['source' => $value['source']];
}, $defaultconfig);
$mergedconfig = array_replace_recursive($defaultconfig, $userconfig, $sourceconfig);
$mergedconfig = array_replace_recursive($defaultconfig, $userconfig);
return $mergedconfig;
}
protected function getUserBaseConfig($dashboard_id)
public function getDefaultConfig($dashboard_id)
{
$funktion_kurzbzs = [];
$rights = $this->_ci->permissionlib->getAccessRights();
@@ -93,11 +87,7 @@ class DashboardLib
$preset = json_decode($presetobj->preset, true);
if (null !== $preset)
{
$preset = array_map(function ($value) use ($presetobj) {
$value['source'] = $presetobj->funktion_kurzbz ?: self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
return $value;
}, $preset);
$defaultconfig = array_merge_recursive($defaultconfig, $preset);
$defaultconfig = array_replace_recursive($defaultconfig, $preset);
}
}
}
@@ -105,7 +95,7 @@ class DashboardLib
return $defaultconfig;
}
protected function getUserOverrideConfig($dashboard_id, $uid)
public function getUserConfig($dashboard_id, $uid)
{
$res_userconfig = $this->_ci->DashboardOverrideModel->getOverride($dashboard_id, $uid);
@@ -134,7 +124,7 @@ class DashboardLib
$emptyoverride = new stdClass();
$emptyoverride->dashboard_id = $dashboard->dashboard_id;
$emptyoverride->uid = $uid;
$emptyoverride->override = '[]';
$emptyoverride->override = '{"' . self::USEROVERRIDE_SECTION . '": {"widgets":{}}, "custom": { "widgets" : {}}}';
return $emptyoverride;
}
@@ -153,7 +143,8 @@ class DashboardLib
$emptypreset = new stdClass();
$emptypreset->dashboard_id = $dashboard->dashboard_id;
$emptypreset->funktion_kurzbz = $funktion_kurzbz;
$emptypreset->preset = '[]';
$section = ($funktion_kurzbz !== null) ? $funktion_kurzbz : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
$emptypreset->preset = '{"' . $section . '": { "widgets" : {}},"custom": { "widgets" : {}}}';
return $emptypreset;
}
@@ -218,4 +209,44 @@ class DashboardLib
return $result;
}
public function addWidgetsToWidgets(&$widgets, $dashboard_kurzbz, $section, $addwigets)
{
foreach ($addwigets as $widget)
{
if(!isset($widget['widgetid']))
{
$widget['widgetid'] = $this->generateWidgetId($dashboard_kurzbz);
}
$this->addWidgetToWidgets($widgets, $section, $widget, $widget['widgetid']);
}
}
public function addWidgetToWidgets(&$widgets, $section, $widget, $widgetid)
{
$section = ($section !== null) ? $section : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
if (!isset($widgets[$section]) || !isset($widgets[$section]["widgets"]) || !is_array($widgets[$section]))
{
$widgets[$section] = array();
$widgets[$section]["widgets"] = array();
}
$widgets[$section]["widgets"][$widgetid] = $widget;
}
public function removeWidgetFromWidgets(&$widgets, $section, $widgetid)
{
$section = ($section !== null) ? $section : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
if (isset($widgets[$section]) && isset($widgets[$section]["widgets"][$widgetid]))
{
unset($widgets[$section]["widgets"][$widgetid]);
if(empty($widgets[$section]["widgets"]) && $section !== self::USEROVERRIDE_SECTION) {
unset($widgets[$section]);
}
return true;
}
else {
return false;
}
}
}
@@ -11,4 +11,15 @@ class Dashboard_model extends DB_Model
$this->dbTable = 'dashboard.tbl_dashboard';
$this->pk = 'dashboard_id';
}
/**
* Get Dashboard by kurzbz.
* @param string dashboard_kurzbz
* @return array
*/
public function getDashboardByKurzbz($dashboard_kurzbz)
{
return $this->loadWhere(array('dashboard_kurzbz' => $dashboard_kurzbz));
}
}
@@ -39,7 +39,7 @@ $includesArray = array(
'vendor/moment/luxonjs/luxon.min.js'
),
'customJSModules' => array(
'public/js/apps/Cis.js',
'public/js/apps/Dashboard/Fhc.js',
),
);
@@ -6,7 +6,7 @@ $includesArray = array(
'fontawesome6' => true,
'axios027' => true,
'customJSModules' => array_merge([
'public/js/apps/Cis/Menu.js'
'public/js/apps/Cis.js'
], $customJSModules ?? []),
'customCSSs' => array_merge([
'public/css/Cis4/Cis.css'
@@ -8,7 +8,7 @@ $includesArray = array(
'axios027' => true,
'primevue3' => true,
'customJSModules' => array_merge([
'public/js/apps/Cis/Menu.js'
'public/js/apps/Cis.js'
], $customJSModules ?? []),
'customCSSs' => array_merge([
'public/css/Cis4/Cis.css',
-14
View File
@@ -70,18 +70,6 @@
}
}
},
{
"type": "package",
"package": {
"name": "drag-drop-touch-js/dragdroptouch",
"version": "2.0.3",
"source": {
"url": "https://github.com/drag-drop-touch-js/dragdroptouch.git",
"type": "git",
"reference": "master"
}
}
},
{
"type": "package",
"package": {
@@ -464,8 +452,6 @@
"easyrdf/easyrdf": "0.9.*",
"drag-drop-touch-js/dragdroptouch": "*",
"fgelinas/timepicker": "0.3.3",
"fortawesome/font-awesome4": "4.7.*",
"fortawesome/font-awesome6": "6.1.*",
Generated
+1 -11
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "869cbc35bd1ba90ab90934fcb41b0f51",
"content-hash": "f4f0af4586f46f97d8b6092c1ac0fb3a",
"packages": [
{
"name": "afarkas/html5shiv",
@@ -804,16 +804,6 @@
"abandoned": true,
"time": "2018-03-09T06:07:41+00:00"
},
{
"name": "drag-drop-touch-js/dragdroptouch",
"version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/drag-drop-touch-js/dragdroptouch.git",
"reference": "master"
},
"type": "library"
},
{
"name": "easyrdf/easyrdf",
"version": "0.9.1",
+37 -19
View File
@@ -41,30 +41,34 @@ html {
color: var(--fhc-primary-highlight) !important;
}
.fhc-body-bg{
background-color:var(--fhc-background) !important;
}
.fhc-body-color{
color: var(--fhc-text) !important;
.fhc-primary-text{
color: var(--fhc-light) !important;
}
.fhc-secondary-bg{
background-color: var(--fhc-tertiary) !important;
}
.fhc-secondary-color {
color: var(--fhc-tertiary) !important;
}
.fhc-tertiary-bg{
background-color: var(--fhc-secondary) !important;
}
.fhc-tertiary-color {
.fhc-secondary-color {
color: var(--fhc-secondary) !important;
}
.fhc-secondary-text{
color: var(--fhc-secondary-text) !important;
}
.fhc-tertiary-bg{
background-color: var(--fhc-tertiary) !important;
}
.fhc-tertiary-color {
color: var(--fhc-tertiary) !important;
}
.fhc-tertiary-text{
color: var(--fhc-tertiary-text) !important;
}
.fhc-fourth-bg {
background-color: var(--fhc-fourth) !important;
}
@@ -73,6 +77,10 @@ html {
color: var(--fhc-fourth) !important;
}
.fhc-fourth-text {
color: var(--fhc-fourth-text) !important;
}
.fhc-fifth-bg {
background-color: var(--fhc-fifth) !important;
}
@@ -81,12 +89,22 @@ html {
color: var(--fhc-fifth) !important;
}
.fhc-fifth-text {
color: var(--fhc-fifth-text) !important;
}
.fhc-body{
color:var(--fhc-text);
background-color: var(--fhc-background);
border-color: var(--fhc-border);
}
.fhc-body-highlight {
color: var(--fhc-text);
background-color: var(--fhc-background-highlight);
border-color: var(--fhc-border);
}
.fhc-secondary {
color: var(--fhc-text);
background-color: var(--fhc-tertiary);
@@ -127,22 +145,22 @@ html {
--fhc-cis-menu-lvl-1-color-hover: var(--fhc-light);
--fhc-cis-menu-lvl-2-bg: var(--fhc-secondary);
--fhc-cis-menu-lvl-2-color: var(--fhc-text);
--fhc-cis-menu-lvl-2-color: var(--fhc-secondary-text);
--fhc-cis-menu-lvl-2-bg-hover: var(--fhc-secondary-highlight);
--fhc-cis-menu-lvl-2-color-hover: var(--fhc-text);
--fhc-cis-menu-lvl-3-bg: var(--fhc-tertiary);
--fhc-cis-menu-lvl-3-color: var(--fhc-text);
--fhc-cis-menu-lvl-3-color: var(--fhc-secondary-text);
--fhc-cis-menu-lvl-3-bg-hover: var(--fhc-tertiary-highlight);
--fhc-cis-menu-lvl-3-color-hover: var(--fhc-text);
--fhc-cis-menu-lvl-4-bg: var(--fhc-fourth);
--fhc-cis-menu-lvl-4-color: var(--fhc-text);
--fhc-cis-menu-lvl-4-color: var(--fhc-secondary-text);
--fhc-cis-menu-lvl-4-bg-hover: var(--fhc-fourth-highlight);
--fhc-cis-menu-lvl-4-color-hover: var(--fhc-text);
--fhc-cis-menu-lvl-5-bg: var(--fhc-fifth);
--fhc-cis-menu-lvl-5-color: var(--fhc-text);
--fhc-cis-menu-lvl-5-color: var(--fhc-secondary-text);
--fhc-cis-menu-lvl-5-bg-hover: var(--fhc-fifth-highlight);
--fhc-cis-menu-lvl-5-color-hover: var(--fhc-text);
--fhc-cis-grade-positive: var(--fhc-success);
+1 -1
View File
@@ -14,7 +14,7 @@
--fhc-calendar-border-event: var(--bs-secondary, #6c757d);
--fhc-calendar-radius-event: var(--bs-border-radius, .375rem);
--fhc-calendar-bg-markings-past: var(--fhc-beige-10, rgba(245, 233, 215, 0.5));
--fhc-calendar-bg-markings-past: var(--fhc-outdated, rgba(245, 233, 215, 0.5));
--fhc-calendar-border-markings-past: var(--fhc-calendar-border);
--fhc-calendar-bg-markings-past-label: var(--fhc-calendar-bg);
--fhc-calendar-border-markings-past-label: var(--fhc-calendar-border);
+52 -60
View File
@@ -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,16 +17,22 @@
--fhc-dashboard-section-info-color-hover: var(--fhc-primary-highlight, #005585);
}
.core-dashboard a {
@media(max-width: 577px) {
:root {
--fhc-dashboard-grid-size: 1;
}
}
.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;
@@ -40,36 +46,27 @@
background-repeat: no-repeat;
background-position: center;
background-size: cover;
cursor: pointer;
cursor:pointer;
}
.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%);
.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 {
@@ -77,6 +74,16 @@
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;
@@ -98,7 +105,6 @@
@media(max-width: 577px) {
:root {
--fhc-dashboard-grid-size: 1;
--fhc-dg-item-py: .75rem;
}
}
@@ -126,64 +132,50 @@
cursor: move !important;
}
.drop-grid-item-resize > .dashboard-item,
.drop-grid-item-move > .dashboard-item {
.draggedItem {
height: 100%;
width: 100%;
background-color: var(--fhc-dashboard-draggeditem-background);
position: relative;
}
.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 {
.dashboard-item-overlay{
background-color: var(--fhc-dashboard-item-overlay-background);
}
.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;
.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-oversized > .dashboard-item {
/* Bootstrap: border-danger */
--bs-border-opacity: 1;
border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important;
}
#deleteBookmark i {
#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);
}
.drop-grid-item-blocker [pinned='true'] {
.denied-dragging-animation {
animation: wiggle 0.5s linear;
color: var(--fhc-dashboard-denied-dragging-animation-color) !important;
}
@@ -212,13 +204,13 @@
}
.hidden-widget {
.hiddenWidget{
background: var(--fhc-disabled-background);
opacity: 40%;
}
.hidden-widget .card,
.hidden-widget .card-body,
.hidden-widget .card-body * {
.hiddenWidget .card,
.hiddenWidget .card-body,
.hiddenWidget .card-body *{
background: inherit !important;
}
+140 -297
View File
@@ -1,313 +1,156 @@
import FhcDashboard from '../components/Dashboard/Dashboard.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 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 MylvStudent from "../components/Cis/Mylv/Student.js";
import Profil from "../components/Cis/Profil/Profil.js";
import Raumsuche from "../components/Cis/Raumsuche/Raumsuche.js";
import CmsNews from "../components/Cis/Cms/News.js";
import CmsContent from "../components/Cis/Cms/Content.js";
import Info from "../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js";
import RoomInformation, {DEFAULT_MODE_RAUMINFO} from "../components/Cis/Mylv/RoomInformation.js";
import AbgabetoolStudent from "../components/Cis/Abgabetool/AbgabetoolStudent.js";
import AbgabetoolMitarbeiter from "../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js";
import AbgabetoolAssistenz from "../components/Cis/Abgabetool/AbgabetoolAssistenz.js";
import DeadlineOverview from "../components/Cis/Abgabetool/DeadlineOverview.js";
import Studium from "../components/Cis/Studium/Studium.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 router = VueRouter.createRouter({
history: VueRouter.createWebHistory(`/${ciPath}`),
routes: [
{
path: `/Cis/Studium`,
name: 'Studium',
component: Studium,
props: true
},
{
path: `/Cis/Profil/View/:uid`,
name: 'ProfilView',
component: Profil,
props: true
},
{
path: `/Cis/Profil`,
name: 'Profil',
component: Profil,
props: true
},
{
path: `/Cis/Abgabetool/Student/:student_uid_prop?`,
name: 'AbgabetoolStudent',
component: AbgabetoolStudent,
props: true
},
{
path: `/Cis/Abgabetool/Mitarbeiter`,
name: 'AbgabetoolMitarbeiter',
component: AbgabetoolMitarbeiter,
props: true
},
{
path: `/Cis/Abgabetool/Assistenz/:stg_kz_prop?`,
name: 'AbgabetoolAssistenz',
component: AbgabetoolAssistenz,
props: true
},
{
path: `/Cis/Abgabetool/Deadlines/:person_uid_prop?`,
name: 'DeadlineOverview',
component: DeadlineOverview,
props: true
},
{
path: `/Cis/Raumsuche`,
name: 'Raumsuche',
component: Raumsuche,
props: true
},
// Redirect old links to new format
{
path: "/CisVue/Cms/getRoomInformation/:ort_kurzbz",
name: "RoomInformationOld",
component: RoomInformation,
redirect: (to) => {
return { // redirect to longer Rauminfo url and map params
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,
focus_date: new Date().toISOString().split("T")[0]
},
};
},
},
{
path: `/CisVue/Cms/getRoomInformation/:mode/:focus_date/:ort_kurzbz`,
name: 'RoomInformation',
component: RoomInformation,
props: (route) => { // validate and set mode/focus date if for some reason missing
const validModes = ["Month", "Week", "Day"];
// check mode string
const mode = route.params.mode &&
validModes.includes(route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase())
? route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()
: DEFAULT_MODE_RAUMINFO;
// default to today date if not provided
const d = new Date(route.params.focus_date)
const focus_date = !isNaN(d) ? route.params.focus_date : new Date().toISOString().split("T")[0];
// for consistency reasons format the props into one object but actually use a new name to we dont collide with
// existing viewData declaration written from codeigniter 3 into routerview tag
return {
propsViewData: {
mode,
focus_date,
ort_kurzbz: route.params.ort_kurzbz
}
};
},
beforeEnter: (to, from, next) => {
// missing mode or focus_date -> set defaults
if (!to.params.mode || !to.params.focus_date) {
next({
name: "RoomInformation",
params: {
mode: to.params.mode || DEFAULT_MODE_RAUMINFO,
focus_date: to.params.focus_date || new Date().toISOString().split("T")[0],
ort_kurzbz: route.params.ort_kurzbz
}
});
} else {
next();
}
}
},
{
path: `/CisVue/Cms/Content/:content_id`,
name: 'Content',
component: CmsContent,
props: true
},
{
path: `/CisVue/Cms/News`,
name: 'News',
component: CmsNews,
props: true
},
{
path: `/Cis/MyLv/:studiensemester?`,
name: 'MyLv',
component: MylvStudent,
props: true,
},
{
path: `/Cis/MyLv/Info/:studien_semester/:lehrveranstaltung_id`,
name: 'LvInfo',
component: Info,
props: true
},
// Redirect old links to new format
{
// only trigger on first param being numeric to avoid paths like "LvPlan/Month" entering here
path: "/Cis/LvPlan/:lv_id(\\d+)",
name: "LvPlanOld",
component: LvPlan,
redirect(to) {
const route = Vue.unref(router.currentRoute);
const { mode, focus_date } = route.params; // keep mode and focus_date if available
return { // redirect to longer LvPlan url and map params
name: "LvPlan",
params: {
mode,
focus_date,
lv_id: to.params.lv_id
},
};
},
},
{
path: `/Cis/LvPlan/:mode?/:focus_date?/:lv_id?`,
name: 'LvPlan',
component: LvPlan,
props(route) {
return {
propsViewData: route.params
};
}
},
{
path: `/Cis/MyLvPlan/:mode?/:focus_date?`,
name: 'MyLvPlan',
component: MyLvPlan,
props(route) {
return {
propsViewData: route.params
};
}
},
{
path: `/Cis4`,
name: 'Cis4',
component: FhcDashboard,
props: {dashboard: 'CIS'},
},
{
path: `/`,
name: 'FhcDashboard',
component: FhcDashboard,
props: {dashboard: 'CIS'},
},
{
path: '/:pathMatch(.*)*',
name: 'Fallback',
component: FhcDashboard,
props: {dashboard: 'CIS'},
redirect: () => {
return {
name: "Cis4",
params: {
dashboard: 'CIS'
},
};
},
},
]
})
import ApiSearchbar from '../api/factory/searchbar.js';
import Theme from "../plugins/Theme.js";
const app = Vue.createApp({
name: 'CisApp',
data: () => ({
appSideMenuEntries: {}
}),
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),
isMobile: this.isMobile
}
},
methods: {
isInternalRoute(href) {
const internalBase = window.location.origin
return href.startsWith(internalBase);
},
handleClick(event) {
const target = event.target.closest('a');
name: 'CisApp',
components: {
FhcSearchbar,
CisMenu
},
data: function() {
return {
searchbaroptions: {
origin: "cis",
cssclass: "",
calcheightonly: true,
types: {
employee: Vue.computed(() => this.$p.t("search/type_employee")),
student: Vue.computed(() => this.$p.t("search/type_student")),
room: Vue.computed(() => this.$p.t("search/type_room")),
organisationunit: Vue.computed(() => this.$p.t("search/type_organisationunit")),
cms: Vue.computed(() => this.$p.t("search/type_cms")),
dms: Vue.computed(() => this.$p.t("search/type_dms"))
},
actions: {
employee: {
defaultaction: {
type: "link",
action: function(data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router+
"/Cis/Profil/View/"+data.uid;
}
},
childactions: []
},
student: {
defaultaction: {
type: "link",
action: function (data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/Cis/Profil/View/" + data.uid;
if(target?.id == 'skiplink') return
if (target && this.isInternalRoute(target.href)) {
const url = new URL(target.href)
const path = url.pathname
const base = this.$router.options.history.base
const route = path.replace(base, '') || '/'
// let click event propagate normally if we dont route internally
const res = this.$router.resolve(route)
if(!res?.matched?.length || res.name === 'Fallback') return
event.preventDefault(); // Prevent browser navigation
if(this.isMobile) { // toggle the menu
const navMain = document.getElementById('nav-main');
// fix unwanted toggle from off to on for some links on mobile
if(navMain.classList.contains('show')){
document.getElementById('nav-main-btn').click();
}
}
this.$router.push(route);
}
}
},
mounted() {
document.addEventListener('click', this.handleClick);
},
beforeUnmount() {
document.removeEventListener('click', this.handleClick);
},
}
},
childactions: []
},
room: {
defaultaction: {
type: "link",
renderif: function(data) {
if(data.content_id === null){
return false;
}
return true;
},
action: function(data) {
const link= FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
return link;
}
},
childactions: [
{
label: "LV-Plan",
icon: "fas fa-bookmark",
type: "link",
action: function(data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/getRoomInformation/' + data.ort_kurzbz;
return link;
}
},
{
label: "Rauminformation",
icon: "fas fa-info-circle",
type: "link",
renderif: function(data) {
if(data.content_id === null){
return false;
}
return true;
},
action: function(data) {
const link= FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
return link;
}
},
]
},
organisationunit: {
defaultaction: {
type: "link",
renderif: function(data) {
if(data.mailgroup) {
return true;
}
return false;
},
action: function(data) {
const link = 'mailto:' + data.mailgroup;
return link;
}
},
childactions: []
},
cms: {
defaultaction: {
type: "link",
action: function (data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
return link;
}
},
childactions: []
},
dms: {
defaultaction: {
type: "link",
action: function (data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
'cms/dms.php?id=' + data.dms_id;
return link;
}
},
childactions: []
}
}
}
};
},
methods: {
searchfunction: function(searchsettings) {
return this.$api.call(ApiSearchbar.searchCis(searchsettings));
}
}
});
// kind of a bandaid for bad css on some pages to avoid horizontal scroll
setScrollbarWidth();
app.config.globalProperties.$capitalize = capitalize;
FhcApps.router.makeExtendable(router);
FhcApps.makeExtendable(app);
app.use(router);
app.use(primevue.config.default, {
zIndex: {
overlay: 9000,
tooltip: 8000
}
})
app.directive('tooltip', primevue.tooltip);
app.use(PluginsPhrasen);
app.use(Theme);
app.directive('contrast', contrast);
app.mount('#fhccontent');
router.afterEach((to, from, failure) => {
app.config.globalProperties.$api.call(ApiRouteInfo.info('cis4', to.fullPath));
});
app.mount('#cis-header');
-156
View File
@@ -1,156 +0,0 @@
import FhcSearchbar from "../../components/searchbar/searchbar.js";
import CisMenu from "../../components/Cis/Menu.js";
import PluginsPhrasen from '../../plugins/Phrasen.js';
import ApiSearchbar from '../../api/factory/searchbar.js';
import Theme from "../../plugins/Theme.js";
const app = Vue.createApp({
name: 'CisMenuApp',
components: {
FhcSearchbar,
CisMenu
},
data: function() {
return {
searchbaroptions: {
origin: "cis",
cssclass: "",
calcheightonly: true,
types: {
employee: Vue.computed(() => this.$p.t("search/type_employee")),
student: Vue.computed(() => this.$p.t("search/type_student")),
room: Vue.computed(() => this.$p.t("search/type_room")),
organisationunit: Vue.computed(() => this.$p.t("search/type_organisationunit")),
cms: Vue.computed(() => this.$p.t("search/type_cms")),
dms: Vue.computed(() => this.$p.t("search/type_dms"))
},
actions: {
employee: {
defaultaction: {
type: "link",
action: function(data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router+
"/Cis/Profil/View/"+data.uid;
}
},
childactions: []
},
student: {
defaultaction: {
type: "link",
action: function (data) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/Cis/Profil/View/" + data.uid;
}
},
childactions: []
},
room: {
defaultaction: {
type: "link",
renderif: function(data) {
if(data.content_id === null){
return false;
}
return true;
},
action: function(data) {
const link= FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
return link;
}
},
childactions: [
{
label: "LV-Plan",
icon: "fas fa-bookmark",
type: "link",
action: function(data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/getRoomInformation/' + data.ort_kurzbz;
return link;
}
},
{
label: "Rauminformation",
icon: "fas fa-info-circle",
type: "link",
renderif: function(data) {
if(data.content_id === null){
return false;
}
return true;
},
action: function(data) {
const link= FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
return link;
}
},
]
},
organisationunit: {
defaultaction: {
type: "link",
renderif: function(data) {
if(data.mailgroup) {
return true;
}
return false;
},
action: function(data) {
const link = 'mailto:' + data.mailgroup;
return link;
}
},
childactions: []
},
cms: {
defaultaction: {
type: "link",
action: function (data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
'/CisVue/Cms/content/' + data.content_id;
return link;
}
},
childactions: []
},
dms: {
defaultaction: {
type: "link",
action: function (data) {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
'cms/dms.php?id=' + data.dms_id;
return link;
}
},
childactions: []
}
}
}
};
},
methods: {
searchfunction: function(searchsettings) {
return this.$api.call(ApiSearchbar.searchCis(searchsettings));
}
}
});
FhcApps.makeExtendable(app);
app.use(primevue.config.default, {
zIndex: {
overlay: 9000,
tooltip: 8000
}
})
app.use(PluginsPhrasen);
app.use(Theme);
app.mount('#cis-header');
+45 -1
View File
@@ -3,10 +3,13 @@ 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: {}
appSideMenuEntries: {},
renderers: null
}),
components: {
CoreNavigationCmpt,
@@ -14,8 +17,49 @@ 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(PluginsPhrasen);
+356
View File
@@ -0,0 +1,356 @@
import FhcDashboard from '../../components/Dashboard/Dashboard.js';
import PluginsPhrasen from '../../plugins/Phrasen.js';
import Theme from '../../plugins/Theme.js';
import contrast from '../../directives/contrast.js';
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js";
import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js";
import MyLvPlan from "../../components/Cis/LvPlan/Personal.js";
import MylvStudent from "../../components/Cis/Mylv/Student.js";
import Profil from "../../components/Cis/Profil/Profil.js";
import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js";
import CmsNews from "../../components/Cis/Cms/News.js";
import CmsContent from "../../components/Cis/Cms/Content.js";
import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js";
import RoomInformation, {DEFAULT_MODE_RAUMINFO} from "../../components/Cis/Mylv/RoomInformation.js";
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js";
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js";
import AbgabetoolAssistenz from "../../components/Cis/Abgabetool/AbgabetoolAssistenz.js";
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js";
import Studium from "../../components/Cis/Studium/Studium.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 router = VueRouter.createRouter({
history: VueRouter.createWebHistory(`/${ciPath}`),
routes: [
{
path: `/Cis/Studium`,
name: 'Studium',
component: Studium,
props: true
},
{
path: `/Cis/Profil/View/:uid`,
name: 'ProfilView',
component: Profil,
props: true
},
{
path: `/Cis/Profil`,
name: 'Profil',
component: Profil,
props: true
},
{
path: `/Cis/Abgabetool/Student/:student_uid_prop?`,
name: 'AbgabetoolStudent',
component: AbgabetoolStudent,
props: true
},
{
path: `/Cis/Abgabetool/Mitarbeiter`,
name: 'AbgabetoolMitarbeiter',
component: AbgabetoolMitarbeiter,
props: true
},
{
path: `/Cis/Abgabetool/Assistenz/:stg_kz_prop?`,
name: 'AbgabetoolAssistenz',
component: AbgabetoolAssistenz,
props: true
},
{
path: `/Cis/Abgabetool/Deadlines/:person_uid_prop?`,
name: 'DeadlineOverview',
component: DeadlineOverview,
props: true
},
{
path: `/Cis/Raumsuche`,
name: 'Raumsuche',
component: Raumsuche,
props: true
},
// Redirect old links to new format
{
path: "/CisVue/Cms/getRoomInformation/:ort_kurzbz",
name: "RoomInformationOld",
component: RoomInformation,
redirect: (to) => {
return { // redirect to longer Rauminfo url and map params
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,
focus_date: new Date().toISOString().split("T")[0]
},
};
},
},
{
path: `/CisVue/Cms/getRoomInformation/:mode/:focus_date/:ort_kurzbz`,
name: 'RoomInformation',
component: RoomInformation,
props: (route) => { // validate and set mode/focus date if for some reason missing
const validModes = ["Month", "Week", "Day"];
// check mode string
const mode = route.params.mode &&
validModes.includes(route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase())
? route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()
: DEFAULT_MODE_RAUMINFO;
// default to today date if not provided
const d = new Date(route.params.focus_date)
const focus_date = !isNaN(d) ? route.params.focus_date : new Date().toISOString().split("T")[0];
// for consistency reasons format the props into one object but actually use a new name to we dont collide with
// existing viewData declaration written from codeigniter 3 into routerview tag
return {
propsViewData: {
mode,
focus_date,
ort_kurzbz: route.params.ort_kurzbz
}
};
},
beforeEnter: (to, from, next) => {
// missing mode or focus_date -> set defaults
if (!to.params.mode || !to.params.focus_date) {
next({
name: "RoomInformation",
params: {
mode: to.params.mode || DEFAULT_MODE_RAUMINFO,
focus_date: to.params.focus_date || new Date().toISOString().split("T")[0],
ort_kurzbz: route.params.ort_kurzbz
}
});
} else {
next();
}
}
},
{
path: `/CisVue/Cms/Content/:content_id`,
name: 'Content',
component: CmsContent,
props: true
},
{
path: `/CisVue/Cms/News`,
name: 'News',
component: CmsNews,
props: true
},
{
path: `/Cis/MyLv/:studiensemester?`,
name: 'MyLv',
component: MylvStudent,
props: true,
},
{
path: `/Cis/MyLv/Info/:studien_semester/:lehrveranstaltung_id`,
name: 'LvInfo',
component: Info,
props: true
},
// Redirect old links to new format
{
// only trigger on first param being numeric to avoid paths like "LvPlan/Month" entering here
path: "/Cis/LvPlan/:lv_id(\\d+)",
name: "LvPlanOld",
component: LvPlan,
redirect(to) {
const route = Vue.unref(router.currentRoute);
const { mode, focus_date } = route.params; // keep mode and focus_date if available
return { // redirect to longer LvPlan url and map params
name: "LvPlan",
params: {
mode,
focus_date,
lv_id: to.params.lv_id
},
};
},
},
{
path: `/Cis/LvPlan/:mode?/:focus_date?/:lv_id?`,
name: 'LvPlan',
component: LvPlan,
props(route) {
return {
propsViewData: route.params
};
}
},
{
path: `/Cis/MyLvPlan/:mode?/:focus_date?`,
name: 'MyLvPlan',
component: MyLvPlan,
props(route) {
return {
propsViewData: route.params
};
}
},
{
path: `/Cis4`,
name: 'Cis4',
component: FhcDashboard,
props: {dashboard: 'CIS'},
},
{
path: `/`,
name: 'FhcDashboard',
component: FhcDashboard,
props: {dashboard: 'CIS'},
},
{
path: '/:pathMatch(.*)*',
name: 'Fallback',
component: FhcDashboard,
props: {dashboard: 'CIS'},
redirect: () => {
return {
name: "Cis4",
params: {
dashboard: 'CIS'
},
};
},
},
]
})
const app = Vue.createApp({
name: 'FhcApp',
data: () => ({
appSideMenuEntries: {},
renderers: null,
}),
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
}
},
methods: {
isInternalRoute(href) {
const internalBase = window.location.origin
return href.startsWith(internalBase);
},
handleClick(event) {
const target = event.target.closest('a');
if(target?.id == 'skiplink') return
if (target && this.isInternalRoute(target.href)) {
const url = new URL(target.href)
const path = url.pathname
const base = this.$router.options.history.base
const route = path.replace(base, '') || '/'
// let click event propagate normally if we dont route internally
const res = this.$router.resolve(route)
if(!res?.matched?.length || res.name === 'Fallback') return
event.preventDefault(); // Prevent browser navigation
if(this.isMobile) { // toggle the menu
const navMain = document.getElementById('nav-main');
// fix unwanted toggle from off to on for some links on mobile
if(navMain.classList.contains('show')){
document.getElementById('nav-main-btn').click();
}
}
this.$router.push(route);
}
}
},
async created(){
await this.$api
.call(ApiRenderers.loadRenderers())
.then(res => res.data)
.then(data => {
for (let rendertype of Object.keys(data)) {
let modalTitle = null;
let modalContent = null;
let calendarEvent = null;
if (data[rendertype].modalTitle)
modalTitle = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].modalTitle)));
if (data[rendertype].modalContent)
modalContent = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].modalContent)));
if (data[rendertype].calendarEvent)
calendarEvent = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].calendarEvent)));
if (data[rendertype].calendarEventStyles){
var head = document.head;
if(!head.querySelector(`link[href="${data[rendertype].calendarEventStyles}"]`)){
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = data[rendertype].calendarEventStyles;
head.appendChild(link);
}
}
if(this.renderers === null) {
this.renderers = {};
}
if (!this.renderers[rendertype]) {
this.renderers[rendertype] = {}
}
this.renderers[rendertype].modalTitle = modalTitle;
this.renderers[rendertype].modalContent = modalContent;
this.renderers[rendertype].calendarEvent = calendarEvent;
}
});
},
mounted() {
document.addEventListener('click', this.handleClick);
},
beforeUnmount() {
document.removeEventListener('click', this.handleClick);
},
});
// kind of a bandaid for bad css on some pages to avoid horizontal scroll
setScrollbarWidth();
app.config.globalProperties.$capitalize = capitalize;
FhcApps.router.makeExtendable(router);
FhcApps.makeExtendable(app);
app.use(router);
app.use(primevue.config.default, {
zIndex: {
overlay: 9000,
tooltip: 8000
}
})
app.directive('tooltip', primevue.tooltip);
app.use(PluginsPhrasen);
app.use(Theme);
app.directive('contrast', contrast);
app.mount('#fhccontent');
router.afterEach((to, from, failure) => {
app.config.globalProperties.$api.call(ApiRouteInfo.info('cis4', to.fullPath));
});
+16
View File
@@ -0,0 +1,16 @@
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');
@@ -20,8 +20,7 @@ export default {
},
inject: {
mode: "mode",
dropableEvents: "dropableEvents",
timezone: "timezone"
dropableEvents: "dropableEvents"
},
props: {
events: Array,
+8 -6
View File
@@ -3,7 +3,6 @@ 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';
@@ -14,7 +13,14 @@ export default {
components: {
FhcCalendar
},
inject: [
"renderers"
],
props: {
timezone: {
type: String,
required: true
},
date: {
type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local()
@@ -35,7 +41,6 @@ export default {
],
data() {
return {
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
modes: {
day: Vue.markRaw(ModeDay),
week: Vue.markRaw(ModeWeek),
@@ -94,13 +99,10 @@ export default {
context.emit('update:lv', newValue);
});
const { renderers } = useRenderers();
return {
rangeInterval,
events,
lv,
renderers
lv
};
},
created() {
+9 -7
View File
@@ -1,7 +1,6 @@
import FhcCalendar from "./Base.js";
import { useEventLoader } from '../../composables/EventLoader.js';
import { useRenderers } from '../../composables/Renderers.js';
import ModeList from '../Calendar/Mode/List.js';
@@ -10,17 +9,22 @@ 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 {
timezone,
now: luxon.DateTime.now().setZone(timezone),
now: luxon.DateTime.now().setZone(this.timezone),
modes: {
list: Vue.markRaw(ModeList)
},
@@ -55,12 +59,10 @@ export default {
const rangeInterval = Vue.ref(null);
const { events } = useEventLoader(rangeInterval, props.getPromiseFunc);
const { renderers } = useRenderers();
return {
rangeInterval,
events,
renderers
events
};
},
template: /* html */`
+1 -1
View File
@@ -103,7 +103,7 @@ export default {
<div class="row">
<div class="col" v-html="content">
</div>
<div class="col-auto">
<div class="col-auto " style="align-self:flex-start;">
<div style="width:15rem">
<studiengang-information></studiengang-information>
</div>
@@ -22,7 +22,7 @@ export default {
computed:{
currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
return luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
return this.propsViewData?.focus_date;
},
currentMode() {
@@ -95,6 +95,7 @@ export default {
<fhc-calendar
v-else-if="lv"
ref="calendar"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
+4 -2
View File
@@ -15,6 +15,7 @@ export default {
propsViewData: Object
},
data() {
const now = luxon.DateTime.now().setZone(this.viewData.timezone);
return {
studiensemester_kurzbz: null,
studiensemester_start: null,
@@ -25,7 +26,7 @@ export default {
},
computed:{
currentDay() {
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
},
currentMode() {
return this.propsViewData?.mode || DEFAULT_MODE_LVPLAN;
@@ -34,7 +35,7 @@ export default {
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
return false;
const opts = { zone: FHC_JS_DATA_STORAGE_OBJECT.timezone };
const opts = { zone: this.viewData.timezone };
const start = luxon.DateTime
.fromISO(this.studiensemester_start, opts)
.toUnixInteger();
@@ -114,6 +115,7 @@ export default {
<fhc-calendar
ref="calendar"
v-model:lv="lv"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
+3 -2
View File
@@ -27,7 +27,7 @@ export default {
computed:{
currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
return luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
return this.propsViewData?.focus_date;
},
currentMode() {
@@ -47,7 +47,7 @@ export default {
return;
}
const opts = { zone: FHC_JS_DATA_STORAGE_OBJECT.timezone };
const opts = { zone: this.viewData.timezone };
const start = luxon.DateTime
.fromISO(this.studiensemester_start, opts)
.toUnixInteger();
@@ -124,6 +124,7 @@ export default {
<hr>
<fhc-calendar
ref="calendar"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
+14 -2
View File
@@ -11,6 +11,18 @@ export default {
CisSprachen,
ThemeSwitch,
},
directives:{
navToggleButton:{
updated(el, binding, vnode, prevVnode){
const { menuOpen, themeName } = binding.value;
if(menuOpen && themeName === "contrast"){
el.querySelector('i').style.setProperty('color', 'white', 'important')
} else if (!menuOpen && themeName === "contrast"){
el.querySelector('i').style.setProperty('color', 'black', 'important')
}
}
}
},
props: {
rootUrl: String,
logoUrl: String,
@@ -143,8 +155,8 @@ export default {
<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>
<button v-navToggleButton="{menuOpen, themeName: $theme.theme_name.value}" :aria-label="menuCollapseAriaLabel" :title="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-secondary-text"></i>
</button>
</div>
<div class="offcanvas-body p-0">
@@ -15,7 +15,7 @@ export default {
},
computed: {
currentDay() {
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
},
currentMode() {
return this.propsViewData?.mode || DEFAULT_MODE_RAUMINFO;
@@ -51,6 +51,7 @@ export default {
<hr>
<fhc-calendar
ref="calendar"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
+1
View File
@@ -27,6 +27,7 @@ export default {
<button id="themeSwitch" :aria-label="$p.t('global','switchTheme',[nextTheme])" @click="switchTheme(nextTheme)" class="fhc-primary-highlight-bg align-self-center btn btn-secondary rounded-5">
<i v-if="theme == 'light'" class="fa-solid fa-sun " aria-hidden="true"></i>
<i v-else-if="theme == 'dark'" class="fa-solid fa-moon " aria-hidden="true"></i>
<i v-else-if="theme == 'contrast'" class="fa-solid fa-circle-half-stroke " aria-hidden="true"></i>
<!--<i v-else-if="theme == 'purple'" class="fa-solid fa-wine-bottle" aria-hidden="true"></i>-->
</button>
`
+45 -92
View File
@@ -4,6 +4,7 @@ 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',
@@ -15,7 +16,7 @@ export default {
provide() {
return {
adminMode: true,
widgetsSetup: Vue.computed(() => this.dashboard ? this.dashboard.widgetSetup : null)
widgetsSetup: Vue.computed(() => this.dashboards[this.current] ? this.dashboards[this.current].widgetSetup : null)
};
},
data() {
@@ -33,32 +34,33 @@ export default {
methods: {
dashboardAdd() {
let _name = '';
BsPrompt
.popup('New Dashboard name')
.then(dashboard_kurzbz => {
BsPrompt.popup('New Dashboard name').then(
name => {
_name = name;
const params = {
dashboard_kurzbz
dashboard_kurzbz: name
};
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,
dashboard_kurzbz: _name,
beschreibung: ''
};
this.dashboards.push(newDashboard);
this.current = newDashboard.dashboard_id;
})
.catch(this.$fhcAlert.handleSystemError);
});
});
},
dashboardUpdate(dashboard) {
this.$api
return 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);
@@ -68,122 +70,73 @@ export default {
.catch(this.$fhcAlert.handleSystemError);
},
dashboardDelete(dashboard_id) {
this.$api
return 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;
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);
}
})
.catch(this.$fhcAlert.handleSystemError);
},
template: /* html */`
<div class="dashboard-admin">
template: `<div class="dashboard-admin">
<div class="input-group">
<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>
<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>
</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
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>
<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>
</li>
<li class="nav-item" role="presentation">
<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>
<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>
</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
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 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>
<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 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>
<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 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>
</div>
</div>
+13 -34
View File
@@ -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,43 +18,22 @@ 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: /* html */`
<div class="dashboard-admin-edit px-3">
template: `<div class="dashboard-admin-edit px-3">
<div class="mb-3">
<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"
>
<label for="dashboard-admin-edit-kurzbz">Kurz Bezeichnung</label>
<input id="dashboard-admin-edit-kurzbz" type="text" class="form-control" v-model="kurzbz">
</div>
<div class="mb-3">
<label for="dashboard-admin-edit-beschreibung">{{ $p.t('global/beschreibung') }}</label>
<textarea
id="dashboard-admin-edit-beschreibung"
v-model="desc"
class="form-control"
></textarea>
<label for="dashboard-admin-edit-beschreibung">Beschreibung</label>
<textarea id="dashboard-admin-edit-beschreibung" class="form-control" v-model="desc"></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>`
}
+32 -56
View File
@@ -12,27 +12,18 @@ export default {
dashboard: String,
widgets: Array
},
data() {
return {
funktionen: {},
sections: [],
selectedFunktionen: [],
abortController: null
};
},
data: () => ({
funktionen: {},
sections: [],
tmpLoading: ''
}),
computed: {
pickerWidgets() {
return this.widgets.filter(widget => widget.allowed);
}
},
watch: {
dashboard() {
this.loadSections();
this.loadFunktionen();
}
},
methods: {
widgetAdd(widget, section_name) {
widgetAdd(section_name, widget) {
this.$refs.widgetpicker.getWidget().then(widget_id => {
widget.widget = widget_id;
widget.id = 'loading_' + String((new Date()).valueOf());
@@ -73,26 +64,22 @@ export default {
})
.catch(() => {});
},
widgetUpdate(payload, section_name) {
widgetUpdate(section_name, payload) {
payload = 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]);
// NOTE(chris): remove internal props
for (var prop of ['_x', '_y', '_w', '_h', 'index', 'id', 'custom'])
for (var prop of ['_x', '_y', '_w', '_h', 'index', 'id'])
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]) => [
@@ -119,7 +106,7 @@ export default {
})
.catch(this.$fhcAlert.handleSystemError);
},
widgetRemove(id, section_name) {
widgetRemove(section_name, id) {
const params = {
db: this.dashboard,
funktion_kurzbz: section_name,
@@ -135,22 +122,21 @@ export default {
})
.catch(this.$fhcAlert.handleSystemError);
},
loadSections() {
loadSections(evt) {
let funktionen = Array.from(evt.target.querySelectorAll("option:checked"),e=>e.value);
this.sections = [];
this.tmpLoading = funktionen.join('###');
const params = {
db: this.dashboard,
funktionen: this.selectedFunktionen
funktionen
};
if (this.abortController)
this.abortController.abort();
this.abortController = new AbortController();
const signal = this.abortController.signal;
this.sections = [];
return this.$api
.call(ApiDashboardPreset.getBatch(params), { signal })
.call(ApiDashboardPreset.getBatch(params))
.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]) {
@@ -165,6 +151,7 @@ export default {
}
})
.catch(this.$fhcAlert.handleSystemError);
},
loadFunktionen() {
this.$api
@@ -178,17 +165,17 @@ export default {
created() {
this.loadFunktionen();
},
template: /* html */`
<div class="dashboard-admin-presets">
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">
<div class="row">
<div class="col-3">
<select
v-model="selectedFunktionen"
class="form-control"
style="height:30em"
multiple
@change="loadSections"
>
<select ref="funktionenList" style="height:30em" class="form-control" multiple @input="loadSections">
<option
v-for="funktion in funktionen"
:key="funktion.funktion_kurzbz"
@@ -198,20 +185,9 @@ 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 {
props: {
dashboard_id: Number,
widgets: Array
},
emits: [
"change",
"assignWidgets"
],
props: {
dashboard_id: Number,
widgets: Array
},
methods: {
sendChange(widget_id) {
let allow = !this.widgets.find(el => el.widget_id == widget_id).allowed;
@@ -29,27 +29,11 @@ export default {
})
.catch(this.$fhcAlert.handleSystemError);
},
template: /* html */`
template: `
<div class="dashboard-admin-widgets">
<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 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>
</div>`
}
+17 -34
View File
@@ -21,7 +21,7 @@ export default {
type: Object,
required: true,
validator(value) {
return value && value.name
return value && value.name && value.timezone
}
}
},
@@ -35,12 +35,14 @@ export default {
},
provide() {
return {
editMode: Vue.computed(() => this.editMode),
widgetsSetup: Vue.computed(() => this.widgetsSetup)
editMode: Vue.computed(()=>this.editMode),
widgetsSetup: Vue.computed(() => this.widgetsSetup),
timezone: Vue.computed(() => this.viewData.timezone)
}
},
methods: {
widgetAdd(widget) {
widgetAdd(section_name, widget) {
// TODO(chris): remove section_name? (change order of params => get rid of it)
this.$refs.widgetpicker
.getWidget()
.then(widget_id => {
@@ -62,24 +64,19 @@ export default {
})
.catch(() => {});
},
widgetUpdate(payload) {
widgetUpdate(section_name, payload) {
payload = payload[section_name];
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]);
// 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
@@ -116,7 +113,7 @@ export default {
})
.catch(this.$fhcAlert.handleSystemError);
},
widgetRemove(id) {
widgetRemove(section_name, id) {
this.$api
.call(ApiDashboardUser.removeWidget(this.dashboard, id))
.then(() => {
@@ -141,8 +138,8 @@ export default {
const widgets = [];
const remove = [];
for (var wid in res.data) {
let widget = res.data[wid];
for (var wid in res.data.general.widgets) {
let widget = res.data.general.widgets[wid];
widget.id = wid;
if (widget.custom || widget.preset) {
widgets.push(widget);
@@ -152,33 +149,19 @@ export default {
}
}
remove.forEach(wid => this.widgetRemove(wid));
remove.forEach(wid => this.widgetRemove('general', wid));
this.widgets = widgets;
})
.catch(this.$fhcAlert.handleSystemError);
},
template: /* html */`
template: `
<div class="core-dashboard">
<h3>
{{ $p.t('global/personalGreeting', [ viewData?.name ]) }}
<button
class="btn ms-2"
aria-label="edit dashboard"
v-tooltip="{ showDelay: 1000, value: $p.t('dashboard/edit') }"
@click="editMode = !editMode"
><i class="fa-solid fa-gear" aria-hidden="true"></i></button>
<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
name="general"
:widgets="widgets"
@widget-add="widgetAdd"
@widget-update="widgetUpdate"
@widget-remove="widgetRemove"
></dashboard-section>
<dashboard-widget-picker
ref="widgetpicker"
:widgets="widgetsSetup"
></dashboard-widget-picker>
<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>
</div>`
}
+134 -294
View File
@@ -1,13 +1,7 @@
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: {
@@ -17,14 +11,18 @@ 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",
@@ -32,85 +30,41 @@ export default {
],
props: [
"id",
"widgetID",
"config",
"width",
"height",
"custom",
"hidden",
"editMode",
"loading", // widget got added and is waiting for backend to save in db
"loading",
"item_data",
"place",
"widgetTemplate",
"source"
"setup",
"dragstate",
"resizeOverlay",
"additionalRow"
],
computed: {
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]);
maxHeight(){
return this.setup?.height?.max;
},
maxWidth(){
if (Object.prototype.toString.call(this.setup?.width) == "[object Number]"){
return this.setup?.width;
}
return this.setup?.width?.max;
},
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;
minHeight() {
return this.setup?.height?.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;
minWidth() {
return this.setup?.width?.min;
},
isResizeable() {
return this.isResizeableVertical || this.isResizeableHorizontal;
isResizeable(){
return this.maxWidth >1 || this.maxHeight >1;
},
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() {
isPinned(){
return this.place?.pinned ? true : false;
},
ready() {
@@ -126,16 +80,16 @@ export default {
}
},
methods: {
unpin() {
unpin(){
// Unpinning is only possible in edit mode
if (!this.editMode)
if(!this.editMode)
return;
let result = { item: this.item_data, pinned: false };
let result = { item: this.item_data, x: this.item_data.x, y: this.item_data.y };
this.$emit('unPinItem', [result]);
},
pinItem() {
let result = { item: this.item_data, pinned: true };
this.$emit('pinItem', [result]);
pinItem(){
let result = { item: this.item_data, x: this.item_data.x, y: this.item_data.y};
this.$emit('pinItem',[result]);
},
getWidgetC4Link(widget) {
return (FHC_JS_DATA_STORAGE_OBJECT.app_root +
@@ -147,6 +101,22 @@ 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();
@@ -165,240 +135,110 @@ export default {
},
sendChangeConfig(config) {
for (var k in config) {
if (this.widgetTemplate.arguments[k] == config[k]) {
delete config[k];
if (this.widget.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.widgetTemplate?.arguments, ...this.config };
this.arguments = { ...this.widget?.arguments, ...this.config };
this.tmpConfig = { ...this.arguments };
this.$refs.config && this.$refs.config.hide();
this.isLoading = false;
},
widgetTemplate() {
this.initializeComponent();
}
},
created() {
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 };
},
template: /*html*/ `
<article
v-if="!hidden || editMode"
class="dashboard-item card overflow-hidden h-100 position-relative"
:class="{
'hidden-widget': hidden,
[arguments?.className]: arguments && arguments.className
}"
>
<div v-if="loading" class="d-flex justify-content-center align-items-center h-100">
<div v-if="loading">
<div class="d-flex justify-content-center align-items-center h-100">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<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>
<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>
<!-- 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>
<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>
</span>
</template>
<template v-else>
<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 button -->
<a
v-if="custom && 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>
<!-- hide button -->
<Transition>
<div v-if="!custom && editMode" class="col-auto px-1 form-switch">
<input
type="checkbox"
role="switch"
v-model="visible"
class="form-check-input ms-0"
:value="true"
:aria-label="$p.t('dashboard/widget_toggle_visibility')"
>
</div>
</Transition>
</header>
<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>
{{ 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 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>
</footer>
</height-transition>
</template>
</article>`,
</template>
</div>
</height-transition>
</div>
</div>`,
};
+122 -135
View File
@@ -1,12 +1,9 @@
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: {
@@ -14,11 +11,8 @@ export default {
DashboardItem,
WidgetIcon,
},
directives: {
dragClick
},
inject: {
widgetsSetup: {
widgetsSetup:{
type: Array,
default: [],
},
@@ -45,8 +39,9 @@ export default {
configOpened: false,
gridWidth: 1,
gridHeight: null,
additionalRow: false
};
draggedItem:null,
additionalRow:false,
}
},
provide() {
return {
@@ -54,40 +49,22 @@ export default {
this.editModeIsActive
),
sectionName: Vue.computed(() => this.name),
};
}
},
computed: {
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;
computedWidgetsSetup(){
if(!this.widgetsSetup) return {};
return this.widgetsSetup.reduce((acc, setup)=>{
acc[setup.widget_id] = setup.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) =>{
@@ -108,55 +85,76 @@ export default {
if(!item?.widgetid && item?.id){
item.widgetid = item.id;
}
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 { ...item, reorder: false, ...(item.place[this.gridWidth] || { reorder: true, ...{ x: 0, y: 0, w: 1, h: 1 } })};
});
if (this.editModeIsActive)
return placedItems;
return placedItems.filter(item => !item.hidden);
}
},
watch: {
items() {
this.additionalRow = false;
}
return placedItems;
},
},
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) {
if (item.custom) {
BsConfirm.popup(this.$p.t('dashboard', 'alert_deleteWidget')).then(() => this.$emit('widgetRemove', item.id, this.name));
BsConfirm.popup(this.$p.t('dashboard', 'alert_deleteWidget')).then(() => this.$emit('widgetRemove', this.name, item.id));
} else {
let update = {};
update[item.id] = { hidden: !revert };
if (!revert) {
// NOTE(chris): move to last line
update[item.id].place = [];
let y = this.gridHeight;
if (this.additionalRow)
y--;
update[item.id].place[this.gridWidth] = { x: 0, y };
}
this.updatePreset(update);
}
},
@@ -165,42 +163,49 @@ export default {
payload[item.id] = { config };
this.updatePreset(payload);
},
updatePositions(updated) {
updatePositions(updated, pinned=false) {
let result = {};
updated.forEach(update => {
let item = structuredClone(ObjectUtils.deepToRaw(update.item));
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.pinned;
delete item.weight;
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;
}
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;
result[item.id] = item;
}
});
this.updatePreset(result);
},
updatePreset(update) {
this.$emit('widgetUpdate', update, this.name);
let payload = {};
payload[this.name] = update;
this.$emit('widgetUpdate', this.name, payload);
}
},
setup() {
const { state: widgetState } = useCachedWidgetLoader();
return {
widgetState
};
},
mounted() {
let self = this;
let cont = self.$refs.container;
@@ -210,62 +215,44 @@ export default {
self.gridWidth = parseInt(window.getComputedStyle(cont).getPropertyValue('--fhc-dashboard-grid-size'));
});
},
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: `
<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 #default="item">
<div
v-if="item.placeholder"
class="empty-tile-hover"
@click="$emit('widgetAdd', { widget: 1, config: {}, place: {[gridWidth]: {x:item.x,y:item.y,w:1,h:1}}, custom: 1 }, name)"
></div>
<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>
<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,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,reorder:item.reorder,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]"
:widget-template="indexedWidgetsTemplates[item.widget]"
:source="adminMode ? null : item.source || 'custom'"
:setup="computedWidgetsSetup[item.widget]"
@change="saveConfig($event, item)"
@remove="removeWidget(item, $event)"
@config-opened="handleConfigOpened"
@config-closed="handleConfigClosed"
@pin-item="updatePositions"
@un-pin-item="updatePositions"
></dashboard-item>
@pinItem="updatePositions($event,true)"
@unPinItem="updatePositions">
</dashboard-item>
</template>
</drop-grid>
</section>`
</div>`
}
/*
@@ -32,31 +32,19 @@ export default {
},
},
template: /* html */`
<div class="dashboard-widget-picker">
<bs-modal
ref="modal"
class="fade"
:dialog-class="{ 'modal-fullscreen-sm-down': 1, 'modal-xl': widgets && widgets.length > 0 }"
@hiddenBsModal="close"
>
<template v-slot:title>{{ $p.t('dashboard/createWidget') }}</template>
template: `<div class="dashboard-widget-picker">
<bs-modal ref="modal" class="fade" :dialog-class="{'modal-fullscreen-sm-down': 1, 'modal-xl': widgets && widgets.length > 0}" @hiddenBsModal="close">
<template v-slot:title>Create new widget</template>
<template v-slot:default>
<div v-if="widgets" class="row g-2">
<div v-if="!widgets.length">
{{ $p.t('dashboard/noWidgetsAvailable') }}
No Widgets available
</div>
<div
v-for="widget in widgets"
:key="widget.widget_id"
class="widget-icon-container col-sm-6 col-md-4 col-lg-3 col-xl-2"
>
<widget-icon @select="pick" :widget="widget"></widget-icon>
<div v-for="widget in widgets" :key="widget.widget_id" class="widget-icon-container col-sm-6 col-md-4 col-lg-3 col-xl-2">
<widget-icon @select="pick" :widget="widget" ></widget-icon>
</div>
</div>
<div v-else class="text-center">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<div v-else class="text-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></div>
</template>
</bs-modal>
</div>`
@@ -11,6 +11,9 @@ export default {
mixins: [
AbstractWidget
],
inject: [
"timezone"
],
methods: {
getPromiseFunc(start, end) {
return [
@@ -24,6 +27,6 @@ export default {
},
template: /*html*/`
<div class="dashboard-widget-lvplan d-flex flex-column h-100">
<fhc-calendar :get-promise-func="getPromiseFunc" />
<fhc-calendar :timezone="timezone" :get-promise-func="getPromiseFunc" />
</div>`
}
File diff suppressed because it is too large Load Diff
+71 -9
View File
@@ -1,26 +1,88 @@
export default {
name:'GridItem',
components: {
},
inject: {
},
props: {
item: Object
item: Object,
active: Boolean
},
emits: [
"mouseDown",
"mouseUp",
"startMove",
"startResize"
"startResize",
"dragging",
"endDrag",
"dropDrag",
"item",
"touchStart",
"touchEnd",
],
data() {
return {
dragAction: '',
dragging: false
}
},
computed: {
},
methods: {
tryDragStart(evt) {
let dragAction = evt.target.getAttribute('drag-action');
registerDragAction(evt) {
this.$emit('mouseDown', evt);
if (evt.target.hasAttribute('drag-action')) {
this.dragAction = evt.target.getAttribute('drag-action');
} else {
let parent = evt.target.closest('[drag-action]');
if (parent) {
this.dragAction = parent.getAttribute('drag-action');
} else {
this.dragAction = '';
}
}
},
tryDragStart(evt, item) {
let dragAction = this.dragAction || evt.target.getAttribute('drag-action');
if (dragAction) {
this.dragging = true;
if (dragAction == 'move')
return this.$emit('startMove', evt, this.item);
return this.$emit('startMove', evt, item);
else if (dragAction == 'resize')
return this.$emit('startResize', evt, this.item);
return this.$emit('startResize', evt, item);
}
//evt.preventDefault();
},
touchDragEnd(evt) {
if (!this.dragging)
return;
this.dragging = false;
this.$emit('touchEnd', evt);
},
touchStart(event){
this.$emit('touchStart', event);
this.registerDragAction(event);
this.tryDragStart(event, this.item);
},
touchMove(event){
if(this.dragging){
event.preventDefault();
this.$emit('dragging', event);
}
}
},
template: /* html */`
<li class="drop-grid-item" @dragstart="tryDragStart">
template: `
<div class="drop-grid-item"
@mousedown="registerDragAction"
@mouseup="$emit('mouseUp', $event)"
@touchstart="touchStart"
@touchend="touchDragEnd"
@dragstart="tryDragStart($event, item)"
@drag="$emit('dragging',$event)"
@touchmove="touchMove"
@dragend="$emit('endDrag', $event); dragging = false"
:draggable="active && !item.placeholder">
<slot v-bind="item"></slot>
</li>`
</div>`
}
+1 -1
View File
@@ -97,7 +97,7 @@ export default {
class="h-100 input-group me-2 searchbar_searchbox"
:class="showresult ? 'open' : 'closed'"
>
<span class="input-group-text">
<span class="input-group-text" type="button" @click="$refs.input.focus()">
<i class="fa-solid fa-magnifying-glass"></i>
</span>
<input
@@ -0,0 +1,36 @@
import ApiWidget from "../../api/factory/dashboard/widget.js";
const promises = Vue.ref([]);
const stateRef = Vue.ref([]);
const state = Vue.readonly(stateRef);
export function useCachedWidgetLoader() {
const $api = Vue.inject('$api');
const $fhcAlert = Vue.inject('$fhcAlert');
function load(id) {
if (state.value[id])
return Promise.resolve(state.value[id]);
if (!promises.value[id])
promises.value[id] = new Promise((resolve, reject) => {
$api
.call(ApiWidget.get(id))
.then(res => {
stateRef.value[id] = res.data;
promises.value[id] = undefined;
resolve(state.value[id]);
})
.catch($fhcAlert.handleSystemError);
});
return promises.value[id];
}
return {
state,
actions: {
load
}
};
}
+52 -80
View File
@@ -1,10 +1,4 @@
/**
* This class arranges rectangular items on a grid with a defined width and
* a potential infinite height. It calculates repositioning of already placed
* items if a new item would overlap one or more of said placed items.
* This can be manipulated by adding weights to the items or by defining an
* item as pinned.
*/
// TODO(chris): Comments
const DIR_UP = 0;
const DIR_LEFT = 1;
@@ -29,23 +23,33 @@ class GridLogic {
const i = y*this.w + x;
return !this.grid[i] && this.grid[i] !== 0;
}
getMaxY(){
return this.data.reduce((acc, item) => {
if (item?.y > acc) {
acc = item.y;
}
return acc;
}, 0);
}
getFreeSlots() {
const freeSlots = [];
let i = this.w * this.h;
while (i--) {
if (!this.grid[i] && this.grid[i] !== 0) {
let biggestY = this.getMaxY();
let totalSpaces = this.w * (biggestY+1);
for(let i=0; i < totalSpaces; i++){
if (!this.grid[i] && this.grid[i] !== 0){
this.grid[i] = undefined;
}
}
for(let i =0; i < this.grid.length; i++){
if (!this.grid[i] && this.grid[i] !== 0){
let x = i % this.w;
let y = Math.floor(i / this.w);
freeSlots.push({x, y});
}
}
return freeSlots;
}
add(item, prefer) {
if (!item.frame)
item.frame = this.getItemFrame(item);
let occupiers = this.getItemsInFrame(item.frame);
if (!occupiers.length) {
item.frame.forEach(f => this.grid[f] = item.index);
@@ -57,14 +61,6 @@ class GridLogic {
item.frame.forEach(f => intermGrid.grid[f] = -1);
intermGrid.data.forEach(currItem => {
if (currItem.pinned) {
if (!currItem.frame)
currItem.frame = intermGrid.getItemFrame(currItem);
currItem.frame.forEach(f => intermGrid.grid[f] = -1);
}
});
const possiblities = intermGrid.tryMoving(occupiers, prefer);
if (possiblities.length) {
const bestOption = possiblities.sort((a,b) => {
@@ -87,9 +83,7 @@ class GridLogic {
result[move.index] = {
index: currItem.index,
x: currItem.x,
y: currItem.y,
w: currItem.w,
h: currItem.h
y: currItem.y
};
});
item.frame.forEach(f => this.grid[f] = item.index);
@@ -97,12 +91,12 @@ class GridLogic {
return result;
} else {
return null;
console.error('FATAL', "can't arrange item on grid");
}
}
}
move(item, x, y) {
if (item.pinned)
if (item.data.place[this.w]?.pinned)
return [];
if (item.x == x && item.y == y)
return [];
@@ -122,6 +116,8 @@ class GridLogic {
prefer = DIR_RIGHT;
}
const originalFrame = Array.isArray(item.frame) ? [...item.frame] : [item.frame];
const currItem = {...item};
currItem.x = x;
currItem.y = y;
@@ -129,60 +125,33 @@ class GridLogic {
let occupiers = this.getItemsInFrame(currItem.frame);
// does not update if the target conatins pinned widgets
if (occupiers.some(frame => this.data[frame]?.pinned)) {
if (occupiers.some(frame => this.data[frame]?.data.place[this.w]?.pinned)) {
return [];
}
// checks if target contains moving widgets start position
// so swapping should be avoided
const targetAndItemOverlap = this.getItemFrame(item).some(frame => currItem.frame.includes(frame))
if (!targetAndItemOverlap) {
// checks if target contains widget with the same high and width
// so swapping is possible
const occupiersFrame = occupiers.map(occupier => this.data[occupier].frame).flat();
const occupiersInsideMovingItem = occupiersFrame.every(frame => currItem.frame.includes(frame));
if (occupiersInsideMovingItem) {
// every slot of all items in the target zone is inside said zone
const replaceUpdate = [];
const diffX = item.x - x;
const diffY = item.y - y;
occupiers.forEach(occupier => {
const data = { ...this.data[occupier] };
data.x += diffX;
data.y += diffY;
data.frame = this.getItemFrame(data);
this.remove(data);
this.add(data);
replaceUpdate[occupier] = {
index: data.index,
x: data.x,
y: data.y,
w: data.w,
h: data.h
};
});
this.add({ ...item, x, y });
replaceUpdate[item.index] = {
index: item.index,
x,
y,
w: item.w,
h: item.h
};
return replaceUpdate;
// checks if target contains widget with the same high and width
let occupiersData = occupiers.map(occupier => this.data[occupier]);
let occupiersFrame = occupiersData.map(occupier => occupier.frame).flat();
if (!occupiersFrame.some(frame => !currItem.frame.includes(frame)) && !occupiersFrame.some(frame => originalFrame.includes(frame))){
let replaceUpdate = [];
let newOccupierFrames = [];
for(let f of originalFrame){
if(newOccupierFrames.includes(f)){
continue;
}
let occ = occupiersData.shift();
if(occ){
newOccupierFrames = [...newOccupierFrames, ...this.getItemFrame({ ...occ, ...this.getSingleFramePosition(f) })];
replaceUpdate[occ.index] = { index: occ.index, ...this.getSingleFramePosition(f)}
}
}
replaceUpdate[item.index] = { index: item.index, x, y };
return replaceUpdate;
}
const updates = this.add(currItem, prefer);
if (updates)
updates[item.index] = { index: item.index, x, y, w: item.w, h: item.h };
updates[item.index] = {index: item.index, x, y};
return updates;
}
resize(item, w, h) {
@@ -197,7 +166,7 @@ class GridLogic {
const updates = this.add(currItem);
if(updates)
updates[item.index] = { index: item.index, w, h, x: item.x, y: item.y };
updates[item.index] = {index: item.index, w, h, x:item.x, y:item.y, resize:true};
return updates;
}
@@ -236,13 +205,13 @@ class GridLogic {
let targetframe;
switch(dir) {
case DIR_UP:
if (this.data[index].pinned || this.data[index].y - amount < 0)
if (this.data[index].data?.place[this.w]?.pinned || this.data[index].y - amount < 0)
return false;
targetframe = this.data[index].frame.map(i => i-this.w*amount);
move.y = -amount;
break;
case DIR_DOWN:
if (this.data[index].pinned)
if (this.data[index].data?.place[this.w]?.pinned)
return false;
if (this.data[index].y + this.data[index].h + amount > this.h)
cost += .4;
@@ -250,13 +219,13 @@ class GridLogic {
move.y = amount;
break;
case DIR_LEFT:
if (this.data[index].pinned || this.data[index].x - amount < 0)
if (this.data[index].data?.place[this.w]?.pinned || this.data[index].x - amount < 0)
return false;
targetframe = this.data[index].frame.map(i => i-amount);
move.x = -amount;
break;
case DIR_RIGHT:
if (this.data[index].pinned || this.data[index].x + this.data[index].w + amount > this.w)
if (this.data[index].data?.place[this.w]?.pinned || this.data[index].x + this.data[index].w + amount > this.w)
return false;
targetframe = this.data[index].frame.map(i => i+amount);
move.x = amount;
@@ -293,6 +262,9 @@ class GridLogic {
frame.push(i + item.x + (j + item.y) * this.w);
return frame;
}
getSingleFramePosition(frame){
return { x: frame % this.w, y: Math.floor(frame / this.w)};
}
debug() {
return this.grid;
}
-50
View File
@@ -1,50 +0,0 @@
import ApiRenderers from '../api/factory/renderers.js';
/**
* @return object { renderers: Object }
*/
export function useRenderers() {
/* Result Vars */
const renderers = Vue.ref(null);
/* Helper Vars */
const $api = Vue.inject('$api');
const $fhcAlert = Vue.inject('$fhcAlert');
/* Main Logic */
$api
.call(ApiRenderers.loadRenderers())
.then(res => {
const head = document.head;
for (const rendertype of Object.keys(res.data)) {
const renderersForType = {};
for (const name of Object.keys(res.data[rendertype])) {
const rendererUrl = res.data[rendertype][name];
if (rendererUrl.substr(-4) == ".css") {
// add to head
if (!head.querySelector(`link[href="${rendererUrl}"]`)) {
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = rendererUrl;
head.appendChild(link);
}
} else {
renderersForType[name] = Vue.markRaw(
Vue.defineAsyncComponent(() => import(rendererUrl))
);
}
}
if (Object.keys(renderersForType).length) {
if (renderers.value === null)
renderers.value = {};
renderers.value[rendertype] = renderersForType;
}
}
})
.catch($fhcAlert.handleSystemErrors);
return {
renderers
};
}
-7
View File
@@ -1,12 +1,5 @@
import { bindDragEnterLeave } from '../helpers/DragAndDrop.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 {
mounted(el, binding) {
const delay = parseInt(binding.arg) || 300;
-7
View File
@@ -1,12 +1,5 @@
import { setTransferData, convertToValidDragObject } from '../helpers/DragAndDrop.js';
import { enableDragDropTouch } from "../../../vendor/drag-drop-touch-js/dragdroptouch/dist/drag-drop-touch.esm.min.js";
if (!document.dragDropTouchActive) {
enableDragDropTouch();
document.dragDropTouchActive = true;
}
const EFFECTS = [
'none',
'copy',
-7
View File
@@ -1,12 +1,5 @@
import { getValidTransferData, eventHasTypes, bindDragEnterLeave } from '../helpers/DragAndDrop.js';
import { enableDragDropTouch } from "../../../vendor/drag-drop-touch-js/dragdroptouch/dist/drag-drop-touch.esm.min.js";
if (!document.dragDropTouchActive) {
enableDragDropTouch();
document.dragDropTouchActive = true;
}
const EFFECTS = [
'move',
'copy',
-1
View File
@@ -94,7 +94,6 @@ require_once('dbupdate_3.4/71399_dashboard_update_widget_paths.php');
require_once('dbupdate_3.4/71645_studvw_messagetab_ladezeit.php');
require_once('dbupdate_3.4/71566_studienordnungsdokument_neuer_organisationseinheitstyp_programm.php');
require_once('dbupdate_3.4/70376_lohnguide.php');
require_once('dbupdate_3.4/68530_Dashboard_Cleanup.php');
// *** Pruefung und hinzufuegen der neuen Attribute und Tabellen
echo '<H2>Pruefe Tabellen und Attribute!</H2>';
@@ -1,91 +0,0 @@
<?php
/* 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 2 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, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
*
* Authors: Christopher Hacker <christopher.hacker@technikum-wien.at>,
*
* Description:
* Cleanup Dashboard DB data
*/
if (! defined('DB_NAME')) exit('No direct script access allowed');
// Cleanup presets
if ($result = @$db->db_query("
SELECT 1
FROM dashboard.tbl_dashboard_preset
WHERE preset ? COALESCE(funktion_kurzbz, 'general')
OR preset ? 'custom'
LIMIT 1
")) {
if ($db->db_num_rows($result)) {
$qry = "
UPDATE dashboard.tbl_dashboard_preset
SET preset = COALESCE(preset->COALESCE(funktion_kurzbz, 'general'), preset->'custom')->'widgets'
WHERE preset ? COALESCE(funktion_kurzbz, 'general')
OR preset ? 'custom'
";
$result = $db->db_query($qry);
if (!$result) {
echo '<strong>dashboard.tbl_dashboard_preset '.$db->db_last_error().'</strong><br>';
} else {
$affected_rows = $db->db_affected_rows($result);
echo 'dashboard.tbl_dashboard_preset: ' . $affected_rows . ' rows migrated<br>';
}
}
}
// Cleanup user overrides
if ($result = @$db->db_query("
SELECT 1
FROM dashboard.tbl_dashboard_benutzer_override
WHERE EXISTS (
SELECT 1
FROM jsonb_each(override)
WHERE value ? 'widgets'
LIMIT 1
) AND override <> '[]'::jsonb
LIMIT 1
")) {
if ($db->db_num_rows($result)) {
$qry = "
UPDATE dashboard.tbl_dashboard_benutzer_override
SET override = COALESCE((
SELECT json_object_agg(key, value) FROM (
SELECT value->'widgets' AS widgets
FROM jsonb_each(override)
WHERE jsonb_typeof(value->'widgets') = 'object'
) x, jsonb_each(widgets)
), '[]')
WHERE EXISTS (
SELECT 1
FROM jsonb_each(override)
WHERE value ? 'widgets'
LIMIT 1
) AND override <> '[]'::jsonb
";
$result = $db->db_query($qry);
if (!$result) {
echo '<strong>dashboard.tbl_dashboard_benutzer_override '.$db->db_last_error().'</strong><br>';
} else {
$affected_rows = $db->db_affected_rows($result);
echo 'dashboard.tbl_dashboard_benutzer_override: ' . $affected_rows . ' rows migrated<br>';
}
}
}
-320
View File
@@ -47126,26 +47126,6 @@ array(
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'edit',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Dashboard bearbeiten',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Edit dashboard',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
@@ -47166,46 +47146,6 @@ array(
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'createWidget',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Neues Widget hinzufügen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Create new widget',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'noWidgetsAvailable',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Keine Widgets verfügbar',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'No Widgets available',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
@@ -47286,266 +47226,6 @@ array(
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'widgetFromGeneralSection',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Dieses Widget ist für das Dashboard vordefiniert',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'This is a predefined widget of this dashboard',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'widgetFromCustomSection',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Dieses Widget wurde von Ihnen hinzugefügt',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'This widget has been added by you',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'widgetFromFunktionSection',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Dieses Widget wurde hinzugefügt weil Sie der "{0}" Funktion zugewiesen wurden',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'This widget has been added because you have been assigned to the "{0}" function',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'widget_pin',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Pin Widget',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Pin widget',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'widget_unpin',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Unpin Widget',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Unpin widget',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'widget_toggle_visiblity',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Sichtbarkeit des Widgets umschalten',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Toggle widget visibility',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'widget_move',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Widget verschieben',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Move widget',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'widget_resize',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Widgetgröße anpassen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Resize widget',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'widget_delete',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Widget löschen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Delete widget',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'widget_configure',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Widget konfigurieren',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Configure widget',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'widget_config_title',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => '"{name}" konfigurieren',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Configure "{name}"',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'widget_link',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Widget link',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Widget link',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'dashboard',
'phrase' => 'kurzbz',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Name',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Identifier',
'description' => '',
'insertvon' => 'system'
)
)
),
// CIS4 phrases from legacy code end
// FHC4 Phrases Abschlusspruefung
array(