Widget admistration (first draft)

This commit is contained in:
chfhtw
2026-08-25 12:39:33 +02:00
parent e2a2d28f90
commit 74bcfe3c8d
13 changed files with 761 additions and 12 deletions
+14 -8
View File
@@ -170,13 +170,6 @@ $config['navigation_header'] = array(
'expand' => true,
'sort' => 51,
'requiredPermissions' => 'vertrag/mitarbeiter:r'
),
'studierendenverwaltung' => array(
'link' => site_url('studentenverwaltung'),
'description' => 'Studierendenverwaltung',
'expand' => true,
'sort' => 52,
'requiredPermissions' => ['admin:r', 'assistenz:r']
)
)
),
@@ -385,8 +378,21 @@ $config['navigation_menu']['apps'] = [
],
'lav' => [
'link' => site_url('lehre/lehrauftrag/Lehrauftrag/Dashboard'),
'description' => 'Lehrauftragsverwaltung',
'description' => 'Lehraufträge',
#'icon' => 'person-chalkboard',
'requiredPermissions' => array('lehre/lehrauftrag_bestellen:r', 'lehre/lehrauftrag_erteilen:r')
]
];
$config['navigation_menu']['dashboard/*'] = [
'dashboards' => [
'link' => site_url('dashboard/Admin'),
'description' => 'Dashboard',
'requiredPermissions' => 'dashboard/admin:rw'
],
'widgets' => [
'link' => site_url('dashboard/Admin/widgets'),
'description' => 'Widgets',
'requiredPermissions' => 'dashboard/admin:rw'
]
];
@@ -18,6 +18,8 @@
if (! defined('BASEPATH')) exit('No direct script access allowed');
use \DirectoryIterator as DirectoryIterator;
/**
* This controller operates between (interface) the JS (GUI) and the back-end
* Provides data to the ajax get calls about the users dashboard
@@ -31,7 +33,12 @@ class Widget extends FHCAPI_Controller
'get' => ['dashboard/benutzer:r', 'dashboard/admin:r'],
'list' => 'dashboard/admin:r',
'listAllowed' => ['dashboard/benutzer:rw', 'dashboard/admin:r'],
'setAllowed' => 'dashboard/admin:rw'
'listAll' => 'dashboard/admin:r',
'listAllOriginal' => 'dashboard/admin:rw',
'setAllowed' => 'dashboard/admin:rw',
'create' => 'dashboard/admin:rw',
'update' => 'dashboard/admin:rw',
'generators' => 'dashboard/admin:rw',
]);
// Libraries
@@ -107,6 +114,38 @@ class Widget extends FHCAPI_Controller
$this->terminateWithSuccess($widgets);
}
public function listAll()
{
$result = $this->WidgetModel->load();
$widgets = $this->getDataOrTerminateWithError($result);
$widgets = array_map(function ($widget) {
$widget->arguments = json_decode($widget->arguments);
$tmpsetup = json_decode($widget->setup);
$tmpsetup->file = absoluteJsImportUrl($tmpsetup->file);
$widget->setup = $tmpsetup;
return $widget;
}, $widgets);
$this->terminateWithSuccess($widgets);
}
public function listAllOriginal()
{
$result = $this->WidgetModel->load();
$widgets = $this->getDataOrTerminateWithError($result);
$widgets = array_map(function ($widget) {
$widget->arguments = json_decode($widget->arguments);
$widget->setup = json_decode($widget->setup);
return $widget;
}, $widgets);
$this->terminateWithSuccess($widgets);
}
public function setAllowed()
{
$this->load->library('form_validation');
@@ -134,4 +173,149 @@ class Widget extends FHCAPI_Controller
$this->terminateWithSuccess($data);
}
public function create()
{
$this->loadPhrases(['mobility', 'dashboard']);
$this->load->library('form_validation');
$this->form_validation->set_rules(
'widget_kurzbz',
$this->p->t('mobility', 'kurzbz'),
'required|is_not_in_db[dashboard/Widget_model:widget_kurzbz]',
[
'is_not_in_db' => 'TODO(chris): error phrase'
]
);
$this->form_validation->set_rules(
'setup[generator]',
$this->p->t('dashboard', 'generator'),
'required'
);
if (!$this->form_validation->run())
$this->terminateWithValidationErrors($this->form_validation->error_array());
$setup = $this->input->post('setup');
$file = explode(':', $setup['generator']);
if (count($file) == 1) {
$setup['file'] = 'public/js/components/DashboardWidget/' . $file[0] . '.js';
} else {
$setup['file'] = 'public/extensions/' . $file[0] . '/js/components/DashboardWidget/' . $file[1] . '.js';
}
$data = [
'widget_kurzbz' => $this->input->post('widget_kurzbz'),
'setup' => json_encode($setup),
'arguments' => json_encode([]),
];
$result = $this->WidgetModel->insert($data);
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($data);
}
public function update()
{
$this->loadPhrases(['zeitaufzeichnung']);
$this->load->library('form_validation');
$this->form_validation->set_rules(
'widget_id',
$this->p->t('zeitaufzeichnung', 'id'),
'required'
);
if (!$this->form_validation->run())
$this->terminateWithValidationErrors($this->form_validation->error_array());
$widget_id = $this->input->post('widget_id');
$data = [];
foreach ([
'widget_kurzbz',
'beschreibung',
'setup',
'arguments',
'berechtigung_kurzbz',
] as $post) {
$value = $this->input->post($post);
if ($value) {
if ($post == 'setup' || $post == 'arguments')
$value = json_encode($value);
$data[$post] = $value;
}
}
if (!$data)
$this->terminateWithSuccess();
$result = $this->WidgetModel->update($widget_id, $data);
$this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess();
}
public function generators()
{
$result = $this->getGeneratorsRecursive();
$this->load->library('ExtensionsLib');
$extensions = $this->extensionslib->getInstalledExtensions();
if (hasData($extensions)) {
foreach (getData($extensions) as $extension) {
$result = array_merge($result, $this->getGeneratorsRecursive($extension->name));
}
}
return $this->terminateWithSuccess($result);
}
protected function getGeneratorsRecursive($extension = '', $path = '')
{
$result = [];
$dashboard_widget_path = 'public/js/components/DashboardWidget/';
$name = '';
$file_path = $dashboard_widget_path . $path;
$generator_path = $dashboard_widget_path . 'Generators/' . $path;
if ($extension) {
$name = $extension . ':';
$file_path = APPPATH . 'extensions/' . $extension . '/' . $file_path;
$generator_path = APPPATH . 'extensions/' . $extension . '/' . $generator_path;
} else {
$file_path = FHCPATH . $file_path;
$generator_path = FHCPATH . $generator_path;
}
$name .= $path;
$generator_path = str_replace('/', DIRECTORY_SEPARATOR, $generator_path);
if (is_dir($generator_path)) {
$dir = new DirectoryIterator($generator_path);
foreach ($dir as $file) {
if ($file->isDot())
continue;
$filename = $file->getFilename();
if ($file->isDir()) {
if (is_dir(str_replace('/', DIRECTORY_SEPARATOR, $file_path . $filename))) {
$res = $this->getGeneratorsRecursive($extension, $path . $filename . '/');
$result = array_merge($result, $res);
}
} else {
if (file_exists(str_replace('/', DIRECTORY_SEPARATOR, $file_path . $filename))) {
if ($file->getExtension() == 'js')
$result[] = $name . $file->getBasename('.js');
}
}
}
}
return $result;
}
}
@@ -31,6 +31,7 @@ class Admin extends Auth_Controller
parent::__construct(
array(
'index' => 'dashboard/admin:rw',
'widgets' => 'dashboard/admin:rw',
'preview' => 'dashboard/admin:r',
)
);
@@ -43,6 +44,11 @@ class Admin extends Auth_Controller
$this->load->view('dashboard/admin.php', []);
}
public function widgets()
{
$this->load->view('dashboard/widgets.php', [9]);
}
public function preview($dashboard_kurzbz = 'CIS')
{
$this->load->view('dashboard/preview.php', [
+11
View File
@@ -529,6 +529,17 @@ function is_in_db($key, $model = '')
return (isSuccess($result) && hasData($result));
}
/**
* check if an entry does not exist in the database
*/
function is_not_in_db($key, $model = '')
{
if (!$model)
return false;
return !is_in_db($key, $model);
}
/**
* is building an array for Dropdown Entry in Print Dropdown
* @param $id id for the Document to add to the Document Array
+32
View File
@@ -0,0 +1,32 @@
<?php
$this->load->view(
'templates/FHC-Header',
array(
'title' => 'FH-Complete',
'bootstrap5' => true,
'fontawesome6' => true,
'axios027' => true,
'restclient' => true,
'vue3' => true,
'tabulator6' => true,
'primevue3' => true,
'vuedatepicker11' => true,
'customJSs' => [
'vendor/moment/luxonjs/luxon.min.js'
],
'customJSModules' => ['public/js/apps/Dashboard/WidgetAdmin.js'],
'customCSSs' => [
'public/css/Fhc.css',
'public/css/components/verticalsplit.css',
'public/css/components/dashboard.css',
'public/css/components/primevue.css',
'public/css/components/vue-datepicker.css',
],
'navigationcomponent' => true
)
);
?>
<div id="main"></div>
<?php $this->load->view('templates/FHC-Footer'); ?>
+25 -2
View File
@@ -1,3 +1,26 @@
.fhc-page-grid {
height: 100vh;
display: grid;
grid: "header header" auto "nav main" 1fr / auto 1fr;
}
.fhc-page-grid > * {
grid-area: header;
}
.fhc-page-grid > .navbar-left-side {
grid-area: nav;
position: initial;
align-items: start;
}
.fhc-page-grid > .content {
grid-area: main;
margin: 0;
}
@media (max-width: 992px) {
.fhc-page-grid {
grid: "header main" auto "nav main" 1fr / auto 1fr;
}
}
.fhc-header {
display: flex;
flex-direction: row;
@@ -100,10 +123,10 @@
}
.modal-msg {
max-width: 1378px;
max-width: 1378px;
}
.modal-msg .modal-content {
height: 95vh;
height: 95vh;
}
.fhcSkipLink {
+33 -1
View File
@@ -34,6 +34,18 @@ export default {
url: '/api/frontend/v1/dashboard/widget/listAllowed/' + dashboard
};
},
listAll() {
return {
method: 'get',
url: '/api/frontend/v1/dashboard/widget/listAll'
};
},
listAllOriginal() {
return {
method: 'get',
url: '/api/frontend/v1/dashboard/widget/listAllOriginal'
};
},
setAllowed(dashboard_id, widget_id, allowed) {
return {
method: 'post',
@@ -42,5 +54,25 @@ export default {
dashboard_id, widget_id, allowed
}
};
}
},
create(params) {
return {
method: 'post',
url: '/api/frontend/v1/dashboard/widget/create',
params
};
},
update(params) {
return {
method: 'post',
url: '/api/frontend/v1/dashboard/widget/update',
params
};
},
generators() {
return {
method: 'get',
url: '/api/frontend/v1/dashboard/widget/generators'
};
},
};
+41
View File
@@ -0,0 +1,41 @@
import { CoreNavigationCmpt } from '../../components/navigation/Navigation.js';
import WidgetAdmin from '../../components/Dashboard/WidgetAdmin.js';
import PluginsPhrasen from '../../plugins/Phrasen.js';
const app = Vue.createApp({
name: 'WidgetsAdminApp',
data: () => ({
appSideMenuEntries: {}
}),
components: {
CoreNavigationCmpt,
WidgetAdmin
},
provide() {
return {
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone
};
},
template: /* html */`
<div class="widgets-admin-app fhc-page-grid">
<core-navigation-cmpt :add-side-menu-entries="appSideMenuEntries" />
<div class="content d-flex flex-column gap-3 w-100 h-100 overflow-hidden">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3">
<h1 class="h2 pb-3 border-bottom mb-0 w-100">Widgets</h1>
</div>
<widget-admin class="h-100 overflow-hidden" />
</div>
</div>
`
});
app.use(primevue.config.default, {
zIndex: {
overlay: 9000,
tooltip: 8000
}
})
app.use(PluginsPhrasen);
app.directive('tooltip', primevue.tooltip);
app.mount('#main');
@@ -0,0 +1,77 @@
import VerticalSplit from "../verticalsplit/verticalsplit.js";
import WidgetNew from "./WidgetAdmin/New.js";
import WidgetList from "./WidgetAdmin/List.js";
import WidgetEdit from "./WidgetAdmin/Edit.js";
export default {
name: 'WidgetAdmin',
components: {
VerticalSplit,
WidgetNew,
WidgetList,
WidgetEdit,
},
data() {
return {
originalData: null,
currentData: null,
};
},
computed: {
unsavedProgress() {
if (JSON.stringify(this.originalData) != JSON.stringify(this.currentData))
return true;
return false;
},
},
methods: {
select(data) {
this.originalData = data;
this.currentData = JSON.parse(JSON.stringify(data));
},
create(data) {
// TODO(chris): modify data with base info
this.currentData = JSON.parse(JSON.stringify(data));
this.save();
},
save() {
this.select(this.currentData);
this.$nextTick(() => {
this.$refs.list.$refs.table.tabulator
.updateOrAddData([this.originalData])
.then(res => {
res[0].select();
})
.catch(this.$fhcAlert.handleSystemError);
});
},
},
template: /* html */`
<div class="widgets-admin">
<widget-new ref="new" @create="create" />
<vertical-split class="h-100 position-relative" use-div-height>
<template #top>
<widget-list
ref="list"
:unsaved-progress="unsavedProgress"
@new="$refs.new?.show"
@select="select"
/>
</template>
<template #bottom>
<widget-edit
v-if="currentData"
ref="edit"
v-model="currentData"
:original-data="originalData"
:unsaved-progress="unsavedProgress"
@saved="save"
/>
<div v-else class="h-100 d-flex justify-content-center align-items-center">
Please select a widget
</div>
</template>
</vertical-split>
</div>`
}
@@ -0,0 +1,76 @@
import CoreForm from "../../Form/Form.js";
import EditBasics from "./Edit/Basics.js";
import ApiWidget from "../../../api/factory/dashboard/widget.js";
export default {
name: 'WidgetsAdminEdit',
components: {
CoreForm,
EditBasics,
},
props: {
originalData: Object,
modelValue: Object,
unsavedProgress: Boolean,
},
emits: [
"saved",
"update:modelValue",
],
data() {
return {
generator: null,
saving: false,
};
},
watch: {
'originalData.setup.generator': {
async handler(gen) {
if (!gen)
return this.generator = null;
// TODO(chris): extensions
let file = '../../DashboardWidget/Generators/' + gen + '.js';
this.generator = Vue.markRaw((await import(file)).default);
},
immediate: true,
}
},
methods: {
save() {
this.saving = true;
this.$refs.form.clearValidation();
const data = JSON.parse(JSON.stringify(this.modelValue));
this.$refs.form
.call(ApiWidget.update(data))
.then(() => {
this.$emit('update:modelValue', data);
this.$emit('saved');
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => this.saving = false);
},
},
template: /* html */`
<core-form
v-if="modelValue"
ref="form"
class="widgets-admin-edit mx-2"
@submit.prevent="save"
>
<edit-basics v-model="modelValue" :original="originalData" />
<template v-if="generator">
<component :is="generator" v-model="modelValue" :original="originalData" :key="modelValue.widget_id" />
</template>
<div class="position-absolute bottom-0 end-0 z-1">
<button
type="submit"
:disabled="saving || !unsavedProgress"
class="btn btn-primary"
>{{ $p.t('ui/speichern') }}</button>
</div>
</core-form>
`
}
@@ -0,0 +1,60 @@
import FormInput from "../../../Form/Input.js";
export default {
name: 'WidgetsAdminEditBasics',
components: {
FormInput,
},
props: {
original: Object,
modelValue: Object,
},
emits: [
"update:modelValue"
],
data() {
return {
};
},
computed: {
img() {
if (this.modelValue.setup?.icon)
return this.modelValue.setup.icon;
return FHC_JS_DATA_STORAGE_OBJECT.app_root + 'skin/images/fh_technikum_wien_illustration_klein.png';
}
},
methods: {
},
template: /* html */`
<div class="widgets-admin-edit-basics border-bottom mb-3">
<div class="d-flex gap-3 my-2">
<div class="col-auto">
<img
:src="img"
class="object-fit-contain"
style="height: calc(3em + 1.75rem + var(--bs-border-width) * 2 + 2 * var(--bs-body-font-size) * var(--bs-body-line-height)); width: calc(3em + 1.75rem + var(--bs-border-width) * 2 + 2 * var(--bs-body-font-size) * var(--bs-body-line-height))"
>
</div>
<div class="col">
<div class="d-flex gap-3 mb-2">
<div class="col">
<b>{{ $p.t('mobility/kurzbz') }}: </b>
{{ modelValue.widget_kurzbz }}
</div>
<div v-if="modelValue.widget_id !== undefined" class="col-auto">
<b>{{ $p.t('zeitaufzeichnung/id') }}: </b>
{{ modelValue.widget_id }}
</div>
</div>
<div>
<form-input
:label="$p.t('global/beschreibung') + ':'"
type="textarea"
v-model="modelValue.beschreibung"
/>
</div>
</div>
</div>
</div>
`
}
@@ -0,0 +1,118 @@
import { CoreFilterCmpt } from "../../filter/Filter.js";
import BsConfirm from "../../Bootstrap/Confirm.js";
import ApiDashboardWidget from "../../../api/factory/dashboard/widget.js";
export default {
name: 'WidgetsAdminList',
components: {
CoreFilterCmpt,
},
props: {
unsavedProgress: Boolean,
},
emit: [
"new",
"select",
],
data() {
return {
tabulatorOptions: {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(ApiDashboardWidget.listAllOriginal()),
ajaxResponse: (url, params, response) => response?.data,
columns: [
{
field: 'setup.icon',
title: 'Icon',
mutator: value => {
if (!value)
return FHC_JS_DATA_STORAGE_OBJECT.app_root + 'skin/images/fh_technikum_wien_illustration_klein.png';
if (value[0] == '/')
return FHC_JS_DATA_STORAGE_OBJECT.app_root + value.substr(1);
return value;
},
formatter: 'image',
formatterParams: {
height: '1.5em',
},
hozAlign: 'center'
},
{
field: 'setup.name',
title: 'Name',
},
{
field: 'widget_kurzbz',
title: 'Kurzbz',
},
{
field: 'berechtigung_kurzbz',
title: 'Berechtigung',
},
],
locale: true,
selectableRows: 1,
index: 'widget_id',
persistenceID: 'widgets-admin-list',
height: '100%',
selectableRowsCheck: this.selectableCheck,
},
tabulatorEvents: [
{
event: 'rowSelected',
handler: this.selectWidget,
},
],
};
},
methods: {
selectableCheck(row) {
if (row.isSelected())
return false;
if (!this.unsavedProgress)
return true;
BsConfirm
.popup('selectablecheck' + row.getData().widget_id)
.then(() => {
const currentRows = row.getTable().getSelectedRows();
if (currentRows.length)
currentRows[0].deselect();
row.select();
})
.catch(() => {});
return false;
},
newWidget() {
if (this.unsavedProgress) {
BsConfirm
.popup('newcheck')
.then(() => this.$emit('new'))
.catch(() => {});
} else {
this.$emit('new');
}
},
selectWidget(data) {
this.$emit('select', data.getData());
},
},
template: /* html */`
<div class="widgets-admin-list h-100 d-flex flex-column">
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
:side-menu="false"
table-only
reload
new-btn-show
new-btn-label="Widget"
@click:new="newWidget"
>
</core-filter-cmpt>
</div>
`
}
@@ -0,0 +1,83 @@
import BsModal from '../../Bootstrap/Modal.js';
import CoreForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
import ApiWidget from '../../../api/factory/dashboard/widget.js';
export default {
name: 'WidgetsAdminNew',
components: {
BsModal,
CoreForm,
FormInput,
},
emit: [
"create",
],
data() {
return {
generators: [],
result: { setup: {} },
};
},
methods: {
show() {
this.result = {
widget_kurzbz: '',
setup: {
generator: null,
},
};
this.$refs.form.clearValidation();
this.$refs.modal.show();
},
finish() {
this.$refs.form
.call(ApiWidget.create(this.result))
.then(res => {
this.$emit('create', { ...this.result, widget_id: res.data });
this.$refs.modal.hide();
})
.catch(this.$fhcAlert.handleSystemError);
},
},
created() {
this.$api
.call(ApiWidget.generators())
.then(res => {
this.generators = res.data
})
.catch(this.$fhcAlert.handleSystemError);
},
template: /* html */`
<core-form
class="widgets-admin-new"
ref="form"
@submit.prevent="finish"
>
<bs-modal ref="modal">
<template #default>
<form-input
type="select"
:label="'Generator:'"
v-model="result.setup.generator"
>
<option v-for="(generator, name) in generators" :key="name" :value="generator">
{{ generator }}
</option>
</form-input>
<form-input
type="text"
name="widget_kurzbz"
:label="$p.t('mobility/kurzbz') + ':'"
v-model="result.widget_kurzbz"
/>
</template>
<template #footer>
<button type="submit" class="btn btn-primary">OK</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</template>
</bs-modal>
</core-form>
`
}