Merge branch 'feature-24647/Konfigurierbare_Dashboards' into feature-25999/C4

This commit is contained in:
cgfhtw
2022-11-24 15:36:38 +01:00
39 changed files with 3556 additions and 1 deletions
+78
View File
@@ -0,0 +1,78 @@
<?php
defined('BASEPATH') || exit('No direct script access allowed');
class Api extends Auth_Controller
{
public function __construct()
{
parent::__construct(
array(
'index' => 'dashboard/admin:rw',
'getNews' => 'dashboard/admin:rw',
'getAmpeln' => 'dashboard/admin:rw',
)
);
$this->load->library('AuthLib', null, 'AuthLib');
$this->_setAuthUID();
}
public function index()
{
echo 'Dashboard API Controller';
}
/**
* Get News.
*/
public function getNews()
{
$limit = $this->input->get('limit');
$this->load->model('content/News_model', 'NewsModel');
$result = $this->NewsModel->getAll($limit);
if(hasData($result))
{
$this->outputJson(getData($result), REST_Controller::HTTP_OK);
}
else
{
$this->terminateWithJsonError('fehler entdeckt');
}
}
/**
* Get Ampeln.
*/
public function getAmpeln(){
$this->load->model('content/Ampel_model', 'AmpelModel');
$result = $this->AmpelModel->getByUser($this->_uid);
if(hasData($result))
{
$this->outputJson(getData($result), REST_Controller::HTTP_OK);
}
else
{
$this->terminateWithJsonError('fehler entdeckt');
}
}
/**
* Retrieve the UID of the logged user and checks if it is valid
*/
private function _setAuthUID()
{
$this->_uid = getAuthUID();
if (!$this->_uid) show_error('User authentification failed');
}
}
@@ -0,0 +1,218 @@
<?php
defined('BASEPATH') || exit('No direct script access allowed');
/**
* Description of Config
*
* @author bambi
*/
class Config extends Auth_Controller
{
public function __construct()
{
parent::__construct(
array(
'index' => 'dashboard/benutzer:r',
'dummy' => 'dashboard/benutzer:r',
'genWidgetId' => 'dashboard/benutzer:rw',
'addWidgetsToPreset' => 'dashboard/admin:rw',
'removeWidgetFromPreset' => 'dashboard/admin:rw',
'addWidgetsToUserOverride' => 'dashboard/benutzer:rw',
'removeWidgetFromUserOverride' => 'dashboard/benutzer:rw',
'Funktionen' => 'dashboard/admin:r',
'Preset' => 'dashboard/admin:r',
'PresetBatch' => 'dashboard/admin:r'
)
);
$this->load->library('dashboard/DashboardLib', null, 'DashboardLib');
$this->load->library('AuthLib', null, 'AuthLib');
$this->load->model('ressource/Funktion_model', 'FunktionModel');
}
public function index()
{
$dashboard_kurzbz = $this->input->get('db');
$uid = $this->AuthLib->getAuthObj()->username;
$dashboard = $this->DashboardLib->getDashboardByKurzbz($dashboard_kurzbz);
if(!$dashboard) {
http_response_code(404);
$this->terminateWithJsonError(array(
'error' => 'Dashboard ' . $dashboard_kurzbz . ' not found.'
));
}
$mergedconfig = $this->DashboardLib->getMergedConfig($dashboard->dashboard_id, $uid);
$this->outputJsonSuccess($mergedconfig);
}
public function genWidgetId()
{
$dashboard_kurzbz = $this->input->get('db');
$widgetid = $this->DashboardLib->generateWidgetId($dashboard_kurzbz);
$this->outputJsonSuccess(array(
'widgetid' => $widgetid
));
}
public function addWidgetsToPreset()
{
$input = json_decode($this->input->raw_input_stream);
$dashboard_kurzbz = $input->db;
$funktion_kurzbz = $input->funktion_kurzbz;
$preset = $this->DashboardLib->getPresetOrCreateEmptyPreset($dashboard_kurzbz, $funktion_kurzbz);
$preset_decoded = json_decode($preset->preset, true);
$this->DashboardLib->addWidgetsToWidgets($preset_decoded['widgets'],
$dashboard_kurzbz, $funktion_kurzbz, $input->widgets);
$preset->preset = json_encode($preset_decoded);
$result = $this->DashboardLib->insertOrUpdatePreset($preset);
if( isError($result) ) {
http_response_code(500);
$this->terminateWithJsonError('preset could not be saved');
}
$this->outputJsonSuccess(array('msg' => 'preset successfully stored.', 'data' => $preset_decoded));
}
public function removeWidgetFromPreset()
{
$input = json_decode($this->input->raw_input_stream);
$dashboard_kurzbz = $input->db;
$funktion_kurzbz = $input->funktion_kurzbz;
$widgetid = $input->widgetid;
$preset = $this->DashboardLib->getPreset($dashboard_kurzbz, $funktion_kurzbz);
if( $preset === null ) {
http_response_code(404);
$this->terminateWithJsonError('preset for dashboard '
. $dashboard_kurzbz . ' and funktion ' . $funktion_kurzbz
. ' not found.');
}
$preset_decoded = json_decode($preset->preset, true);
if (!$this->DashboardLib->removeWidgetFromWidgets($preset_decoded['widgets'],
$funktion_kurzbz, $widgetid))
{
http_response_code(404);
$this->terminateWithJsonError('widgetid ' . $widgetid . ' not found');
}
$preset->preset = json_encode($preset_decoded);
$result = $this->DashboardLib->insertOrUpdatePreset($preset);
if( isError($result) )
{
http_response_code(500);
$this->terminateWithJsonError('failed to remove widget');
}
$this->outputJsonSuccess(array('msg' => 'preset successfully updated.'));
}
public function addWidgetsToUserOverride()
{
$input = json_decode($this->input->raw_input_stream);
$dashboard_kurzbz = $input->db;
$funktion_kurzbz = $input->funktion_kurzbz;
$uid = $this->AuthLib->getAuthObj()->username;
$override = $this->DashboardLib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid);
$override_decoded = json_decode($override->override, true);
$this->DashboardLib->addWidgetsToWidgets($override_decoded['widgets'],
$dashboard_kurzbz, $funktion_kurzbz, $input->widgets);
$override->override = json_encode($override_decoded);
$result = $this->DashboardLib->insertOrUpdateOverride($override);
if( isError($result) ) {
http_response_code(500);
$this->terminateWithJsonError('override could not be saved');
}
$this->outputJsonSuccess(array('msg' => 'override successfully stored.', 'data' => $override_decoded));
}
public function removeWidgetFromUserOverride()
{
$input = json_decode($this->input->raw_input_stream);
$dashboard_kurzbz = $input->db;
$funktion_kurzbz = $input->funktion_kurzbz;
$uid = $this->AuthLib->getAuthObj()->username;
$widgetid = $input->widgetid;
$override = $this->DashboardLib->getOverride($dashboard_kurzbz, $uid);
if( empty($override) ) {
http_response_code(404);
$this->terminateWithJsonError('userconfig for dashboard '
. $dashboard_kurzbz . ' not found.');
}
$override_decoded = json_decode($override->override, true);
if( !$this->DashboardLib->removeWidgetFromWidgets($override_decoded['widgets'],
$funktion_kurzbz, $widgetid) )
{
http_response_code(404);
$this->terminateWithJsonError('widgetid ' . $widgetid . ' not found');
}
$override->override = json_encode($override_decoded);
$result = $this->DashboardLib->insertOrUpdateOverride($override, $uid);
if( isError($result) )
{
http_response_code(500);
$this->terminateWithJsonError('failed to remove widget');
}
$this->outputJsonSuccess(array('msg' => 'override successfully updated.'));
}
public function Funktionen()
{
$funktionen = $this->FunktionModel->load();
if (isError($funktionen)) {
http_response_code(404);
$this->terminateWithJsonError([
'error' => getError($funktionen)
]);
}
return $this->outputJsonSuccess(getData($funktionen) ?: []);
}
public function Preset()
{
$db = $this->input->get('db');
$funktion = $this->input->get('funktion');
$conf = $this->DashboardLib->getPreset($db, $funktion);
if (!$conf)
return $this->outputJsonSuccess(['widgets' => [$funktion => []]]);
return $this->outputJsonSuccess(json_decode($conf->preset, true));
}
public function PresetBatch()
{
$db = $this->input->get('db');
$funktionen = $this->input->get('funktionen');
$result = [];
foreach ($funktionen as $funktion) {
$conf = $this->DashboardLib->getPreset($db, $funktion);
if ($conf)
$result[$funktion] = json_decode($conf->preset, true)['widgets'][$funktion];
else
$result[$funktion] = [];
}
return $this->outputJsonSuccess($result);
}
}
@@ -0,0 +1,87 @@
<?php
defined('BASEPATH') || exit('No direct script access allowed');
/**
* Description of Widget
*
* @author chris
*/
class Dashboard extends Auth_Controller
{
public function __construct()
{
parent::__construct(
array(
'index' => 'dashboard/admin:r',
'Create' => 'dashboard/admin:rw',
'Update' => 'dashboard/admin:rw',
'Delete' => 'dashboard/admin:rw'
)
);
$this->load->library('dashboard/DashboardLib', null, 'DashboardLib');
$this->load->model('dashboard/Dashboard_model', 'DashboardModel');
}
public function index()
{
$result = $this->DashboardModel->load();
if (isError($result)) {
http_response_code(404);
$this->terminateWithJsonError([
'error' => getError($result)
]);
}
return $this->outputJsonSuccess(getData($result) ?: []);
}
public function Create()
{
$input = $this->getPostJSON();
$result = $this->DashboardModel->insert($input);
if (isError($result)) {
http_response_code(404);
$this->terminateWithJsonError([
'error' => getError($result)
]);
}
return $this->outputJsonSuccess(getData($result) ?: []);
}
public function Update()
{
$input = $this->getPostJSON();
$result = $this->DashboardModel->update($input->dashboard_id, $input);
if (isError($result)) {
http_response_code(404);
$this->terminateWithJsonError([
'error' => getError($result)
]);
}
return $this->outputJsonSuccess(getData($result) ?: []);
}
public function Delete()
{
$input = $this->getPostJSON();
$result = $this->DashboardModel->delete($input->dashboard_id);
if (isError($result)) {
http_response_code(404);
$this->terminateWithJsonError([
'error' => getError($result)
]);
}
return $this->outputJsonSuccess(getData($result) ?: []);
}
}
@@ -0,0 +1,51 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
*/
class DashboardDemo extends Auth_Controller
{
private $_uid; // uid of the logged user
/**
* Constructor
*/
public function __construct()
{
// Set required permissions
parent::__construct(
array(
'index' => 'user:r',
)
);
$this->load->library('AuthLib');
$this->load->library('WidgetLib');
$this->_setAuthUID(); // sets property uid
$this->setControllerId(); // sets the controller id
}
// -----------------------------------------------------------------------------------------------------------------
// Public methods
public function index()
{
$this->load->view('dashboard/dashboard_demo.php', []);
}
// -----------------------------------------------------------------------------------------------------------------
// Private methods
/**
* Retrieve the UID of the logged user and checks if it is valid
*/
private function _setAuthUID()
{
$this->_uid = getAuthUID();
if (!$this->_uid) show_error('User authentification failed');
}
}
@@ -0,0 +1,108 @@
<?php
defined('BASEPATH') || exit('No direct script access allowed');
/**
* Description of Widget
*
* @author chris
*/
class Widget extends Auth_Controller
{
public function __construct()
{
parent::__construct(
array(
'index' => 'dashboard/benutzer:r',
'getAll' => 'dashboard/admin:r',
'getWidgetsForDashboard' => 'dashboard/benutzer:rw',
'setAllowed' => 'dashboard/admin:rw'
)
);
$this->load->library('dashboard/DashboardLib', null, 'DashboardLib');
$this->load->model('dashboard/Widget_model', 'WidgetModel');
$this->load->model('dashboard/Dashboard_Widget_model', 'DashboardWidgetModel');
}
public function index()
{
$widget_id = $this->input->get('id');
$widget = $this->WidgetModel->load($widget_id);
if (isError($widget) || !getData($widget))
return $this->outputJsonSuccess([
"widget_id" => 0,
"widget_kurzbz" => "notfound",
"arguments" => json_encode([
"className" => 'alert-danger',
"title" => 'Widget Not Found',
"msg" => 'The widget with the id ' . $widget_id . ' could not be found'
]),
"setup" => json_encode([
"name" => 'Widget Not Found',
"file" => 'DashboardWidget/Default.js',
"width" => 1,
"height" => 1
])
]);
return $this->outputJsonSuccess(current(getData($widget)));
}
public function getAll()
{
$dashboard_id = $this->input->get('dashboard_id');
$result = $this->WidgetModel->getWithAllowedForDashboard($dashboard_id);
if (isError($result))
return $this->outputJsonError(getError($result));
$this->outputJsonSuccess(getData($result) ?: []);
}
public function getWidgetsForDashboard()
{
$db = $this->input->get('db');
$result = $this->WidgetModel->getForDashboard($db);
if (isError($result)) {
http_response_code(404);
$this->terminateWithJsonError([
'error' => getError($result)
]);
}
$this->outputJsonSuccess(getData($result) ?: []);
}
public function setAllowed() {
$input = $this->getPostJSON();
$dashboard_id = $input->dashboard_id;
$widget_id = $input->widget_id;
$action = $input->action;
if ($action == 'add') {
$result = $this->DashboardWidgetModel->insert([
'dashboard_id' => $dashboard_id,
'widget_id' => $widget_id
]);
} elseif ($action == 'delete') {
$result = $this->DashboardWidgetModel->delete([
'dashboard_id' => $dashboard_id,
'widget_id' => $widget_id
]);
} else {
http_response_code(404); // TODO(chris): 400?
$this->terminateWithJsonError([
'error' => 'action value invalid'
]);
}
if (isError($result)) {
http_response_code(404);
$this->terminateWithJsonError([
'error' => getError($result)
]);
}
return $this->outputJsonSuccess(getData($result));
}
}
@@ -0,0 +1,228 @@
<?php
defined('BASEPATH') || exit('No direct script access allowed');
/**
* Description of DashboardLib
*
* @author bambi
*/
class DashboardLib
{
const WIDGET_ID_RANDOM_BYTES = 16;
const DEFAULT_DASHBOARD_KURZBZ = 'fhcomplete';
const SECTION_IF_FUNKTION_KURZBZ_IS_NULL = 'general';
const USEROVERRIDE_SECTION = 'custom';
private $_ci; // CI instance
public function __construct($params=null)
{
// Loads CI instance
$this->_ci =& get_instance();
$this->_ci->load->model('dashboard/Dashboard_model', 'DashboardModel');
$this->_ci->load->model('dashboard/Dashboard_Preset_model', 'DashboardPresetModel');
$this->_ci->load->model('dashboard/Dashboard_Override_model', 'DashboardOverrideModel');
}
public function generateWidgetId($dashboard_kurzbz='')
{
$dashboard_kurzbz = (!empty($dashboard_kurzbz)) ? $dashboard_kurzbz
: self::DEFAULT_DASHBOARD_KURZBZ;
$widgetid_input = time() . '_' . $dashboard_kurzbz . '_'
. bin2hex(random_bytes(self::WIDGET_ID_RANDOM_BYTES));
$widgetid = md5($widgetid_input);
return $widgetid;
}
public function getDashboardByKurzbz($dashboard_kurzbz)
{
$dashboard = null;
$result = $this->_ci->DashboardModel->getDashboardByKurzbz($dashboard_kurzbz);
if( isSuccess($result) && ($dashboards = getData($result)) )
{
$dashboard = $dashboards[0];
}
return $dashboard;
}
public function getMergedConfig($dashboard_id, $uid)
{
$defaultconfig = $this->getDefaultConfig($dashboard_id, $uid);
$userconfig = $this->getUserConfig($dashboard_id, $uid);
$mergedconfig = array_replace_recursive($defaultconfig, $userconfig);
return $mergedconfig;
}
public function getDefaultConfig($dashboard_id, $uid)
{
$res_presets = $this->_ci->DashboardPresetModel->getPresets($dashboard_id, $uid);
$defaultconfig = array();
if( isSuccess($res_presets) && hasData($res_presets) )
{
$presets = getData($res_presets);
foreach ($presets as $presetobj)
{
if( null !== ($preset = json_decode($presetobj->preset, true)) )
{
$defaultconfig = array_replace_recursive($defaultconfig,
$preset);
}
}
}
return $defaultconfig;
}
public function getUserConfig($dashboard_id, $uid)
{
$res_userconfig = $this->_ci->DashboardOverrideModel->getOverride($dashboard_id, $uid);
$userconfig = array();
if( isSuccess($res_userconfig) && hasData($res_userconfig) )
{
$data = getData($res_userconfig);
if( null !== ($decodedconfig = json_decode($data[0]->override, true)) )
{
$userconfig = $decodedconfig;
}
}
return $userconfig;
}
public function getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid)
{
$override = $this->getOverride($dashboard_kurzbz, $uid);
if( null !== $override ) {
return $override;
}
$dashboard = $this->getDashboardByKurzbz($dashboard_kurzbz);
$emptyoverride = new stdClass();
$emptyoverride->dashboard_id = $dashboard->dashboard_id;
$emptyoverride->uid = $uid;
$emptyoverride->override = '{"widgets": {"' . self::USEROVERRIDE_SECTION . '": {}}}';
return $emptyoverride;
}
public function getPresetOrCreateEmptyPreset($dashboard_kurzbz, $funktion_kurzbz)
{
$preset = $this->getPreset($dashboard_kurzbz, $funktion_kurzbz);
if( null !== $preset ) {
return $preset;
}
$dashboard = $this->getDashboardByKurzbz($dashboard_kurzbz);
$emptypreset = new stdClass();
$emptypreset->dashboard_id = $dashboard->dashboard_id;
$emptypreset->funktion_kurzbz = $funktion_kurzbz;
$section = ($funktion_kurzbz !== null) ? $funktion_kurzbz : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
$emptypreset->preset = '{"widgets": {"' . $funktion_kurzbz . '": {}}}';
return $emptypreset;
}
public function getPreset($dashboard_kurzbz, $section)
{
$dashboard = $this->getDashboardByKurzbz($dashboard_kurzbz);
$preset = null;
$funktion_kurzbz = ($section === self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL) ? null : $section;
$result = $this->_ci->DashboardPresetModel
->getPresetByDashboardAndFunktion($dashboard->dashboard_id, $funktion_kurzbz);
if( isSuccess($result) && hasData($result) && ($presets = getData($result)) )
{
$preset = $presets[0];
}
return $preset;
}
public function getOverride($dashboard_kurzbz, $uid)
{
$dashboard = $this->getDashboardByKurzbz($dashboard_kurzbz);
$override = null;
$result = $this->_ci->DashboardOverrideModel
->getOverride($dashboard->dashboard_id, $uid);
if( isSuccess($result) && hasData($result) && ($overrides = getData($result)) )
{
$override = $overrides[0];
}
return $override;
}
public function insertOrUpdatePreset($preset)
{
if( isset($preset->preset_id) && $preset->preset_id > 0 )
{
$result = $this->_ci->DashboardPresetModel->update($preset->preset_id, $preset);
} else
{
$result = $this->_ci->DashboardPresetModel->insert($preset);
}
return $result;
}
public function insertOrUpdateOverride($override)
{
if( isset($override->override_id) && $override->override_id > 0 )
{
$result = $this->_ci->DashboardOverrideModel->update($override->override_id, $override);
} else
{
$result = $this->_ci->DashboardOverrideModel->insert($override);
}
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]) || !is_array($widgets[$section]) )
{
$widgets[$section] = array();
}
$widgets[$section][$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][$widgetid]) )
{
unset($widgets[$section][$widgetid]);
if(empty($widgets[$section]) && $section !== self::USEROVERRIDE_SECTION) {
unset($widgets[$section]);
}
return true;
}
else {
return false;
}
}
}
+15
View File
@@ -11,4 +11,19 @@ class News_model extends DB_Model
$this->dbTable = 'campus.tbl_news';
$this->pk = 'news_id';
}
/**
* Get all News ordered by date. (most actual on top)
* @param null $limit Amount of news.
* @return array
*/
public function getAll($limit = null)
{
return $this->loadWhere('
text IS NOT NULL
AND datum <= NOW() AND (datum_bis IS NULL OR datum_bis >= now()::date)
ORDER BY datum DESC
LIMIT '. $this->escape($limit)
);
}
}
@@ -0,0 +1,26 @@
<?php
class Dashboard_Override_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'dashboard.tbl_dashboard_benutzer_override';
$this->pk = 'override_id';
}
/**
* Get Overrides of given uid.
* @param integer dashboard_id
* @param string $uid
* @return array
*/
public function getOverride($dashboard_id, $uid)
{
return $this->loadWhere(array('dashboard_id' => $dashboard_id, 'uid'=> $uid));
}
}
@@ -0,0 +1,67 @@
<?php
class Dashboard_Preset_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'dashboard.tbl_dashboard_preset';
$this->pk = 'preset_id';
}
/**
* Get Presets of given uid.
* @param integer dashboard_id
* @param string $uid
* @return array
*/
public function getPresets($dashboard_id, $uid)
{
// TODO: get Funktionen for uid and load all preset for all funktionen for uid
//return $this->loadWhere(array('dashboard_id' => $dashboard_id, 'funktion_kurzbz'=> null));
$sql = <<<EOSQL
SELECT
*
FROM
dashboard.tbl_dashboard_preset
WHERE
dashboard_id = ?
AND (
funktion_kurzbz IN (
SELECT
DISTINCT funktion_kurzbz
FROM
public.tbl_benutzerfunktion
WHERE
uid = ?
AND
NOW()::date
BETWEEN
COALESCE(datum_von, '1970-01-01')
AND
COALESCE(datum_bis, '2170-12-31')
)
OR
funktion_kurzbz IS NULL
)
ORDER BY
funktion_kurzbz DESC
EOSQL;
return $this->execQuery($sql, array($dashboard_id, $uid));
}
/**
* Get Preset by Dashboard and Funktion
* @param integer dashboard_id
* @param string funktion_kurzbz
* @return array
*/
public function getPresetByDashboardAndFunktion($dashboard_id, $funktion_kurzbz)
{
return $this->loadWhere(array('dashboard_id' => $dashboard_id, 'funktion_kurzbz' => $funktion_kurzbz));
}
}
@@ -0,0 +1,16 @@
<?php
class Dashboard_Widget_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'dashboard.tbl_dashboard_widget';
$this->pk = ['dashboard_id', 'widget_id'];
$this->hasSequence = false;
}
}
@@ -0,0 +1,25 @@
<?php
class Dashboard_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$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));
}
}
@@ -0,0 +1,33 @@
<?php
class Widget_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'dashboard.tbl_widget';
$this->pk = 'widget_id';
}
public function getWithAllowedForDashboard($dashboard_id)
{
$this->addSelect($this->dbTable . '.*');
$this->addSelect('CASE WHEN dashboard_id = ? THEN 1 ELSE 0 END AS allowed', false);
$this->addJoin('dashboard.tbl_dashboard_widget', 'widget_id', 'LEFT');
return $this->execQuery($this->db->get_compiled_select($this->dbTable), [$dashboard_id]);
}
public function getForDashboard($db)
{
$this->addSelect($this->dbTable . '.*');
$this->addJoin('dashboard.tbl_dashboard_widget', 'widget_id');
$this->addJoin('dashboard.tbl_dashboard', 'dashboard_id');
return $this->loadWhere(['dashboard_kurzbz' => $db]);
}
}
@@ -0,0 +1,31 @@
<?php
$this->load->view('templates/FHC-Header',
array(
'title' => 'FH-Complete',
'bootstrap5' => true,
'fontawesome6' => true,
'axios027' => true,
'restclient' => true,
'vue3' => true,
'customJSModules' => ['public/js/apps/Dashboard.js'],
'customCSSs' => [
'public/css/components/dashboard.css'
],
'navigationcomponent' => true
)
);
?>
<div id="main">
<core-navigation-cmpt :add-side-menu-entries="appSideMenuEntries"></core-navigation-cmpt>
<div id="content">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Dashboard</h1>
</div>
<core-dashboard dashboard="CIS" apiurl="<?= site_url('dashboard'); ?>"></core-dashboard>
</div>
</div>
<?php $this->load->view('templates/FHC-Footer'); ?>
+27
View File
@@ -0,0 +1,27 @@
.alert-danger .form-check-input:checked {
border-color: #842029;
background-color: #842029;
}
.draganddropcontainer {
grid-template-columns:repeat(4,1fr);
gap: 1rem;
place-items: stretch;
place-content: stretch;
}
@media(max-width: 577px) {
.draganddropcontainer {
grid-template-columns:repeat(2,1fr);
}
}
.mirror-x {
transform: scaleX(-1);
}
.cursor-nw-resize {
cursor: nw-resize;
}
.cursor-move {
cursor: move;
}
+16
View File
@@ -0,0 +1,16 @@
import {CoreNavigationCmpt} from '../components/navigation/Navigation.js';
import CoreDashboard from '../components/Dashboard/Dashboard.js';
Vue.createApp({
data: () => ({
appSideMenuEntries: {}
}),
components: {
CoreNavigationCmpt,
CoreDashboard
/*,
"CoreFilterCmpt": CoreFilterCmpt,
"verticalsplit": verticalsplit,
"searchbar": searchbar*/
}
}).mount('#main');
+38
View File
@@ -0,0 +1,38 @@
export default {
name: "BaseModal",
props: [
"id",
"cModalClass", // optional: add custom classes to modal class
"cModalDialogClass" // optional: add custom classes to modal-dialog class
],
onMounted()
{
const modal = new bootstrap.Modal(this.$refs.modal, {})
modal.show();
},
template: `
<div class="modal fade"
ref="modal"
:class="this.cModalClass"
:id="this.id"
:aria-labelledby="this.id + '_label'" aria-hidden="true" tabindex="-1">
<div class="modal-dialog" :class="this.cModalDialogClass">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" :id="this.id + '_label'">
<slot name="title"></slot>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<slot name="body"></slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button><!-- default -->
</slot>
</div>
</div>
</div>
</div>`
}
+32
View File
@@ -0,0 +1,32 @@
export default {
name: "BaseOffcanvas",
props: [
"id",
"cOffcanvasClass", // optional: add custom classes to offcanvas class
"closeFunc"
],
computed: {
OffcanvasClass() {
return this.cOffcanvasClass || 'offcanvas-end'; // default: slide in from right to left
}
},
template: `
<div class="offcanvas"
:class="this.OffcanvasClass"
:id="this.id"
:aria-labelledby="this.id + '_label'"
tabindex="-1"
data-bs-backdrop="false">
<div class="offcanvas-header">
<h5 class="offcanvas-title" :id="this.id + '_label'">
<slot name="title"></slot>
</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" @click="closeFunc" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div>
<slot name="body"></slot>
</div>
</div>
</div>`
}
+44
View File
@@ -0,0 +1,44 @@
import BsModal from './Modal.js';
export default {
components: {
BsModal
},
mixins: [
BsModal
],
props: {
dialogClass: {
type: [String,Array,Object],
default: 'modal-dialog-centered'
},
/*
* NOTE(chris):
* Hack to expose in "emits" declared events to $props which we use
* in the v-bind directive to forward all events.
* @see: https://github.com/vuejs/core/issues/3432
*/
onHideBsModal: Function,
onHiddenBsModal: Function,
onHidePreventedBsModal: Function,
onShowBsModal: Function,
onShownBsModal: Function
},
data: () => ({
result: true
}),
mounted() {
this.modal = this.$refs.modalContainer.modal;
},
popup(msg, options) {
return BsModal.popup.bind(this)(msg, options);
},
template: `<bs-modal ref="modalContainer" class="bootstrap-alert" v-bind="$props">
<template v-slot:default>
<slot></slot>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</template>
</bs-modal>`
}
+22
View File
@@ -0,0 +1,22 @@
import BsAlert from './Alert';
export default {
mixins: [
BsAlert
],
data: () => ({
result: false
}),
popup(msg, options) {
return BsAlert.popup.bind(this)(msg, options);
},
template: `<bs-modal ref="modalContainer" class="bootstrap-confirm" v-bind="$props">
<template v-slot:default>
<slot></slot>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-primary" @click="result=true;this.hide()">OK</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</template>
</bs-modal>`
}
+104
View File
@@ -0,0 +1,104 @@
export default {
data: () => ({
modal: null
}),
props: {
backdrop: {
type: [Boolean,String],
default: true,
validator(value) {
return ['static', true, false].includes(value);
}
},
focus: {
type: Boolean,
default: true
},
keyboard: {
type: Boolean,
default: true
},
noCloseBtn: Boolean,
dialogClass: [String,Array,Object]
},
emits: [
"hideBsModal",
"hiddenBsModal",
"hidePreventedBsModal",
"showBsModal",
"shownBsModal"
],
methods: {
dispose() {
return this.modal.dispose();
},
handleUpdate() {
return this.modal.handleUpdate();
},
hide() {
return this.modal.hide();
},
show(relatedTarget) {
return this.modal.show(relatedTarget);
},
toggle() {
return this.modal.toggle();
}
},
mounted() {
if (this.$refs.modal)
this.modal = new bootstrap.Modal(this.$refs.modal, {
backdrop: this.backdrop,
focus: this.focus,
keyboard: this.keyboard
});
},
popup(body, options, title, footer) {
const BsModal = this;
return new Promise((resolve,reject) => {
const instance = Vue.createApp({
setup() {
return () => Vue.h(BsModal, {...{
class: 'fade'
},...options, ...{
ref: 'modal',
'onHidden.bs.modal': instance.unmount
}}, {
title: () => title,
default: () => body,
footer: () => footer
});
},
mounted() {
this.$refs.modal.show();
},
beforeUnmount() {
if (this.$refs.modal)
this.$refs.modal.result !== false ? resolve(this.$refs.modal.result) : reject();
},
unmounted() {
wrapper.parentElement.removeChild(wrapper);
}
});
const wrapper = document.createElement("div");
document.body.appendChild(wrapper);
instance.mount(wrapper);
});
},
template: `<div ref="modal" class="bootstrap-modal modal" tabindex="-1" @[\`hide.bs.modal\`]="$emit('hideBsModal')" @[\`hidden.bs.modal\`]="$emit('hiddenBsModal')" @[\`hidePrevented.bs.modal\`]="$emit('hidePreventedBsModal')" @[\`show.bs.modal\`]="$emit('showBsModal')" @[\`shown.bs.modal\`]="$emit('shownBsModal')">
<div class="modal-dialog" :class="dialogClass">
<div class="modal-content">
<div v-if="$slots.title" class="modal-header">
<h5 class="modal-title"><slot name="title"/></h5>
<button v-if="!noCloseBtn" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body px-4 py-5">
<slot></slot>
</div>
<div v-if="$slots.footer" class="modal-footer">
<slot name="footer"/>
</div>
</div>
</div>
</div>`
}
+36
View File
@@ -0,0 +1,36 @@
import BsAlert from './Alert';
export default {
mixins: [
BsAlert
],
props: {
placeholder: String,
default: String
},
data: () => ({
value: '',
result: false
}),
created() {
if (this.default)
this.value = this.default;
},
popup(msg, options) {
if (typeof options === 'string')
options = { default: options };
return BsAlert.popup.bind(this)(msg, options);
},
template: `<bs-modal ref="modalContainer" class="bootstrap-prompt" v-bind="$props">
<template v-slot:default>
<slot></slot>
<div>
<input ref="input" type="text" class="form-control" :placeholder="placeholder" v-model="value">
</div>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-primary" @click="result=value;this.hide()">OK</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</template>
</bs-modal>`
}
+113
View File
@@ -0,0 +1,113 @@
import BsPrompt from "../Bootstrap/Prompt.js";
import DashboardAdminEdit from "./Admin/Edit.js";
import DashboardAdminWidgets from "./Admin/Widgets.js";
import DashboardAdminPresets from "./Admin/Presets.js";
export default {
components: {
DashboardAdminEdit,
DashboardAdminWidgets,
DashboardAdminPresets
},
provide() {
return {
adminMode: true
};
},
data() {
return {
dashboards: [],
current: -1,
widgets: []
};
},
computed: {
apiurl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/dashboard';
},
dashboard() {
return this.dashboards.find(el => el.dashboard_id == this.current);
}
},
methods: {
dashboardAdd() {
let _name = '';
BsPrompt.popup('New Dashboard name').then(
name => {
_name = name;
return axios.post(this.apiurl + '/Dashboard/Create', {
dashboard_kurzbz: name
})
}
).then(res => {
let newDashboard = {
dashboard_id: res.data.retval,
dashboard_kurzbz: _name,
beschreibung: ''
};
this.dashboards.push(newDashboard);
this.current = newDashboard.dashboard_id;
}).catch(err => err !== undefined ? console.error('ERROR:', err) : 0);
},
dashboardUpdate(dashboard) {
// TODO(chris): Loading or message
axios.post(this.apiurl + '/Dashboard/Update', dashboard).then(() => {
let old = this.dashboards.find(el => el.dashboard_id == dashboard.dashboard_id);
old.dashboard_kurzbz = dashboard.dashboard_kurzbz;
old.beschreibung = dashboard.beschreibung;
}).catch(err => console.error('ERROR:', err));
},
dashboardDelete(dashboard_id) {
axios.post(this.apiurl + '/Dashboard/Delete', {dashboard_id}).then(() => {
this.current = -1;
this.dashboards = this.dashboards.filter(el => el.dashboard_id != dashboard_id);
}).catch(err => console.error('ERROR:', err));
},
assignWidgets(widgets) {
this.widgets = widgets;
/*while (this.widgets.length)
this.widgets.pop();
for (var i in widgets)
this.widgets.push(widgets[i]);*/
}
},
created() {
axios.get(this.apiurl + '/Dashboard').then(res => {
//console.log(res.data.retval);
this.dashboards = res.data.retval;
}).catch(err => console.error('ERROR:', err));
},
template: `<div class="dashboard-admin">
<div class="input-group">
<label for="dashbaord-select" class="input-group-text">Dashboard:</label>
<select id="dashbaord-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>
</div>
<div v-if="dashboard">
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link" id="edit-tab" data-bs-toggle="tab" data-bs-target="#edit" type="button" role="tab" aria-controls="edit" aria-selected="false">Edit</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link active" id="widgets-tab" data-bs-toggle="tab" data-bs-target="#widgets" type="button" role="tab" aria-controls="widgets" aria-selected="true">Widgets</button>
</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>
</li>
</ul>
<div class="tab-content pt-3">
<div class="tab-pane fade" id="edit" role="tabpanel" aria-labelledby="edit-tab">
<dashboard-admin-edit v-bind="dashboard" :key="dashboard.dashboard_id" @change="dashboardUpdate($event)" @delete="dashboardDelete($event)"></dashboard-admin-edit>
</div>
<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" @change="test" @assign-widgets="assignWidgets"></dashboard-admin-widgets>
</div>
<div class="tab-pane fade" id="presets" role="tabpanel" aria-labelledby="presets-tab">
<dashboard-admin-presets :dashboard="dashboard.dashboard_kurzbz" :widgets="widgets"></dashboard-admin-presets>
</div>
</div>
</div>
</div>`
}
@@ -0,0 +1,38 @@
import BsConfirm from '../../Bootstrap/Confirm.js';
export default {
emits: [
"change",
"delete"
],
props: {
dashboard_id: Number,
dashboard_kurzbz: String,
beschreibung: String
},
data() {
return {
kurzbz: this.dashboard_kurzbz,
desc: this.beschreibung
}
},
methods: {
sendDelete() {
BsConfirm.popup('Sure?').then(() => this.$emit('delete', this.dashboard_id)).catch();
}
},
template: `<div class="dashboard-admin-edit px-3">
<div class="mb-3">
<label for="dashboard-admin-edit-kurzbz">Kurz Bezeichnung</label>
<input id="dashboard-admin-edit-kurzbz" type="text" class="form-control" v-model="kurzbz">
</div>
<div class="mb-3">
<label for="dashboard-admin-edit-beschreibung">Beschreibung</label>
<textarea id="dashboard-admin-edit-beschreibung" class="form-control" v-model="desc"></textarea>
</div>
<div>
<button class="btn btn-danger" @click="sendDelete">Delete</button>
<button class="btn btn-primary" @click="$emit('change', {dashboard_id,dashboard_kurzbz:kurzbz,beschreibung:desc})">Update</button>
</div>
</div>`
}
@@ -0,0 +1,161 @@
import DashboardSection from "../Section.js";
import DashboardWidgetPicker from "../Widget/Picker.js";
import ObjectUtils from "../../../composables/ObjectUtils.js";
export default {
components: {
DashboardSection,
DashboardWidgetPicker
},
props: {
dashboard: String,
widgets: Array
},
data: () => ({
funktionen: {},
sections: [],
}),
computed: {
apiurl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/dashboard';
},
pickerWidgets() {
return this.widgets.filter(widget => widget.allowed);
}
},
methods: {
widgetAdd(section_name, widget) {
this.$refs.widgetpicker.getWidget().then(widget_id => {
widget.widget = widget_id;
delete widget.custom;
let loading = {...widget};
loading.loading = true;
this.sections.forEach(section => {
if (section.name == section_name)
section.widgets.push(loading);
});
axios.post(this.apiurl + '/Config/addWidgetsToPreset', {
db: this.dashboard,
funktion_kurzbz: section_name,
widgets: [widget]
}).then(result => {
let newId = Object.keys(result.data.retval.data.widgets[section_name]).pop();
widget.id = newId;
widget.custom = 1;
this.sections.forEach(section => {
if (section.name == section_name) {
section.widgets.splice(section.widgets.indexOf(loading),1);
section.widgets.push(widget);
}
});
}).catch(error => {
console.error('ERROR: ', error);
alert('ERROR: ' + error.response.data.retval);
});
}).catch(() => {});
},
widgetUpdate(section_name, payload) {
payload = payload[section_name];
for (var k in payload) {
for (var i in this.sections) {
if (this.sections[i].name == section_name) {
for (var wid in this.sections[i].widgets) {
if (this.sections[i].widgets[wid].id == k) {
payload[k] = ObjectUtils.mergeDeep(this.sections[i].widgets[wid], payload[k]);
// NOTE(chris): remove internal props
for (var prop in {_x:1,_y:1,_w:1,_h:1,index:1,id:1})
if (payload[k][prop])
delete payload[k][prop];
break;
}
}
break;
}
}
payload[k].widgetid = k;
delete payload[k].custom;
}
axios.post(this.apiurl + '/Config/addWidgetsToPreset', {
db: this.dashboard,
funktion_kurzbz: section_name,
widgets: payload
}).then(() => {
this.sections.forEach(section => {
if (section.name == section_name) {
section.widgets.forEach((widget, i) => {
if (payload[widget.id]) {
payload[widget.id].id = widget.id;
payload[widget.id].index = widget.index;
section.widgets[i] = payload[widget.id];
section.widgets[i].custom = 1;
}
});
}
});
}).catch(error => {
// TODO(chris): revert placement on failure
console.error('ERROR: ', error);
alert('ERROR: ' + error.response.data.retval);
});
},
widgetRemove(section_name, id) {
axios.post(this.apiurl + '/Config/removeWidgetFromPreset', {
db: this.dashboard,
funktion_kurzbz: section_name,
widgetid: id
}).then(() => {
this.sections.forEach(section => {
if (section.name == section_name)
section.widgets = section.widgets.filter(widget => widget.id != id);
});
}).catch(error => {
console.error('ERROR: ', error);
alert('ERROR: ' + error.response.data.retval);
});
},
loadSections(evt) {
let funktionen = Array.from(evt.target.querySelectorAll("option:checked"),e=>e.value);
this.sections = [];
axios.get(this.apiurl + '/Config/PresetBatch', {params: {
db: this.dashboard,
funktionen
}}).then(res => {
for (var section in res.data.retval) {
let widgets = [];
for (var wid in res.data.retval[section]) {
res.data.retval[section][wid].id = wid;
res.data.retval[section][wid].custom = 1;
widgets.push(res.data.retval[section][wid]);
}
this.sections.push({
name: section,
widgets
});
}
}).catch(err => console.error('ERROR:', err));
}
},
created() {
axios.get(this.apiurl + '/Config/Funktionen').then(res => {
//console.log(res.data.retval);
this.funktionen = {general: 'GENERAL'};
res.data.retval.forEach(funktion => {
this.funktionen[funktion.funktion_kurzbz] = funktion.beschreibung;
});
}).catch(err => console.error('ERROR:', err));
},
template: `<div class="dashboard-admin-presets">
<div class="row">
<div class="col-3">
<select class="form-control" multiple @input="loadSections">
<option v-for="name,id in funktionen" :key="id" :value="id">{{ name }}</option>
</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>
</div>
</div>
<dashboard-widget-picker ref="widgetpicker" :widgets="pickerWidgets"></dashboard-widget-picker>
</div>`
}
@@ -0,0 +1,45 @@
export default {
emits: [
"change",
"assignWidgets"
],
props: {
dashboard_id: Number,
widgets: Array
},
computed: {
apiurl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/dashboard';
}
},
methods: {
sendChange(widget_id) {
let allow = !this.widgets.find(el => el.widget_id == widget_id).allowed;
axios.post(this.apiurl + '/Widget/setAllowed', {
dashboard_id: this.dashboard_id,
widget_id,
action: allow ? 'add' : 'delete'
}).catch(err => console.error('ERROR: ' + err));
}
},
created() {
axios.get(this.apiurl + '/Widget/getAll', {
params:{
dashboard_id: this.dashboard_id
}
}).then(
result => {
this.$emit('assignWidgets', result.data.retval.map(el => ({
...el,
...{setup:JSON.parse(el.setup),arguments:JSON.parse(el.arguments),allowed:!!el.allowed}
})));
}
).catch(err => console.error('ERROR:', err));
},
template: `<div class="dashboard-admin-widgets">
<div v-for="widget in widgets" :key="widget.widget_id" class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" :id="'dashboard-admin-widgets-' + widget.widget_id" v-model="widget.allowed" @input.prevent="sendChange(widget.widget_id)">
<label class="form-check-label" :for="'dashboard-admin-widgets-' + widget.widget_id">{{(widget.setup && widget.setup.name) || widget.widget_kurzbz}}</label>
</div>
</div>`
}
+144
View File
@@ -0,0 +1,144 @@
import DashboardSection from "./Section.js";
import DashboardWidgetPicker from "./Widget/Picker.js";
import ObjectUtils from "../../composables/ObjectUtils.js";
export default {
components: {
DashboardSection,
DashboardWidgetPicker
},
props: [
"dashboard"
],
data: () => ({
sections: [],
widgets: null
}),
computed: {
apiurl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/dashboard';
}
},
methods: {
widgetAdd(section_name, widget) {
if (this.widgets === null) {
axios.get(this.apiurl + '/Widget/getWidgetsForDashboard', {params:{
db: this.dashboard
}}).then(res => {
//console.log(res.data.retval);
res.data.retval.forEach(widget => {
widget.arguments = JSON.parse(widget.arguments);
widget.setup = JSON.parse(widget.setup);
});
this.widgets = res.data.retval;
}).catch(err => console.error('ERROR:', err));
}
this.$refs.widgetpicker.getWidget().then(widget_id => {
widget.widget = widget_id;
let loading = {...widget};
loading.loading = true;
this.sections.forEach(section => {
if (section.name == section_name)
section.widgets.push(loading);
});
axios.post(this.apiurl + '/Config/addWidgetsToUserOverride', {
db: this.dashboard,
funktion_kurzbz: section_name,
widgets: [widget]
}).then(result => {
let newId = Object.keys(result.data.retval.data.widgets[section_name]).pop();
widget.id = newId;
this.sections.forEach(section => {
if (section.name == section_name) {
section.widgets.splice(section.widgets.indexOf(loading),1);
section.widgets.push(widget);
}
});
}).catch(error => {
console.error('ERROR: ', error);
alert('ERROR: ' + error.response.data.retval);
});
}).catch(() => {});
},
widgetUpdate(section_name, payload) {
payload = payload[section_name];
for (var k in payload) {
for (var i in this.sections) {
if (this.sections[i].name == section_name) {
for (var wid in this.sections[i].widgets) {
if (this.sections[i].widgets[wid].id == k) {
payload[k] = ObjectUtils.mergeDeep(this.sections[i].widgets[wid], payload[k]);
// NOTE(chris): remove internal props
for (var prop in {_x:1,_y:1,_w:1,_h:1,index:1,id:1})
if (payload[k][prop])
delete payload[k][prop];
break;
}
}
break;
}
}
payload[k].widgetid = k;
}
axios.post(this.apiurl + '/Config/addWidgetsToUserOverride', {
db: this.dashboard,
funktion_kurzbz: section_name,
widgets: payload
}).then(() => {
this.sections.forEach(section => {
if (section.name == section_name) {
section.widgets.forEach((widget, i) => {
if (payload[widget.id]) {
payload[widget.id].id = widget.id;
payload[widget.id].index = widget.index;
section.widgets[i] = payload[widget.id];
}
});
}
});
}).catch(error => {
// TODO(chris): revert placement on failure
console.error('ERROR: ', error);
alert('ERROR: ' + error.response.data.retval);
});
},
widgetRemove(section_name, id) {
axios.post(this.apiurl + '/Config/removeWidgetFromUserOverride', {
db: this.dashboard,
funktion_kurzbz: section_name,
widgetid: id
}).then(() => {
this.sections.forEach(section => {
if (section.name == section_name)
section.widgets = section.widgets.filter(widget => widget.id != id);
});
}).catch(error => {
console.error('ERROR: ', error);
alert('ERROR: ' + error.response.data.retval);
});
}
},
created() {
axios.get(this.apiurl + '/Config', {params:{
db: this.dashboard
}}).then(res => {
//console.log(res.data.retval);
for (var name in res.data.retval.widgets) {
let widgets = [];
for (var wid in res.data.retval.widgets[name]) {
res.data.retval.widgets[name][wid].id = wid;
widgets.push(res.data.retval.widgets[name][wid]);
}
this.sections.push({
name: name,
widgets: widgets
});
}
}).catch(err => console.error('ERROR:', err));
},
template: `<div class="core-dashboard">
<dashboard-section v-for="section in sections" :key="section.name" :name="section.name" :widgets="section.widgets" @widgetAdd="widgetAdd" @widgetUpdate="widgetUpdate" @widgetRemove="widgetRemove"></dashboard-section>
<dashboard-widget-picker ref="widgetpicker" :widgets="widgets"></dashboard-widget-picker>
</div>`
}
+138
View File
@@ -0,0 +1,138 @@
import BsModal from "../Bootstrap/Modal.js";
import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
export default {
components: {
BsModal
},
data: () => ({
component: '',
arguments: null,
target: false,
widget: null,
tmpConfig: {},
isLoading: false,
hasConfig: true
}),
emits: [
"change",
"remove",
"dragstart",
"resizestart",
],
props: [
"id",
"config",
"width",
"height",
"custom",
"hidden",
"editMode",
"loading"
],
computed: {
isResizeable() {
if (!this.widget)
return false;
return this.widget.setup.width.max || this.widget.setup.height.max;
},
ready() {
return this.component && this.arguments !== null
}
},
methods: {
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();
},
setConfig(hasConfig) {
this.hasConfig = hasConfig;
},
changeConfig() {
this.isLoading = true;
let config = {...this.tmpConfig};
this.sendChangeConfig(config);
},
changeConfigManually() {
let config = {...this.arguments};
this.sendChangeConfig(config);
},
sendChangeConfig(config) {
for (var k in config) {
if (this.widget.arguments[k] == config[k]) {
delete config[k];
}
}
this.$emit('change', config);
}
},
watch: {
config() {
this.arguments = {...this.widget.arguments, ...this.config};
this.tmpConfig = {...this.arguments};
this.$refs.config.hide();
this.isLoading = false;
}
},
async created() {
this.widget = await CachedWidgetLoader.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: `<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>
</div>
<div v-else-if="!hidden || editMode" :class="'dashboard-item card overflow-hidden ' + (arguments ? arguments.className : '')" @mousedown="mouseDown($event)" @dragstart="startDrag($event)" :draggable="!!editMode">
<div v-if="editMode && widget" class="card-header d-flex ps-0 pe-2">
<span ref="dragHandle" class="col-auto mx-2 px-2 cursor-move"><i class="fa-solid fa-grip-vertical"></i></span>
<span class="col">{{ widget.setup.name }}</span>
<a v-if="hasConfig" class="col-auto px-1" href="#" @click.prevent="openConfig"><i class="fa-solid fa-gear"></i></a>
<a v-if="custom" class="col-auto px-1" href="#" @click.prevent="$emit('remove')">
<i class="fa-solid fa-trash"></i>
</a>
<div v-else class="col-auto px-1 form-switch">
<input class="form-check-input ms-0" type="checkbox" role="switch" id="flexSwitchCheckChecked" :checked="!hidden" @input="$emit('remove', hidden)">
</div>
</div>
<div v-if="ready" class="card-body overflow-hidden">
<component :is="component" :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 ref="config">
<template v-slot:title>
{{ widget ? 'Config for ' + widget.setup.name : '' }}
</template>
<template v-slot:default>
<component v-if="ready && !isLoading" :is="component" :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-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>
<div v-if="editMode && isResizeable" class="card-footer d-flex justify-content-end p-0">
<span ref="resizeHandle" class="col-auto px-1 cursor-nw-resize" @dragstart.prevent="$emit('resize')"><i class="fa-solid fa-up-right-and-down-left-from-center mirror-x"></i></span>
</div>
</div>`
}
+429
View File
@@ -0,0 +1,429 @@
import BsConfirm from "../Bootstrap/Confirm.js";
import DashboardItem from "./Item.js";
import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
// TODO(chris): handle overflow (moving outside the box)
export default {
components: {
DashboardItem
},
inject: {
adminMode: {
default: false
}
},
props: [
"name",
"widgets"
],
emits: [
"widgetAdd",
"widgetUpdate",
"widgetRemove"
],
data() {
return {
gridWidth: 0,
containerRect: {top:0,left:0},
changeHeight: 1,
movedObjects: [],
editMode: this.adminMode ? 1 : 0,
gridXLast: 0,
gridYLast: 0,
dataTransfer: {}
}
},
computed: {
items() {
this.widgets.forEach((item,i) => item.index = i);
return this.widgets;
},
itemCoords() {
if (!this.gridWidth)
return [];
let itemCoords = this.items.map(item => item.place[this.gridWidth] || this.createItemPlacement(item));
// TODO(chris): verify positions & sizes
let occupiers = [];
let wrongPlacedItems = [];
let gridWidth = this.gridWidth;
this.items.forEach(item => {
let x = item._x !== undefined ? item._x : itemCoords[item.index].x;
let y = item._y !== undefined ? item._y : itemCoords[item.index].y;
let w = item._w !== undefined ? item._w : itemCoords[item.index].w;
let h = item._h !== undefined ? item._h : itemCoords[item.index].h;
// TODO(chris): check with and height params here?
for (var i = 0; i < w; i++) {
for (var j = 0; j < h; j++) {
var c = (y+j-1) * gridWidth + (x+i-1);
// NOTE(chris): check for overlaping items
if (occupiers[c] !== undefined) {
//console.log('try to add ' + item.index + ' to ' + x + '/' + y + ', but ' + occupiers[c] + ' is already there');
// NOTE(chris): remove possible other entries of this item
for (var c2 = c; c2; c2--)
if (occupiers[c2] == item.index)
occupiers[c2] = undefined;
wrongPlacedItems.push(item);
return;
}
occupiers[c] = item.index;
}
}
});
wrongPlacedItems.forEach(item => {
let w = item._w !== undefined ? item._w : itemCoords[item.index].w;
let h = item._h !== undefined ? item._h : itemCoords[item.index].h;
for (var c = 0; c < occupiers.length + gridWidth; c++) {
if (occupiers[c] === undefined) {
var occupied = false, i, j;
for (i = 0; i < w; i++) {
for (j = 0; j < h; j++) {
if (occupiers[c + i + j * gridWidth] !== undefined) {
i = w;
occupied = true;
break;
}
}
}
if (!occupied) {
item.place[gridWidth].x = c%gridWidth + 1;
item.place[gridWidth].y = Math.floor(c/gridWidth) + 1;
for (i = 0; i < w; i++) {
for (j = 0; j < h; j++) {
occupiers[c + i + j * gridWidth] = item.index;
}
}
return;
}
}
}
});
return itemCoords;
},
gridHeight() {
if (!this.gridWidth || !this.changeHeight)
return 0;
let minH = 0;
this.itemCoords.forEach((item,i) => minH = Math.max(minH, (!this.editMode && this.items[i].hidden) ? 0 : item.y + item.h - 1));
// TODO(chris): the extraline should only be present if all slots are occupied
return minH + this.editMode;
},
gridOccupiers() {
let occupiers = [];
let gridWidth = this.gridWidth;
this.items.forEach(item => {
let x = item._x !== undefined ? item._x : this.itemCoords[item.index].x;
let y = item._y !== undefined ? item._y : this.itemCoords[item.index].y;
let w = item._w !== undefined ? item._w : this.itemCoords[item.index].w;
let h = item._h !== undefined ? item._h : this.itemCoords[item.index].h;
for (var i = 0; i < w; i++) {
for (var j = 0; j < h; j++) {
var c = (y+j-1) * gridWidth + (x+i-1);
occupiers[c] = item.index;
}
}
});
return occupiers;
}
},
methods: {
addWidget(evt) {
if (evt.target != this.$refs.container || !this.editMode)
return;
const rect = this.containerRect;
const gridX = Math.floor(this.gridWidth * (evt.clientX - rect.left) / this.$refs.container.clientWidth);
const gridY = Math.floor(this.gridHeight * (evt.clientY - rect.top) / this.$refs.container.clientHeight);
if (this.gridOccupiers[gridY * this.gridWidth + gridX] === undefined) {
let widget = { widget: 1, config: {}, place: {}, custom: 1 };
widget.place[this.gridWidth] = {
x: gridX,
y: gridY,
w: 1,
h: 1
};
this.$emit('widgetAdd', this.name, widget);
}
},
createItemPlacement(item) {
// TODO(chris): create correct default placement if it is not there
item.place[this.gridWidth] = {x:1,y:1,w:1,h:1};
/*var freeList = [], nextId = 0;
this.items.forEach(item => {
if (!item.place[this.gridWidth]) {
if (!this.gridWidth) {
item.place[this.gridWidth] = {x:1,y:nextId++,w:1,h:1};
} else {
// TODO(chris): IMPLEMENT widths & heights
if (freeList[nextId])
while (freeList[++nextId]);
freeList[nextId] = 1;
item.place[this.gridWidth] = {x:(nextId%this.gridWidth)+1,y:Math.floor(nextId/this.gridWidth)+1,w:1,h:1};
}
}
});*/
return item.place[this.gridWidth];
},
startDrag(evt, item) {
this.gridXLast = -1;
this.gridYLast = -1;
item._x = this.itemCoords[item.index].x;
item._y = this.itemCoords[item.index].y;
evt.dataTransfer.dropEffect = 'move';
evt.dataTransfer.effectAllowed = 'move';
this.dataTransfer = {
action: 'm',
id: item.index,
w: this.itemCoords[item.index].w,
h: this.itemCoords[item.index].h
}
},
startResize(evt, item) {
this.gridXLast = -1;
this.gridYLast = -1;
item._w = this.itemCoords[item.index].w;
item._h = this.itemCoords[item.index].h;
evt.dataTransfer.setDragImage(evt.target, -99999, -99999);
evt.dataTransfer.dropEffect = 'move';
evt.dataTransfer.effectAllowed = 'move';
this.dataTransfer = {
action: 'r',
id: item.index,
x: this.itemCoords[item.index].x,
y: this.itemCoords[item.index].y
}
},
occupyFields(id, x, y, w, h) {
var c;
while ((c = this.movedObjects.pop())) {
if (this.items[c]._y !== undefined) {
this.items[c].place[this.gridWidth].y = this.items[c]._y;
this.items[c]._y = undefined;
}
}
var move = {};
move[id] = this.items[id];
this.getOccupiedItems(x,y,w,h,move);
h = y + h;
y = 0;
for (x in move) {
if (x != id) {
c = move[x]._y !== undefined ? move[x]._y : this.itemCoords[x].y;
if (c < h)
y = Math.max(h-c, y);
}
}
for (x in move) {
if (x != id) {
this.movedObjects.push(x);
if (move[x]._y === undefined) {
move[x]._y = this.itemCoords[x].y;
}
move[x].place[this.gridWidth].y = move[x]._y + y;
}
}
},
getOccupiedItems(x, y, w, h, move) {
var i, j, c;
for (i = 0; i < w; i++) {
for (j = 0; j < h; j++) {
c = (y+j-1) * this.gridWidth + (x+i-1);
if (this.gridOccupiers[c] !== undefined && !move[this.gridOccupiers[c]]) {
move[this.gridOccupiers[c]] = this.items[this.gridOccupiers[c]];
c = this.itemCoords[this.gridOccupiers[c]];
this.getOccupiedItems(c.x, c.y + 1, c.w, c.h, move);
}
}
}
},
onDragOver(evt) {
let id, x, y, w, h;
const action = this.dataTransfer.action;
const rect = this.containerRect;
const gridX = Math.floor(this.gridWidth * (evt.clientX - rect.left) / this.$refs.container.clientWidth);
const gridY = Math.floor(this.gridHeight * (evt.clientY - rect.top) / this.$refs.container.clientHeight);
if (this.gridXLast == gridX && this.gridYLast == gridY)
return;
this.gridXLast = gridX;
this.gridYLast = gridY;
if (action == 'm') {
x = Math.max(gridX + 1, 1);
y = Math.max(gridY + 1, 1);
w = parseInt(this.dataTransfer.w);
h = parseInt(this.dataTransfer.h);
if (x + w > this.gridWidth + 1)
x = this.gridWidth + 1 - w;
id = this.dataTransfer.id;
this.occupyFields(id, x, y, w, h);
this.itemCoords[id].x = x;
this.itemCoords[id].y = y;
} else if (action == 'r') {
x = parseInt(this.dataTransfer.x);
y = parseInt(this.dataTransfer.y);
w = gridX + 2 - x;
h = gridY + 2 - y;
w = Math.max(1, w);
h = Math.max(1, h);
if (x + w > this.gridWidth + 1)
w = this.gridWidth + 1 - x;
id = this.dataTransfer.id;
let widget = CachedWidgetLoader.getWidget(this.items[id].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;
}
this.occupyFields(id, x, y, w, h);
this.itemCoords[id].w = w;
this.itemCoords[id].h = h;
}
},
onDrop() {
let id = 0;
let update = {};
while ((id = this.movedObjects.pop())) {
if (this.items[id]._y !== undefined) {
if (this.itemCoords[id].y != this.items[id]._y) {
update[this.items[id].id] = {place:{}};
update[this.items[id].id].place[this.gridWidth] = {y:this.itemCoords[id].y};
}
this.items[id]._y = undefined;
}
}
id = this.dataTransfer.id;
const action = this.dataTransfer.action;
update[this.items[id].id] = {place:{}};
update[this.items[id].id].place[this.gridWidth] = {};
if (action == 'm') {
if (this.items[id]._x !== undefined) {
if (this.itemCoords[id].x != this.items[id]._x) {
update[this.items[id].id].place[this.gridWidth].x = this.itemCoords[id].x;
}
this.items[id]._x = undefined;
}
if (this.items[id]._y !== undefined) {
if (this.itemCoords[id].y != this.items[id]._y) {
update[this.items[id].id].place[this.gridWidth].y = this.itemCoords[id].y;
}
this.items[id]._y = undefined;
}
} else if (action == 'r') {
update[this.items[id].id].place[this.gridWidth].w = this.itemCoords[id].w;
update[this.items[id].id].place[this.gridWidth].h = this.itemCoords[id].h;
}
if (update[this.items[id].id].place[this.gridWidth].x === undefined &&
update[this.items[id].id].place[this.gridWidth].y === undefined &&
update[this.items[id].id].place[this.gridWidth].w === undefined &&
update[this.items[id].id].place[this.gridWidth].h === undefined) {
delete update[this.items[id].id].place[this.gridWidth];
}
this.updatePreset(update);
// TODO(chris): find better way to trigger change for gridHeight
this.changeHeight++
},
removeWidget(item, revert) {
if (item.custom) {
BsConfirm.popup('Are you sure you want to delete this widget?').then(() => this.$emit('widgetRemove', this.name, item.id));
} else {
let update = {};
update[item.id] = { hidden: !revert };
this.updatePreset(update);
}
},
saveConfig(config, item) {
let payload = {};
payload[item.id] = { config };console.log(payload);
this.updatePreset(payload);
},
updatePreset(update) {
let payload = {};
payload[this.name] = update;
this.$emit('widgetUpdate', this.name, payload);
}
},
mounted() {
let self = this;
let cont = self.$refs.container;
self.gridWidth = window.getComputedStyle(cont).getPropertyValue('grid-template-columns').split(" ").length;
self.containerRect = cont.getBoundingClientRect();
window.addEventListener('resize', () => {
for (const child of cont.children) {
child.style.display = 'none';
}
self.gridWidth = window.getComputedStyle(cont).getPropertyValue('grid-template-columns').split(" ").length;
self.containerRect = cont.getBoundingClientRect();
for (const child of cont.children) {
child.style.display = '';
}
});
},
template: `<div class="dashboard-section">
<h3 class="d-flex">
<span class="col">{{name}}</span>
<button class="col-auto btn" @click.prevent="editMode = editMode ? 0 : 1"><i class="fa-solid fa-gear"></i></button>
</h3>
<div class="position-relative" :style="'height:0;padding-bottom:' + (gridHeight * 100/gridWidth) + '%'">
<div ref="container"
class="position-absolute top-0 left-0 w-100 h-100 draganddropcontainer"
:style="'display:grid;grid-template-rows:repeat('+gridHeight+',1fr)'"
@click="addWidget($event)"
@drop="onDrop($event, 1)"
@dragover.prevent="onDragOver"
@dragenter.prevent>
<dashboard-item
v-for="item in items"
:key="item.id"
:id="item.widget"
:loading="item.loading"
:config="item.config"
:custom="item.custom"
:hidden="item.hidden"
:editMode="editMode"
:x="itemCoords[item.index] ? itemCoords[item.index].x : -1"
:y="itemCoords[item.index] ? itemCoords[item.index].y : -1"
:style="itemCoords[item.index] ? {'grid-column-start':itemCoords[item.index].x,'grid-column-end':itemCoords[item.index].x+itemCoords[item.index].w,'grid-row-start':itemCoords[item.index].y,'grid-row-end':itemCoords[item.index].y+itemCoords[item.index].h} : {}"
:width="itemCoords[item.index] ? itemCoords[item.index].w : 0"
:height="itemCoords[item.index] ? itemCoords[item.index].h : 0"
@dragstart="startDrag($event, item)"
@resizestart="startResize($event, item)"
@change="saveConfig($event, item)"
@remove="removeWidget(item, $event)">
</dashboard-item>
</div>
</div>
</div>`
}
@@ -0,0 +1,59 @@
import BsModal from "../../Bootstrap/Modal.js";
export default {
components: {
BsModal
},
props: [
"widgets"
],
data: () => ({
callbacks: {}
}),
methods: {
getWidget() {
return new Promise((resolve,reject) => {
this.callbacks = {resolve,reject};
this.$refs.modal.show();
});
},
close() {
if (this.callbacks.reject)
this.callbacks.reject();
this.callbacks = {};
},
pick(widget_id) {
if (this.callbacks.resolve)
this.callbacks.resolve(widget_id);
this.callbacks = {};
this.$refs.modal.hide();
},
path(src) {
if (src[0] == '/')
return FHC_JS_DATA_STORAGE_OBJECT.app_root + src;
return src;
}
},
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">
<div v-if="!widgets.length">
No Widgets available
</div>
<div v-for="widget in widgets" :key="widget.widget_id" class="col-sm-6 col-md-4 col-lg-3 col-xl-2">
<div class="card h-100" @click="pick(widget.widget_id)">
<img class="card-img-top" :src="path(widget.setup.icon)" :alt="'pictogram for ' + (widget.setup.name || widget.widget_kurzbz)">
<div class="card-body">
<h5 class="card-title">{{ widget.setup.name || widget.widget_kurzbz }}</h5>
<p class="card-text">{{ widget.beschreibung }}</p>
</div>
</div>
</div>
</div>
<div v-else class="text-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></div>
</template>
</bs-modal>
</div>`
}
@@ -0,0 +1,30 @@
export default {
props: [
"config",
"width",
"height",
"configMode"
],
emits: [
"setConfig",
"change" // TODO(chris): do we need this?
],
computed: {
apiurl() {
const app_root = FHC_JS_DATA_STORAGE_OBJECT.app_root;
const ci_router = FHC_JS_DATA_STORAGE_OBJECT.ci_router;
return app_root + ci_router;
}
},
methods: {
formatDateTime: function(dateTime) {
const dt = new Date(dateTime);
return dt.getDate() + '.' + dt.getMonth() + '.' + dt.getFullYear() + ' | '
+ dt.getHours() + ':' + dt.getMinutes();
},
getDate: function(dateTime){
const dt = new Date(dateTime);
return dt.getDate() + '.' + dt.getMonth() + '.' + dt.getFullYear();
}
}
}
@@ -0,0 +1,345 @@
import AbstractWidget from './Abstract';
import BaseOffcanvas from '../Base/Offcanvas';
export default {
name: 'WidgetsAmpel',
components: { BaseOffcanvas },
data: () => ({
filter: '',
source: '',
ampeln: []
}),
mixins: [
AbstractWidget
],
computed: {
widgetAmpeln () {
return this.ampeln.slice(0, 4); // show only newest 4 ampeln
},
offcanvasAmpeln ()
{
switch(this.filter)
{
case 'verpflichtend': return this.ampeln.filter(item => item.verpflichtend);
case 'ueberfaellig': return this.ampeln.filter(item => (new Date() > new Date(item.deadline)) && !item.bestaetigt);
default: return this.ampeln;
}
},
count () {
return {
verpflichtend: this.ampeln.filter(item => item.verpflichtend).length,
ueberfaellig: this.ampeln.filter(item => (new Date() > new Date(item.deadline)) && !item.bestaetigt).length,
alle: this.ampeln.length
}
}
},
methods: {
closeOffcanvasAmpeln()
{
for (let i = 0; i < this.offcanvasAmpeln.length; i++)
{
let ampelId = this.offcanvasAmpeln[i].ampel_id;
this.$refs['ampelCollapse_' + ampelId][0].classList.remove('show');
}
},
openOffcanvasAmpel(ampelId){
// Close earlier opened Ampeln
this.closeOffcanvasAmpeln();
// Show given Ampel
this.$refs['ampelCollapse_' + ampelId][0].classList.add('show');
},
closeOffcanvas(){
this.filter = '';
},
confirm(ampelId){
let indexToRemove = this.ampeln.findIndex((obj => obj.ampel_id === ampelId));
this.ampeln.splice(indexToRemove, 1);
},
changeDisplay(){
this.filter = '';
if (this.source == 'offen')
{
this.ampeln = TEST_OFFENE_AMPELN;
}
if (this.source == 'alle')
{
this.ampeln = TEST_ALLE_AMPELN;
// axios
// .get(this.apiurl + '/dashboard/Api/getAmpeln')
// .then(res => { this.ampeln = res.data })
// .catch(err => { console.error('ERROR: ', err.response.data) });
}
},
validateBtnTxt(buttontext){
return buttontext == null ? 'Bestätigen' : buttontext;
}
},
created() {
this.$emit('setConfig', false);
this.ampeln = TEST_OFFENE_AMPELN;
},
template: `
<div class="widgets-ampel w-100 h-100">
<div class="d-flex flex-column justify-content-between">
<div class="d-flex">
<header class="me-auto"><b>Neueste Ampeln</b></header>
<div class="mb-2 text-danger"><a href="#allAmpelOffcanvas" data-bs-toggle="offcanvas">Alle Ampeln</a></div>
</div>
<div class="d-flex justify-content-end">
<a v-if="count.ueberfaellig > 0" href="#allAmpelOffcanvas" data-bs-toggle="offcanvas" @click="filter = 'ueberfaellig'" class="text-decoration-none"><span class="badge bg-danger me-1"><i class="fa fa-solid fa-bolt"></i> Überfällig: <b>{{ count.ueberfaellig }}</b></span></a>
</div>
<div v-for="ampel in widgetAmpeln" :key="ampel.ampel_id" class="mt-2">
<div class="card">
<div class="card-body">
<div class="position-relative">
<div class="d-flex">
<div class="text-muted small me-auto"><small>Deadline: {{ getDate(ampel.deadline) }}</small></div>
<div v-if="(new Date() > new Date(ampel.deadline)) && !ampel.bestaetigt "><span class="badge bg-danger"><i class="fa fa-solid fa-bolt"></i></span></div>
<div v-if="ampel.verpflichtend"><span class="badge bg-warning ms-1"><i class="fa fa-solid fa-triangle-exclamation"></i></span></div>
<div v-if="ampel.bestaetigt"><span class="badge bg-success ms-1"><i class="fa fa-solid fa-circle-check"></i></span></div>
</div>
</div>
<a href="#allAmpelOffcanvas" data-bs-toggle="offcanvas" class="stretched-link" @click="openOffcanvasAmpel(ampel.ampel_id)">{{ ampel.kurzbz }}</a><br>
</div>
</div>
</div>
<div v-if="ampeln.length == 0" class="card card-body mt-4 p-4 text-center">
<span class="text-success h2"><i class="fa fa-solid fa-circle-check"></i></span>
<span class="text-success h5">Super!</span><br>
<span class="small">Keine offenen Ampeln.</span>
</div>
</div>
</div>
<!-- All Ampeln Offcanvas -->
<BaseOffcanvas id="allAmpelOffcanvas" :closeFunc="closeOffcanvas">
<template #title><header><b>Alle meine Ampeln</b></header></template>
<template #body>
<div class="d-flex justify-content-evenly">
<div class="form-check form-check-inline form-control-sm">
<input class="form-check-input" type="radio" v-model="source" id="offen" value="offen" @change="changeDisplay" checked>
<label class="form-check-label" for="offen">Offene Ampeln</label>
</div>
<div class="form-check form-check-inline form-control-sm">
<input class="form-check-input" type="radio" v-model="source" id="alle" value="alle" @change="changeDisplay">
<label class="form-check-label" for="alle">Alle Ampeln</label>
</div>
</div>
<div class="col"><button class="btn btn-light w-100" @click="filter = ''"><small>Alle: <b>{{ count.alle }}</b></small></button></div>
<div class="row row-cols-2 g-2 mt-1">
<div class="col"><button class="btn btn-danger w-100" @click="filter = 'ueberfaellig'"><i class="fa fa-solid fa-bolt me-2"></i><small>Überfällig: <b>{{ count.ueberfaellig }}</b></small></button></div>
<div class="col"><button class="btn btn-warning w-100" @click="filter = 'verpflichtend'"><i class="fa fa-solid fa-triangle-exclamation me-2"></i><small>Pflicht: <b>{{ count.verpflichtend }}</b></small></button></div>
</div>
<div v-for="ampel in offcanvasAmpeln" :key="ampel.ampel_id" class="mt-2">
<ul class="list-group">
<li class="list-group-item small">
<div class="position-relative"><!-- prevents streched-link from stretching outside this parent element -->
<div class="d-flex">
<span class="small text-muted me-auto"><small>Deadline: {{ getDate(ampel.deadline) }}</small></span>
<div v-if="(new Date() > new Date(ampel.deadline)) && !ampel.bestaetigt"><span class="badge bg-danger"><i class="fa fa-solid fa-bolt"></span></div>
<div v-if="ampel.verpflichtend"><span class="badge bg-warning ms-1"><i class="fa fa-solid fa-triangle-exclamation"></span></div>
<div v-if="ampel.bestaetigt"><span class="badge bg-success ms-1"><i class="fa fa-solid fa-circle-check"></i></span></div>
</div>
<a :href="'#ampelCollapse_' + ampel.ampel_id" data-bs-toggle="collapse" class="stretched-link">{{ ampel.kurzbz }}</a><br>
</div>
<div class="collapse my-3" :id="'ampelCollapse_' + ampel.ampel_id" :ref="'ampelCollapse_' + ampel.ampel_id">
{{ ampel.beschreibung[0] }}
<div class="d-flex justify-content-end mt-3">
<button class="btn btn-sm btn-primary" :class="{disabled: ampel.bestaetigt}" @click="confirm(ampel.ampel_id)">{{ validateBtnTxt(ampel.buttontext[0]) }}</button>
</div>
</div>
</li>
</ul>
</div>
</template>
</BaseOffcanvas>`
}
const TEST_ALLE_AMPELN = [
{
ampel_id: 0,
kurzbz: 'Ampeltitel 1',
deadline: '2022-12-31',
verfallszeit: 10,
verpflichtend: false,
bestaetigt: false,
buttontext: ['Bestätigen', 'Confirm'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'1-Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'1-Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
},
{
ampel_id: 1,
kurzbz: 'Ampeltitel 2 kann auch etwas länger sein',
deadline: '2023-10-03',
verfallszeit: 20,
verpflichtend: true,
bestaetigt: false,
buttontext: ['Gelesen', 'Read'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'2-Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'2-Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
},
{
ampel_id: 2,
kurzbz: 'Ampeltitel 3',
deadline: '2022-10-31',
verfallszeit: null, // Dauerampel, Bis zur Bestätigung
verpflichtend: false,
bestaetigt: true,
buttontext: ['Bestätigen', 'Confirm'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'3-Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'3-Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
},
{
ampel_id: 3,
kurzbz: 'Ampeltitel 4',
deadline: '2022-10-31',
verfallszeit: 40,
verpflichtend: false,
bestaetigt: true,
buttontext: ['Bestätigen', 'Confirm'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
},
{
ampel_id: 4,
kurzbz: 'Ampeltitel 5',
deadline: '2022-10-31',
verfallszeit: 10,
verpflichtend: false,
bestaetigt: true,
buttontext: ['Bestätigen', 'Confirm'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
},
{
ampel_id: 5,
kurzbz: 'Ampeltitel 6',
deadline: '2022-10-31',
verfallszeit: 40,
verpflichtend: false,
bestaetigt: true,
buttontext: ['Bestätigen', 'Confirm'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
},
{
ampel_id: 6,
kurzbz: 'Ampeltitel 7',
deadline: '2020-12-31',
verfallszeit: 10,
verpflichtend: false,
bestaetigt: true,
buttontext: ['Bestätigen', 'Confirm'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
},{
ampel_id: 7,
kurzbz: 'Ampeltitel 8',
deadline: '2022-09-25',
verfallszeit: 40,
verpflichtend: false,
bestaetigt: false,
buttontext: ['Bestätigen', 'Confirm'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
},
{
ampel_id: 8,
kurzbz: 'Ampeltitel 9',
deadline: '2022-10-01',
verfallszeit: 10,
verpflichtend: false,
bestaetigt: false,
buttontext: ['Bestätigen', 'Confirm'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
}
]
const TEST_OFFENE_AMPELN = [
{
ampel_id: 0,
kurzbz: 'Ampeltitel 1',
deadline: '2022-12-31',
verfallszeit: 10,
verpflichtend: false,
bestaetigt: false,
buttontext: ['Bestätigen', 'Confirm'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'1-Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'1-Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
},
{
ampel_id: 1,
kurzbz: 'Ampeltitel 2 kann auch etwas länger sein',
deadline: '2023-10-03',
verfallszeit: 20,
verpflichtend: true,
bestaetigt: false,
buttontext: ['Gelesen', 'Read'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'2-Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'2-Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
},
{
ampel_id: 7,
kurzbz: 'Ampeltitel 8',
deadline: '2022-09-25',
verfallszeit: 40,
verpflichtend: false,
bestaetigt: false,
buttontext: ['Bestätigen', 'Confirm'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
},
{
ampel_id: 8,
kurzbz: 'Ampeltitel 9',
deadline: '2022-10-01',
verfallszeit: 10,
verpflichtend: false,
bestaetigt: false,
buttontext: ['Bestätigen', 'Confirm'],
insertamum: '2022-09-21 15:25:00',
beschreibung: [
'Deutscher Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.',
'Englischer Text Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod.'
]
}
];
@@ -0,0 +1,19 @@
import AbstractWidget from './Abstract';
export default {
mixins: [
AbstractWidget
],
computed: {
css() {
return ['dashboard-widget-default', this.config.css];
}
},
created() {
this.$emit('setConfig', false)
},
template: `<div :class="css">
<h5 class="card-title">{{ config.title }}</h5>
<p class="card-text">{{ config.msg }}</p>
</div>`
}
@@ -0,0 +1,115 @@
import AbstractWidget from './Abstract';
import BsModal from '../Bootstrap/Modal';
const MAX_LOADED_NEWS = 10;
export default {
name: 'WidgetsNews',
components: { BsModal },
data: () => ({
allNewsList: [],
singleNews: {}
}),
mixins: [
AbstractWidget
],
computed: {
newsList(){
//Return news amount depending on widget width and size
let quantity = this.width;
if (this.width === 1) {
quantity = this.height === 1 ? 4 : 10;
}
return this.allNewsList.slice(0, quantity);
}
},
created(){
axios
.get(this.apiurl + '/dashboard/Api/getNews', {params: {limit: MAX_LOADED_NEWS}})
.then(res => { this.allNewsList = res.data })
.catch(err => { console.error('ERROR: ', err.response.data) });
this.$emit('setConfig', false);
},
methods: {
setSingleNews(singleNews){
this.singleNews = singleNews;
this.$refs.newsModal.show();
}
},
template: `<div class="widgets-news w-100 h-100">
<div class="d-flex flex-column h-100">
<div class="d-flex">
<header><b>Top News</b></header>
<a href="#allNewsModal" data-bs-toggle="modal" class="ms-auto mb-2">
<i class="fa fa-arrow-up-right-from-square me-1"></i>Alle News</a>
</div>
<div v-if="width == 1">
<div v-for="news in newsList" :key="news.id" class="mt-2">
<div class="card">
<div class="card-body">
<a href="#newsModal" class="stretched-link" @click="setSingleNews(news)">{{ news.betreff }}</a><br>
<span class="small text-muted">{{ formatDateTime(news.insertamum) }}</span>
</div>
</div>
</div>
</div>
<div v-else-if="width > 1 && height === 1" class="h-100" :class="'row row-cols-' + width">
<div v-for="news in newsList" :key="news.id">
<div class="card h-100">
<div class="card-body">
<a href="#newsModal" class="card-title h5 stretched-link" @click="setSingleNews(news)">{{ news.betreff }}</a><br>
<span class="small text-muted">{{ formatDateTime(news.insertamum) }} </span>
<p class="card-text pt-3" style="overflow: hidden; display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical;">{{ news.text }}</p>
</div>
</div>
</div>
</div>
<div v-else class="h-100" :class="'row row-cols-' + width">
<div v-for="news in newsList" :key="news.id">
<div class="card h-100">
<img src="../../skin/images/fh_technikum_wien_illustration_klein.png" class="card-img-top">
<div class="card-footer"><span class="card-subtitle small text-muted">{{ formatDateTime(news.insertamum) }}</span></div>
<div class="card-body">
<a href="#newsModal" class="card-title h5 stretched-link" @click="setSingleNews(news)">{{ news.betreff }}</a><br>
<p class="card-text pt-3">{{ news.text }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- News Modal -->
<BsModal ref="newsModal" id="newsModal" dialog-class="modal-lg">
<template #title>
<div class="row">
<div class="col-5"><img src="../../skin/images/fh_technikum_wien_illustration_klein.png" class="img-fluid rounded-start"></div>
<div class="col-7 d-flex align-items-end">
<p>{{ singleNews.betreff }}<br><small class="text-muted">{{ formatDateTime(singleNews.insertamum) }}</small></p>
</div>
</div>
</template>
<template #default>{{ singleNews.text }}</template>
</BsModal>
<!-- All News Modal -->
<BsModal ref="allNewsModal" id="allNewsModal" dialog-class="modal-fullscreen">
<template #title>Alle News</template>
<template #default>
<div class="row row-cols-5 g-4 h-100 px-5">
<div v-for="news in allNewsList" :key="news.id">
<div class="card h-100">
<img src="../../skin/images/fh_technikum_wien_illustration_klein.png" class="card-img-top">
<div class="card-footer"><span class="card-subtitle small">{{ formatDateTime(news.insertamum) }}</span></div>
<div class="card-body">
<a href="" class="card-title h5 stretched-link" @click="setSingleNews1(news)">{{ news.betreff }}</a><br>
<p class="card-text">{{ news.text }}</p>
</div>
</div>
</div>
</div>
</template>
</BsModal>`
}
@@ -0,0 +1,84 @@
import AbstractWidget from './Abstract';
export default {
name: 'WidgetsUrl',
data: () => ({
links: []
}),
mixins: [
AbstractWidget
],
computed: {
tagName() {
return this.config.tag !== undefined && this.config.tag.length > 0 ? this.config.tag : 'Meine Urls';
}
},
methods: {
addLink(){
let linkId = this.links.length;
this.links.push({
id: linkId,
tag: this.config.tag,
title: this.title,
url: this.url
})
},
removeLink(linkId){
let indexToRemove = this.links.findIndex((obj => obj.id === linkId));
this.links.splice(indexToRemove, 1);
}
},
created() {
this.links = TEST_LINKS;
// this.links = TEST_KEINE_LINKS;
},
template: `
<div class="widgets-url w-100 h-100">
<div v-if="configMode">
<div class="mb-3">
<header><b>Neuer Link</b></header><br>
<div>
<input class="form-control form-control-sm" placeholder="Titel" type="text" v-model="title" name="title" maxlength="50" required>
<input class="form-control form-control-sm mt-2" type="url" placeholder="URL" v-model="url" name="url" required>
<button class="btn btn-outline-secondary btn-sm w-100 mt-2" @click="addLink()" type="button">Link speichern</button>
</div>
</div>
</div>
<div class="d-flex flex-column justify-content-between">
<header><b>{{ widgetTag }}</b></header>
<div v-for="link in links" :key="link.id" class="d-flex mt-2">
<a target="_blank" :href="link.url"><i class="fa fa-solid fa-arrow-up-right-from-square"></i> {{ link.title }}</a>
<a class="ms-auto" href="#" @click.prevent="removeLink(link.id)" v-show="configMode"><i class="fa fa-regular fa-trash-can"></i></a>
</div>
</div>
</div>`
}
const TEST_KEINE_LINKS = [];
const TEST_LINKS = [
{
id: 0,
tag: 'Zeitverwaltung',
title: 'Zeitverwaltung' + 'link 0',
url: 'https://www.technikum-wien.at'
},
{
id: 1,
tag: 'Zeitverwaltung',
title: 'Zeitverwaltung' + 'link 1',
url: 'https://www.technikum-wien.at'
},
{
id: 2,
tag: 'Zeitverwaltung',
title: 'Zeitverwaltung' + 'link 2',
url: 'https://www.technikum-wien.at'
},
{
id: 3,
tag: 'Zeitverwaltung',
title: 'Zeitverwaltung' + 'link 3',
url: 'https://www.technikum-wien.at'
}
];
@@ -0,0 +1,31 @@
let __widgets = {};
let __widgetsStarted = {};
let __path = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/dashboard/Widget';
export default {
getWidget(id) {
return __widgets[id];
},
loadWidget(id) {
if (__widgets[id])
return Promise.resolve(__widgets[id]);
if (__widgetsStarted[id])
return __widgetsStarted[id];
if (!__path)
return Promise.reject('Widget could not be loaded because there is no path yet!');
__widgetsStarted[id] = new Promise((resolve, reject) => {
axios.get(__path, {params:{id}}).then(res => {
res.data.retval.arguments = JSON.parse(res.data.retval.arguments);
res.data.retval.setup = JSON.parse(res.data.retval.setup);
__widgets[id] = res.data.retval;
__widgetsStarted[id] = undefined;
resolve(__widgets[id]);
}).catch(error => reject(error.response.data.retval.error));
});
return __widgetsStarted[id];
},
setPath(path) {
__path = path;
}
}
+31
View File
@@ -0,0 +1,31 @@
export default {
/**
* Performs a deep merge of objects and returns new object. Does not modify
* objects (immutable) and merges arrays via concatenation.
*
* @param {...object} objects - Objects to merge
* @returns {object} New object with merged key/values
*/
mergeDeep(...objects) {
const isObject = obj => obj && typeof obj === 'object';
return objects.reduce((prev, obj) => {
Object.keys(obj).forEach(key => {
const pVal = prev[key];
const oVal = obj[key];
if (Array.isArray(pVal) && Array.isArray(oVal)) {
prev[key] = pVal.concat(...oVal);
}
else if (isObject(pVal) && isObject(oVal)) {
prev[key] = this.mergeDeep(pVal, oVal);
}
else {
prev[key] = oVal;
}
});
return prev;
}, {});
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+7 -1
View File
@@ -28,7 +28,8 @@ require_once('dbupdate_3.4/example2.php');
...
*/
require_once('dbupdate_3.4/26173_index_webservicelog.php');
// add Dashboard Schema, Tables and Permissions
include __DIR__ . '/dbupdate_3.4/dbupdate_dashboard.php';
// *** Pruefung und hinzufuegen der neuen Attribute und Tabellen
echo '<H2>Pruefe Tabellen und Attribute!</H2>';
@@ -124,6 +125,11 @@ $tabellen=array(
"campus.tbl_zeitsperretyp" => array("zeitsperretyp_kurzbz","beschreibung","farbe"),
"campus.tbl_zeitwunsch" => array("stunde","mitarbeiter_uid","tag","gewicht","updateamum","updatevon","insertamum","insertvon", "zeitwunsch_id", "zeitwunsch_gueltigkeit_id"),
"campus.tbl_zeitwunsch_gueltigkeit" => array("zeitwunsch_gueltigkeit_id","mitarbeiter_uid","von","bis","insertamum","insertvon", "updateamum","updatevon"),
"dashboard.tbl_dashboard" => array("dashboard_id", "dashboard_kurzbz", "beschreibung"),
"dashboard.tbl_dashboard_benutzer_override" => array("override_id", "dashboard_id", "uid", "override"),
"dashboard.tbl_dashboard_preset" => array("preset_id", "dashboard_id", "funktion_kurzbz", "preset"),
"dashboard.tbl_dashboard_widget" => array("dashboard_id", "widget_id"),
"dashboard.tbl_widget" => array("widget_id", "widget_kurzbz", "beschreibung"),
"fue.tbl_aktivitaet" => array("aktivitaet_kurzbz","beschreibung","sort"),
"fue.tbl_aufwandstyp" => array("aufwandstyp_kurzbz","bezeichnung"),
"fue.tbl_projekt" => array("projekt_kurzbz","nummer","titel","beschreibung","beginn","ende","oe_kurzbz","budget","farbe","aufwandstyp_kurzbz","ressource_id","anzahl_ma","aufwand_pt","projekt_id","projekttyp_kurzbz","zeitaufzeichnung"),
+465
View File
@@ -0,0 +1,465 @@
<?php
/* Copyright (C) 2017 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: Harald Bamberger <harald.bamberger@technikum-wien.at>,
*
* Beschreibung:
* Dashboard DB Aenderungen
*/
if (! defined('DB_NAME')) exit('No direct script access allowed');
if (($result = $db->db_query("SELECT schema_name FROM information_schema.schemata WHERE schema_name='dashboard'")))
{
if ($db->db_num_rows($result) == 0)
{
$qry = <<<EODASHBOARDSQL
--
-- Name: dashboard; Type: SCHEMA; Schema: -; Owner: fhcomplete
--
CREATE SCHEMA dashboard;
ALTER SCHEMA dashboard OWNER TO fhcomplete;
SET default_tablespace = '';
SET default_with_oids = false;
--
-- Name: tbl_dashboard; Type: TABLE; Schema: dashboard; Owner: fhcomplete
--
CREATE TABLE dashboard.tbl_dashboard (
dashboard_id integer NOT NULL,
dashboard_kurzbz character varying(32) NOT NULL,
beschreibung text
);
ALTER TABLE dashboard.tbl_dashboard OWNER TO fhcomplete;
--
-- Name: tbl_dashboard_benutzer_override; Type: TABLE; Schema: dashboard; Owner: fhcomplete
--
CREATE TABLE dashboard.tbl_dashboard_benutzer_override (
override_id integer NOT NULL,
dashboard_id integer NOT NULL,
uid character varying(32) NOT NULL,
override jsonb NOT NULL
);
ALTER TABLE dashboard.tbl_dashboard_benutzer_override OWNER TO fhcomplete;
--
-- Name: tbl_dashboard_benutzer_override_override_id_seq; Type: SEQUENCE; Schema: dashboard; Owner: fhcomplete
--
CREATE SEQUENCE dashboard.tbl_dashboard_benutzer_override_override_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE dashboard.tbl_dashboard_benutzer_override_override_id_seq OWNER TO fhcomplete;
--
-- Name: tbl_dashboard_benutzer_override_override_id_seq; Type: SEQUENCE OWNED BY; Schema: dashboard; Owner: fhcomplete
--
ALTER SEQUENCE dashboard.tbl_dashboard_benutzer_override_override_id_seq OWNED BY dashboard.tbl_dashboard_benutzer_override.override_id;
--
-- Name: tbl_dashboard_dashboard_id_seq; Type: SEQUENCE; Schema: dashboard; Owner: fhcomplete
--
CREATE SEQUENCE dashboard.tbl_dashboard_dashboard_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE dashboard.tbl_dashboard_dashboard_id_seq OWNER TO fhcomplete;
--
-- Name: tbl_dashboard_dashboard_id_seq; Type: SEQUENCE OWNED BY; Schema: dashboard; Owner: fhcomplete
--
ALTER SEQUENCE dashboard.tbl_dashboard_dashboard_id_seq OWNED BY dashboard.tbl_dashboard.dashboard_id;
--
-- Name: tbl_dashboard_preset; Type: TABLE; Schema: dashboard; Owner: fhcomplete
--
CREATE TABLE dashboard.tbl_dashboard_preset (
preset_id integer NOT NULL,
dashboard_id integer NOT NULL,
funktion_kurzbz character varying(16),
preset jsonb NOT NULL
);
ALTER TABLE dashboard.tbl_dashboard_preset OWNER TO fhcomplete;
--
-- Name: tbl_dashboard_preset_preset_id_seq; Type: SEQUENCE; Schema: dashboard; Owner: fhcomplete
--
CREATE SEQUENCE dashboard.tbl_dashboard_preset_preset_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE dashboard.tbl_dashboard_preset_preset_id_seq OWNER TO fhcomplete;
--
-- Name: tbl_dashboard_preset_preset_id_seq; Type: SEQUENCE OWNED BY; Schema: dashboard; Owner: fhcomplete
--
ALTER SEQUENCE dashboard.tbl_dashboard_preset_preset_id_seq OWNED BY dashboard.tbl_dashboard_preset.preset_id;
--
-- Name: tbl_dashboard_widget; Type: TABLE; Schema: dashboard; Owner: fhcomplete
--
CREATE TABLE dashboard.tbl_dashboard_widget (
dashboard_id integer NOT NULL,
widget_id integer NOT NULL
);
ALTER TABLE dashboard.tbl_dashboard_widget OWNER TO fhcomplete;
--
-- Name: tbl_widget; Type: TABLE; Schema: dashboard; Owner: fhcomplete
--
CREATE TABLE dashboard.tbl_widget (
widget_id integer NOT NULL,
widget_kurzbz character varying(32) NOT NULL,
beschreibung text,
arguments jsonb NOT NULL,
setup jsonb NOT NULL
);
ALTER TABLE dashboard.tbl_widget OWNER TO fhcomplete;
--
-- Name: tbl_widget_widget_id_seq; Type: SEQUENCE; Schema: dashboard; Owner: fhcomplete
--
CREATE SEQUENCE dashboard.tbl_widget_widget_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE dashboard.tbl_widget_widget_id_seq OWNER TO fhcomplete;
--
-- Name: tbl_widget_widget_id_seq; Type: SEQUENCE OWNED BY; Schema: dashboard; Owner: fhcomplete
--
ALTER SEQUENCE dashboard.tbl_widget_widget_id_seq OWNED BY dashboard.tbl_widget.widget_id;
--
-- Name: tbl_dashboard dashboard_id; Type: DEFAULT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard ALTER COLUMN dashboard_id SET DEFAULT nextval('dashboard.tbl_dashboard_dashboard_id_seq'::regclass);
--
-- Name: tbl_dashboard_benutzer_override override_id; Type: DEFAULT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_benutzer_override ALTER COLUMN override_id SET DEFAULT nextval('dashboard.tbl_dashboard_benutzer_override_override_id_seq'::regclass);
--
-- Name: tbl_dashboard_preset preset_id; Type: DEFAULT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_preset ALTER COLUMN preset_id SET DEFAULT nextval('dashboard.tbl_dashboard_preset_preset_id_seq'::regclass);
--
-- Name: tbl_widget widget_id; Type: DEFAULT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_widget ALTER COLUMN widget_id SET DEFAULT nextval('dashboard.tbl_widget_widget_id_seq'::regclass);
--
-- Name: tbl_dashboard_benutzer_override tbl_dashboard_benutzer_override_pkey; Type: CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_benutzer_override
ADD CONSTRAINT tbl_dashboard_benutzer_override_pkey PRIMARY KEY (override_id);
--
-- Name: tbl_dashboard_benutzer_override tbl_dashboard_benutzer_override_uk1; Type: CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_benutzer_override
ADD CONSTRAINT tbl_dashboard_benutzer_override_uk1 UNIQUE (dashboard_id, uid);
--
-- Name: tbl_dashboard tbl_dashboard_kurz_bz_key; Type: CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard
ADD CONSTRAINT tbl_dashboard_kurz_bz_key UNIQUE (dashboard_kurzbz);
--
-- Name: tbl_dashboard tbl_dashboard_pkey; Type: CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard
ADD CONSTRAINT tbl_dashboard_pkey PRIMARY KEY (dashboard_id);
--
-- Name: tbl_dashboard_preset tbl_dashboard_preset_pkey; Type: CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_preset
ADD CONSTRAINT tbl_dashboard_preset_pkey PRIMARY KEY (preset_id);
--
-- Name: tbl_dashboard_preset tbl_dashboard_preset_uk1; Type: CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_preset
ADD CONSTRAINT tbl_dashboard_preset_uk1 UNIQUE (dashboard_id, funktion_kurzbz);
--
-- Name: tbl_dashboard_widget tbl_dashboard_widget_pkey; Type: CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_widget
ADD CONSTRAINT tbl_dashboard_widget_pkey PRIMARY KEY (dashboard_id, widget_id);
--
-- Name: tbl_widget tbl_widget_pkey; Type: CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_widget
ADD CONSTRAINT tbl_widget_pkey PRIMARY KEY (widget_id);
--
-- Name: tbl_widget tbl_widget_widget_kurzbz_key; Type: CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_widget
ADD CONSTRAINT tbl_widget_widget_kurzbz_key UNIQUE (widget_kurzbz);
--
-- Name: tbl_dashboard_benutzer_override tbl_dashboard_benutzer_override_fk1; Type: FK CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_benutzer_override
ADD CONSTRAINT tbl_dashboard_benutzer_override_fk1 FOREIGN KEY (dashboard_id) REFERENCES dashboard.tbl_dashboard(dashboard_id) ON UPDATE CASCADE ON DELETE RESTRICT;
--
-- Name: tbl_dashboard_benutzer_override tbl_dashboard_benutzer_override_fk2; Type: FK CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_benutzer_override
ADD CONSTRAINT tbl_dashboard_benutzer_override_fk2 FOREIGN KEY (uid) REFERENCES public.tbl_benutzer(uid) ON UPDATE CASCADE ON DELETE RESTRICT;
--
-- Name: tbl_dashboard_preset tbl_dashboard_preset_fk1; Type: FK CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_preset
ADD CONSTRAINT tbl_dashboard_preset_fk1 FOREIGN KEY (dashboard_id) REFERENCES dashboard.tbl_dashboard(dashboard_id) ON UPDATE CASCADE ON DELETE RESTRICT;
--
-- Name: tbl_dashboard_preset tbl_dashboard_preset_fk2; Type: FK CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_preset
ADD CONSTRAINT tbl_dashboard_preset_fk2 FOREIGN KEY (funktion_kurzbz) REFERENCES public.tbl_funktion(funktion_kurzbz) ON UPDATE CASCADE ON DELETE RESTRICT;
--
-- Name: tbl_dashboard_widget tbl_dashboard_widget_fk1; Type: FK CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_widget
ADD CONSTRAINT tbl_dashboard_widget_fk1 FOREIGN KEY (dashboard_id) REFERENCES dashboard.tbl_dashboard(dashboard_id) ON UPDATE CASCADE ON DELETE RESTRICT;
--
-- Name: tbl_dashboard_widget tbl_dashboard_widget_fk2; Type: FK CONSTRAINT; Schema: dashboard; Owner: fhcomplete
--
ALTER TABLE ONLY dashboard.tbl_dashboard_widget
ADD CONSTRAINT tbl_dashboard_widget_fk2 FOREIGN KEY (widget_id) REFERENCES dashboard.tbl_widget(widget_id) ON UPDATE CASCADE ON DELETE RESTRICT;
--
-- Name: SCHEMA dashboard; Type: ACL; Schema: -; Owner: fhcomplete
--
GRANT USAGE ON SCHEMA dashboard TO web;
GRANT USAGE ON SCHEMA dashboard TO vilesci;
--
-- Name: TABLE tbl_dashboard; Type: ACL; Schema: dashboard; Owner: fhcomplete
--
GRANT SELECT ON TABLE dashboard.tbl_dashboard TO web;
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE dashboard.tbl_dashboard TO vilesci;
--
-- Name: TABLE tbl_dashboard_benutzer_override; Type: ACL; Schema: dashboard; Owner: fhcomplete
--
GRANT SELECT,INSERT,UPDATE ON TABLE dashboard.tbl_dashboard_benutzer_override TO web;
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE dashboard.tbl_dashboard_benutzer_override TO vilesci;
--
-- Name: SEQUENCE tbl_dashboard_benutzer_override_override_id_seq; Type: ACL; Schema: dashboard; Owner: fhcomplete
--
GRANT ALL ON SEQUENCE dashboard.tbl_dashboard_benutzer_override_override_id_seq TO vilesci;
--
-- Name: SEQUENCE tbl_dashboard_dashboard_id_seq; Type: ACL; Schema: dashboard; Owner: fhcomplete
--
GRANT ALL ON SEQUENCE dashboard.tbl_dashboard_dashboard_id_seq TO vilesci;
--
-- Name: TABLE tbl_dashboard_preset; Type: ACL; Schema: dashboard; Owner: fhcomplete
--
GRANT SELECT ON TABLE dashboard.tbl_dashboard_preset TO web;
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE dashboard.tbl_dashboard_preset TO vilesci;
--
-- Name: SEQUENCE tbl_dashboard_preset_preset_id_seq; Type: ACL; Schema: dashboard; Owner: fhcomplete
--
GRANT ALL ON SEQUENCE dashboard.tbl_dashboard_preset_preset_id_seq TO vilesci;
--
-- Name: TABLE tbl_dashboard_widget; Type: ACL; Schema: dashboard; Owner: fhcomplete
--
GRANT SELECT ON TABLE dashboard.tbl_dashboard_widget TO web;
--
-- Name: TABLE tbl_widget; Type: ACL; Schema: dashboard; Owner: fhcomplete
--
GRANT SELECT ON TABLE dashboard.tbl_widget TO web;
--
-- Name: SEQUENCE tbl_widget_widget_id_seq; Type: ACL; Schema: dashboard; Owner: fhcomplete
--
GRANT ALL ON SEQUENCE dashboard.tbl_widget_widget_id_seq TO vilesci;
EODASHBOARDSQL;
if (!$db->db_query($qry))
{
echo '<strong>Schema Dashboard: '.$db->db_last_error().'</strong><br>';
}
else
{
echo '<br>Neues Schema dashboard hinzugefuegt';
}
}
}
// Add permission: dashboard/admin
if($result = @$db->db_query("SELECT 1 FROM system.tbl_berechtigung WHERE berechtigung_kurzbz = 'dashboard/admin';"))
{
if($db->db_num_rows($result) == 0)
{
$qry = "INSERT INTO system.tbl_berechtigung(berechtigung_kurzbz, beschreibung) VALUES('dashboard/admin', 'Adminberechtigung');";
if(!$db->db_query($qry))
{
echo '<strong>system.tbl_berechtigung '.$db->db_last_error().'</strong><br>';
}
else
{
echo 'system.tbl_berechtigung: Added permission for dashboard/admin<br>';
}
}
}
// Add permission: dashboard/benutzer
if($result = @$db->db_query("SELECT 1 FROM system.tbl_berechtigung WHERE berechtigung_kurzbz = 'dashboard/benutzer';"))
{
if($db->db_num_rows($result) == 0)
{
$qry = "INSERT INTO system.tbl_berechtigung(berechtigung_kurzbz, beschreibung) VALUES('dashboard/benutzer', 'Benutzerberechtigung');";
if(!$db->db_query($qry))
{
echo '<strong>system.tbl_berechtigung '.$db->db_last_error().'</strong><br>';
}
else
{
echo 'system.tbl_berechtigung: Added permission for dashboard/benutzer<br>';
}
}
}