diff --git a/application/controllers/dashboard/Api.php b/application/controllers/dashboard/Api.php new file mode 100644 index 000000000..c6ac21463 --- /dev/null +++ b/application/controllers/dashboard/Api.php @@ -0,0 +1,78 @@ + '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'); + } +} diff --git a/application/controllers/dashboard/Config.php b/application/controllers/dashboard/Config.php new file mode 100644 index 000000000..213781b74 --- /dev/null +++ b/application/controllers/dashboard/Config.php @@ -0,0 +1,218 @@ + '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); + } + +} diff --git a/application/controllers/dashboard/Dashboard.php b/application/controllers/dashboard/Dashboard.php new file mode 100644 index 000000000..8cf37d579 --- /dev/null +++ b/application/controllers/dashboard/Dashboard.php @@ -0,0 +1,87 @@ + '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) ?: []); + } + +} diff --git a/application/controllers/dashboard/DashboardDemo.php b/application/controllers/dashboard/DashboardDemo.php new file mode 100644 index 000000000..7088c44b9 --- /dev/null +++ b/application/controllers/dashboard/DashboardDemo.php @@ -0,0 +1,51 @@ + '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'); + } + +} diff --git a/application/controllers/dashboard/Widget.php b/application/controllers/dashboard/Widget.php new file mode 100644 index 000000000..9a878fa36 --- /dev/null +++ b/application/controllers/dashboard/Widget.php @@ -0,0 +1,108 @@ + '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)); + } +} diff --git a/application/libraries/dashboard/DashboardLib.php b/application/libraries/dashboard/DashboardLib.php new file mode 100644 index 000000000..3f189fc03 --- /dev/null +++ b/application/libraries/dashboard/DashboardLib.php @@ -0,0 +1,228 @@ +_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; + } + } +} diff --git a/application/models/content/News_model.php b/application/models/content/News_model.php index 8d636d808..472eb2e56 100644 --- a/application/models/content/News_model.php +++ b/application/models/content/News_model.php @@ -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) + ); + } } diff --git a/application/models/dashboard/Dashboard_Override_model.php b/application/models/dashboard/Dashboard_Override_model.php new file mode 100644 index 000000000..d7a12bb42 --- /dev/null +++ b/application/models/dashboard/Dashboard_Override_model.php @@ -0,0 +1,26 @@ +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)); + } +} diff --git a/application/models/dashboard/Dashboard_Preset_model.php b/application/models/dashboard/Dashboard_Preset_model.php new file mode 100644 index 000000000..c899195d4 --- /dev/null +++ b/application/models/dashboard/Dashboard_Preset_model.php @@ -0,0 +1,67 @@ +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 = <<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)); + } +} diff --git a/application/models/dashboard/Dashboard_Widget_model.php b/application/models/dashboard/Dashboard_Widget_model.php new file mode 100644 index 000000000..eb4540cd9 --- /dev/null +++ b/application/models/dashboard/Dashboard_Widget_model.php @@ -0,0 +1,16 @@ +dbTable = 'dashboard.tbl_dashboard_widget'; + $this->pk = ['dashboard_id', 'widget_id']; + $this->hasSequence = false; + } + +} diff --git a/application/models/dashboard/Dashboard_model.php b/application/models/dashboard/Dashboard_model.php new file mode 100644 index 000000000..88946ed83 --- /dev/null +++ b/application/models/dashboard/Dashboard_model.php @@ -0,0 +1,25 @@ +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)); + } +} diff --git a/application/models/dashboard/Widget_model.php b/application/models/dashboard/Widget_model.php new file mode 100644 index 000000000..4e5a31502 --- /dev/null +++ b/application/models/dashboard/Widget_model.php @@ -0,0 +1,33 @@ +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]); + } + +} diff --git a/application/views/dashboard/dashboard_demo.php b/application/views/dashboard/dashboard_demo.php new file mode 100644 index 000000000..3518d769a --- /dev/null +++ b/application/views/dashboard/dashboard_demo.php @@ -0,0 +1,31 @@ +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 + ) +); +?> + +
+ + + +
+
+

Dashboard

