Add widget management functionality: Implement getAvailableWidgets method in dashboardService to retrieve widget types, and create corresponding API endpoint in dashboardRouter. Update LoggedInView to allow users to select and add widgets dynamically, enhancing dashboard customization options.

This commit is contained in:
Torsten Schulz (local)
2026-01-29 16:57:12 +01:00
parent 8d2db95540
commit c09159d6ce
8 changed files with 180 additions and 15 deletions

View File

@@ -5,6 +5,17 @@ function getHashedUserId(req) {
} }
export default { export default {
/** Liste der möglichen Widget-Typen (öffentlich, keine Auth nötig wenn gewünscht aktuell mit Auth). */
async getAvailableWidgets(req, res) {
try {
const list = await dashboardService.getAvailableWidgets();
res.json(list);
} catch (error) {
console.error('Dashboard getAvailableWidgets:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
},
async getConfig(req, res) { async getConfig(req, res) {
const hashedUserId = getHashedUserId(req); const hashedUserId = getHashedUserId(req);
if (!hashedUserId) { if (!hashedUserId) {

View File

@@ -4,6 +4,7 @@ import SettingsType from './type/settings.js';
import UserParamValue from './type/user_param_value.js'; import UserParamValue from './type/user_param_value.js';
import UserParamType from './type/user_param.js'; import UserParamType from './type/user_param.js';
import UserRightType from './type/user_right.js'; import UserRightType from './type/user_right.js';
import WidgetType from './type/widget_type.js';
import User from './community/user.js'; import User from './community/user.js';
import UserParam from './community/user_param.js'; import UserParam from './community/user_param.js';
import UserDashboard from './community/user_dashboard.js'; import UserDashboard from './community/user_dashboard.js';
@@ -151,6 +152,7 @@ const models = {
UserParamValue, UserParamValue,
UserParamType, UserParamType,
UserRightType, UserRightType,
WidgetType,
User, User,
UserParam, UserParam,
UserDashboard, UserDashboard,

View File

@@ -0,0 +1,39 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const WidgetType = sequelize.define('widget_type', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
label: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Anzeigename des Widgets (z. B. "Termine")'
},
endpoint: {
type: DataTypes.STRING,
allowNull: false,
comment: 'API-Pfad (z. B. "/api/termine")'
},
description: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Optionale Beschreibung'
},
orderId: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'order_id',
comment: 'Sortierreihenfolge'
}
}, {
tableName: 'widget_type',
schema: 'type',
underscored: true,
timestamps: false
});
export default WidgetType;

View File

@@ -4,6 +4,7 @@ import dashboardController from '../controllers/dashboardController.js';
const router = Router(); const router = Router();
router.get('/widgets', authenticate, dashboardController.getAvailableWidgets.bind(dashboardController));
router.get('/config', authenticate, dashboardController.getConfig.bind(dashboardController)); router.get('/config', authenticate, dashboardController.getConfig.bind(dashboardController));
router.put('/config', authenticate, dashboardController.setConfig.bind(dashboardController)); router.put('/config', authenticate, dashboardController.setConfig.bind(dashboardController));

View File

@@ -1,7 +1,26 @@
import BaseService from './BaseService.js'; import BaseService from './BaseService.js';
import UserDashboard from '../models/community/user_dashboard.js'; import UserDashboard from '../models/community/user_dashboard.js';
import WidgetType from '../models/type/widget_type.js';
class DashboardService extends BaseService { class DashboardService extends BaseService {
/**
* Liste aller möglichen (verfügbaren) Widget-Typen.
* @returns {Promise<Array<{ id: number, label: string, endpoint: string, description: string|null, orderId: number }>>}
*/
async getAvailableWidgets() {
const rows = await WidgetType.findAll({
order: [['orderId', 'ASC'], ['id', 'ASC']],
attributes: ['id', 'label', 'endpoint', 'description', 'orderId']
});
return rows.map(r => ({
id: r.id,
label: r.label,
endpoint: r.endpoint,
description: r.description ?? null,
orderId: r.orderId
}));
}
/** /**
* @param {string} hashedUserId * @param {string} hashedUserId
* @returns {Promise<{ widgets: Array<{ id: string, title: string, endpoint: string }> }>} * @returns {Promise<{ widgets: Array<{ id: string, title: string, endpoint: string }> }>}

View File

@@ -0,0 +1,25 @@
import WidgetType from '../models/type/widget_type.js';
const DEFAULT_WIDGET_TYPES = [
{ label: 'Termine', endpoint: '/api/termine', description: 'Bevorstehende Termine', orderId: 1 }
];
/**
* Stellt die Standard-Widget-Typen in type.widget_type bereit.
* Idempotent: vorhandene Einträge werden nicht verändert.
*/
const initializeWidgetTypes = async () => {
for (const row of DEFAULT_WIDGET_TYPES) {
await WidgetType.findOrCreate({
where: { endpoint: row.endpoint },
defaults: {
label: row.label,
endpoint: row.endpoint,
description: row.description ?? null,
orderId: row.orderId ?? 0
}
});
}
};
export default initializeWidgetTypes;

View File

@@ -72,6 +72,7 @@ import initializeChat from './initializeChat.js';
import initializeMatch3Data from './initializeMatch3.js'; import initializeMatch3Data from './initializeMatch3.js';
import updateExistingMatch3Levels from './updateExistingMatch3Levels.js'; import updateExistingMatch3Levels from './updateExistingMatch3Levels.js';
import initializeTaxi from './initializeTaxi.js'; import initializeTaxi from './initializeTaxi.js';
import initializeWidgetTypes from './initializeWidgetTypes.js';
// Normale Synchronisation (nur bei STAGE=dev Schema-Updates) // Normale Synchronisation (nur bei STAGE=dev Schema-Updates)
const syncDatabase = async () => { const syncDatabase = async () => {
@@ -89,7 +90,23 @@ const syncDatabase = async () => {
} }
console.log("Initializing database schemas..."); console.log("Initializing database schemas...");
await initializeDatabase(); await initializeDatabase();
// Dashboard: Widget-Typen-Tabelle (mögliche Widgets)
console.log("Ensuring widget_type table exists...");
try {
await sequelize.query(`
CREATE TABLE IF NOT EXISTS type.widget_type (
id SERIAL PRIMARY KEY,
label VARCHAR(255) NOT NULL,
endpoint VARCHAR(255) NOT NULL,
description VARCHAR(255),
order_id INTEGER NOT NULL DEFAULT 0
);
`);
} catch (e) {
console.warn('⚠️ Konnte type.widget_type nicht anlegen:', e?.message || e);
}
// Vokabeltrainer: Tabellen sicherstellen (auch ohne manuell ausgeführte Migrations) // Vokabeltrainer: Tabellen sicherstellen (auch ohne manuell ausgeführte Migrations)
// Hintergrund: In Produktion sind Schema-Updates deaktiviert, und Migrations werden nicht automatisch ausgeführt. // Hintergrund: In Produktion sind Schema-Updates deaktiviert, und Migrations werden nicht automatisch ausgeführt.
@@ -676,6 +693,9 @@ const syncDatabase = async () => {
console.log("Initializing Taxi..."); console.log("Initializing Taxi...");
await initializeTaxi(); await initializeTaxi();
console.log("Initializing widget types...");
await initializeWidgetTypes();
console.log('Database synchronization complete.'); console.log('Database synchronization complete.');
} catch (error) { } catch (error) {
console.error('Unable to synchronize the database:', error); console.error('Unable to synchronize the database:', error);
@@ -1013,6 +1033,9 @@ const syncDatabaseForDeployment = async () => {
console.log("Initializing Taxi..."); console.log("Initializing Taxi...");
await initializeTaxi(); await initializeTaxi();
console.log("Initializing widget types...");
await initializeWidgetTypes();
console.log('Database synchronization for deployment complete.'); console.log('Database synchronization for deployment complete.');
} catch (error) { } catch (error) {
console.error('Unable to synchronize the database for deployment:', error); console.error('Unable to synchronize the database for deployment:', error);

View File

@@ -13,9 +13,22 @@
Dashboard bearbeiten Dashboard bearbeiten
</button> </button>
<template v-else> <template v-else>
<button type="button" class="btn-add" @click="addWidget"> <div class="widget-add-row">
+ Widget hinzufügen <select
</button> v-model="selectedWidgetTypeId"
class="widget-type-select"
@change="onSelectWidgetType"
>
<option value="">+ Widget hinzufügen </option>
<option
v-for="wt in widgetTypeOptions"
:key="wt.id"
:value="wt.id"
>
{{ wt.label }}
</option>
</select>
</div>
<button type="button" class="btn-done" @click="doneEditing"> <button type="button" class="btn-done" @click="doneEditing">
Fertig Fertig
</button> </button>
@@ -106,9 +119,17 @@ function generateId() {
export default { export default {
name: 'HomeLoggedInView', name: 'HomeLoggedInView',
components: { DashboardWidget }, components: { DashboardWidget },
computed: {
widgetTypeOptions() {
if (this.availableWidgets.length > 0) return this.availableWidgets;
return [{ id: 'default', label: 'Termine', endpoint: '/api/termine' }];
}
},
data() { data() {
return { return {
widgets: [], widgets: [],
availableWidgets: [],
selectedWidgetTypeId: '',
loading: true, loading: true,
loadError: null, loadError: null,
editMode: false, editMode: false,
@@ -118,12 +139,21 @@ export default {
}, },
mounted() { mounted() {
this.loadConfig(); this.loadConfig();
this.loadAvailableWidgets();
}, },
methods: { methods: {
...mapActions(['logout']), ...mapActions(['logout']),
handleLogout() { handleLogout() {
this.logout(); this.logout();
}, },
async loadAvailableWidgets() {
try {
const { data } = await apiClient.get('/api/dashboard/widgets');
this.availableWidgets = Array.isArray(data) ? data : [];
} catch (e) {
this.availableWidgets = [];
}
},
async loadConfig() { async loadConfig() {
this.loading = true; this.loading = true;
this.loadError = null; this.loadError = null;
@@ -150,13 +180,19 @@ export default {
console.error('Dashboard speichern fehlgeschlagen:', e); console.error('Dashboard speichern fehlgeschlagen:', e);
} }
}, },
addWidget() { onSelectWidgetType() {
this.widgets.push({ const id = this.selectedWidgetTypeId;
id: generateId(), if (!id) return;
title: 'Neues Widget', const wt = this.widgetTypeOptions.find(w => String(w.id) === String(id));
endpoint: '/api/termine' if (wt) {
}); this.widgets.push({
this.saveConfig(); id: generateId(),
title: wt.label,
endpoint: wt.endpoint
});
this.saveConfig();
}
this.selectedWidgetTypeId = '';
}, },
removeWidget(index) { removeWidget(index) {
this.widgets.splice(index, 1); this.widgets.splice(index, 1);
@@ -250,14 +286,23 @@ export default {
} }
.btn-edit:hover, .btn-edit:hover,
.btn-add:hover,
.btn-done:hover { .btn-done:hover {
background: #f1f3f5; background: #f1f3f5;
} }
.btn-add { .widget-add-row {
border-color: #0d6efd; display: flex;
color: #0d6efd; align-items: center;
}
.widget-type-select {
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #ced4da;
background: #fff;
color: #495057;
font-size: 0.9rem;
min-width: 180px;
} }
.btn-done { .btn-done {