+
+ +
+
+ +load->view('templates/FHC-Footer'); ?> diff --git a/public/css/components/dashboard.css b/public/css/components/dashboard.css new file mode 100644 index 000000000..25d33adcd --- /dev/null +++ b/public/css/components/dashboard.css @@ -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; +} \ No newline at end of file diff --git a/public/js/apps/Dashboard.js b/public/js/apps/Dashboard.js new file mode 100644 index 000000000..54a73441c --- /dev/null +++ b/public/js/apps/Dashboard.js @@ -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'); diff --git a/public/js/components/Base/Modal.js b/public/js/components/Base/Modal.js new file mode 100644 index 000000000..ad3785e62 --- /dev/null +++ b/public/js/components/Base/Modal.js @@ -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: ` + ` +} diff --git a/public/js/components/Base/Offcanvas.js b/public/js/components/Base/Offcanvas.js new file mode 100644 index 000000000..4db2592a6 --- /dev/null +++ b/public/js/components/Base/Offcanvas.js @@ -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: ` +
+
+
+ +
+ +
+
+
+ +
+
+
` +} diff --git a/public/js/components/Bootstrap/Alert.js b/public/js/components/Bootstrap/Alert.js new file mode 100644 index 000000000..b261eeaef --- /dev/null +++ b/public/js/components/Bootstrap/Alert.js @@ -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: ` + + + ` +} diff --git a/public/js/components/Bootstrap/Confirm.js b/public/js/components/Bootstrap/Confirm.js new file mode 100644 index 000000000..1c609d457 --- /dev/null +++ b/public/js/components/Bootstrap/Confirm.js @@ -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: ` + + + ` +} diff --git a/public/js/components/Bootstrap/Modal.js b/public/js/components/Bootstrap/Modal.js new file mode 100644 index 000000000..a74e6daa0 --- /dev/null +++ b/public/js/components/Bootstrap/Modal.js @@ -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: `` +} diff --git a/public/js/components/Bootstrap/Prompt.js b/public/js/components/Bootstrap/Prompt.js new file mode 100644 index 000000000..c056d4237 --- /dev/null +++ b/public/js/components/Bootstrap/Prompt.js @@ -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: ` + + + ` +} diff --git a/public/js/components/Dashboard/Admin.js b/public/js/components/Dashboard/Admin.js new file mode 100644 index 000000000..775cd87d6 --- /dev/null +++ b/public/js/components/Dashboard/Admin.js @@ -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: `
+
+ + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
` +} diff --git a/public/js/components/Dashboard/Admin/Edit.js b/public/js/components/Dashboard/Admin/Edit.js new file mode 100644 index 000000000..ec841a9ad --- /dev/null +++ b/public/js/components/Dashboard/Admin/Edit.js @@ -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: `
+
+ + +
+
+ + +
+
+ + +
+
` +} diff --git a/public/js/components/Dashboard/Admin/Presets.js b/public/js/components/Dashboard/Admin/Presets.js new file mode 100644 index 000000000..44ff2f854 --- /dev/null +++ b/public/js/components/Dashboard/Admin/Presets.js @@ -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: `
+
+
+ +
+
+ +
+
+ +
` +} diff --git a/public/js/components/Dashboard/Admin/Widgets.js b/public/js/components/Dashboard/Admin/Widgets.js new file mode 100644 index 000000000..34b7b482c --- /dev/null +++ b/public/js/components/Dashboard/Admin/Widgets.js @@ -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: `
+
+ + +
+
` +} diff --git a/public/js/components/Dashboard/Dashboard.js b/public/js/components/Dashboard/Dashboard.js new file mode 100644 index 000000000..2f2aa4d79 --- /dev/null +++ b/public/js/components/Dashboard/Dashboard.js @@ -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: `
+ + +
` +} \ No newline at end of file diff --git a/public/js/components/Dashboard/Item.js b/public/js/components/Dashboard/Item.js new file mode 100644 index 000000000..7d7639362 --- /dev/null +++ b/public/js/components/Dashboard/Item.js @@ -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: `
+
+ +
+
+
+
+ + {{ widget.setup.name }} + + + + +
+ +
+
+
+ +
+
+ + + + + + +
` +} \ No newline at end of file diff --git a/public/js/components/Dashboard/Section.js b/public/js/components/Dashboard/Section.js new file mode 100644 index 000000000..250a26a7b --- /dev/null +++ b/public/js/components/Dashboard/Section.js @@ -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: `
+

+ {{name}} + +

+
+
+ + + + +
+
+
` +} \ No newline at end of file diff --git a/public/js/components/Dashboard/Widget/Picker.js b/public/js/components/Dashboard/Widget/Picker.js new file mode 100644 index 000000000..c96e3bc7b --- /dev/null +++ b/public/js/components/Dashboard/Widget/Picker.js @@ -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: `
+ + + + +
` +} diff --git a/public/js/components/DashboardWidget/Abstract.js b/public/js/components/DashboardWidget/Abstract.js new file mode 100644 index 000000000..a21c9834f --- /dev/null +++ b/public/js/components/DashboardWidget/Abstract.js @@ -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(); + } + } +} diff --git a/public/js/components/DashboardWidget/Ampel.js b/public/js/components/DashboardWidget/Ampel.js new file mode 100644 index 000000000..9f0918aa7 --- /dev/null +++ b/public/js/components/DashboardWidget/Ampel.js @@ -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: ` +
+
+
+
Neueste Ampeln
+ +
+ +
+
+
+
+
+
Deadline: {{ getDate(ampel.deadline) }}
+
+
+
+
+
+ {{ ampel.kurzbz }}
+
+
+
+
+ + Super!
+ Keine offenen Ampeln. +
+
+
+ + + + + + ` +} + +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.' + ] + } +]; \ No newline at end of file diff --git a/public/js/components/DashboardWidget/Default.js b/public/js/components/DashboardWidget/Default.js new file mode 100644 index 000000000..859528ca5 --- /dev/null +++ b/public/js/components/DashboardWidget/Default.js @@ -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: `
+
{{ config.title }}
+

{{ config.msg }}

+
` +} \ No newline at end of file diff --git a/public/js/components/DashboardWidget/News.js b/public/js/components/DashboardWidget/News.js new file mode 100644 index 000000000..6b5fc73d1 --- /dev/null +++ b/public/js/components/DashboardWidget/News.js @@ -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: `
+
+
+
Top News
+ + Alle News +
+
+
+
+
+ {{ news.betreff }}
+ {{ formatDateTime(news.insertamum) }} +
+
+
+
+
+
+
+
+ {{ news.betreff }}
+ {{ formatDateTime(news.insertamum) }} +

{{ news.text }}

+
+
+
+
+
+
+
+ + +
+ {{ news.betreff }}
+

{{ news.text }}

+
+
+
+
+
+
+ + + + + + + + + + + + ` +} \ No newline at end of file diff --git a/public/js/components/DashboardWidget/Url.js b/public/js/components/DashboardWidget/Url.js new file mode 100644 index 000000000..f374b7ed8 --- /dev/null +++ b/public/js/components/DashboardWidget/Url.js @@ -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: ` +
+
+
+ +
Neuer Link

+
+ + + +
+
+
+
+
{{ widgetTag }}
+ +
+
` +} +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' + } +]; \ No newline at end of file diff --git a/public/js/composables/Dashboard/CachedWidgetLoader.js b/public/js/composables/Dashboard/CachedWidgetLoader.js new file mode 100644 index 000000000..3ebe6ddcb --- /dev/null +++ b/public/js/composables/Dashboard/CachedWidgetLoader.js @@ -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; + } +} \ No newline at end of file diff --git a/public/js/composables/ObjectUtils.js b/public/js/composables/ObjectUtils.js new file mode 100644 index 000000000..348d18843 --- /dev/null +++ b/public/js/composables/ObjectUtils.js @@ -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; + }, {}); + } +} \ No newline at end of file diff --git a/skin/images/fh_technikum_wien_illustration_klein.png b/skin/images/fh_technikum_wien_illustration_klein.png new file mode 100644 index 000000000..cd9c7d18f Binary files /dev/null and b/skin/images/fh_technikum_wien_illustration_klein.png differ diff --git a/system/dbupdate_3.4.php b/system/dbupdate_3.4.php index 9d89de0d1..28366d073 100644 --- a/system/dbupdate_3.4.php +++ b/system/dbupdate_3.4.php @@ -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 '

Pruefe Tabellen und Attribute!

'; @@ -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"), diff --git a/system/dbupdate_3.4/dbupdate_dashboard.php b/system/dbupdate_3.4/dbupdate_dashboard.php new file mode 100644 index 000000000..22c2ef4cf --- /dev/null +++ b/system/dbupdate_3.4/dbupdate_dashboard.php @@ -0,0 +1,465 @@ +, + * + * 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 = <<db_query($qry)) + { + echo 'Schema Dashboard: '.$db->db_last_error().'
'; + } + else + { + echo '
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 'system.tbl_berechtigung '.$db->db_last_error().'
'; + } + else + { + echo 'system.tbl_berechtigung: Added permission for dashboard/admin
'; + } + } +} + +// 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 'system.tbl_berechtigung '.$db->db_last_error().'
'; + } + else + { + echo 'system.tbl_berechtigung: Added permission for dashboard/benutzer
'; + } + } +} +