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:
@@ -5,6 +5,17 @@ function getHashedUserId(req) {
|
||||
}
|
||||
|
||||
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) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import SettingsType from './type/settings.js';
|
||||
import UserParamValue from './type/user_param_value.js';
|
||||
import UserParamType from './type/user_param.js';
|
||||
import UserRightType from './type/user_right.js';
|
||||
import WidgetType from './type/widget_type.js';
|
||||
import User from './community/user.js';
|
||||
import UserParam from './community/user_param.js';
|
||||
import UserDashboard from './community/user_dashboard.js';
|
||||
@@ -151,6 +152,7 @@ const models = {
|
||||
UserParamValue,
|
||||
UserParamType,
|
||||
UserRightType,
|
||||
WidgetType,
|
||||
User,
|
||||
UserParam,
|
||||
UserDashboard,
|
||||
|
||||
39
backend/models/type/widget_type.js
Normal file
39
backend/models/type/widget_type.js
Normal 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;
|
||||
@@ -4,6 +4,7 @@ import dashboardController from '../controllers/dashboardController.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/widgets', authenticate, dashboardController.getAvailableWidgets.bind(dashboardController));
|
||||
router.get('/config', authenticate, dashboardController.getConfig.bind(dashboardController));
|
||||
router.put('/config', authenticate, dashboardController.setConfig.bind(dashboardController));
|
||||
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
import BaseService from './BaseService.js';
|
||||
import UserDashboard from '../models/community/user_dashboard.js';
|
||||
import WidgetType from '../models/type/widget_type.js';
|
||||
|
||||
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
|
||||
* @returns {Promise<{ widgets: Array<{ id: string, title: string, endpoint: string }> }>}
|
||||
|
||||
25
backend/utils/initializeWidgetTypes.js
Normal file
25
backend/utils/initializeWidgetTypes.js
Normal 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;
|
||||
@@ -72,6 +72,7 @@ import initializeChat from './initializeChat.js';
|
||||
import initializeMatch3Data from './initializeMatch3.js';
|
||||
import updateExistingMatch3Levels from './updateExistingMatch3Levels.js';
|
||||
import initializeTaxi from './initializeTaxi.js';
|
||||
import initializeWidgetTypes from './initializeWidgetTypes.js';
|
||||
|
||||
// Normale Synchronisation (nur bei STAGE=dev Schema-Updates)
|
||||
const syncDatabase = async () => {
|
||||
@@ -91,6 +92,22 @@ const syncDatabase = async () => {
|
||||
console.log("Initializing database schemas...");
|
||||
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)
|
||||
// Hintergrund: In Produktion sind Schema-Updates deaktiviert, und Migrations werden nicht automatisch ausgeführt.
|
||||
// Damit API/Menu nicht mit "relation does not exist" (42P01) scheitert, legen wir die Tabellen idempotent an.
|
||||
@@ -676,6 +693,9 @@ const syncDatabase = async () => {
|
||||
console.log("Initializing Taxi...");
|
||||
await initializeTaxi();
|
||||
|
||||
console.log("Initializing widget types...");
|
||||
await initializeWidgetTypes();
|
||||
|
||||
console.log('Database synchronization complete.');
|
||||
} catch (error) {
|
||||
console.error('Unable to synchronize the database:', error);
|
||||
@@ -1013,6 +1033,9 @@ const syncDatabaseForDeployment = async () => {
|
||||
console.log("Initializing Taxi...");
|
||||
await initializeTaxi();
|
||||
|
||||
console.log("Initializing widget types...");
|
||||
await initializeWidgetTypes();
|
||||
|
||||
console.log('Database synchronization for deployment complete.');
|
||||
} catch (error) {
|
||||
console.error('Unable to synchronize the database for deployment:', error);
|
||||
|
||||
@@ -13,9 +13,22 @@
|
||||
Dashboard bearbeiten
|
||||
</button>
|
||||
<template v-else>
|
||||
<button type="button" class="btn-add" @click="addWidget">
|
||||
+ Widget hinzufügen
|
||||
</button>
|
||||
<div class="widget-add-row">
|
||||
<select
|
||||
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">
|
||||
Fertig
|
||||
</button>
|
||||
@@ -106,9 +119,17 @@ function generateId() {
|
||||
export default {
|
||||
name: 'HomeLoggedInView',
|
||||
components: { DashboardWidget },
|
||||
computed: {
|
||||
widgetTypeOptions() {
|
||||
if (this.availableWidgets.length > 0) return this.availableWidgets;
|
||||
return [{ id: 'default', label: 'Termine', endpoint: '/api/termine' }];
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
widgets: [],
|
||||
availableWidgets: [],
|
||||
selectedWidgetTypeId: '',
|
||||
loading: true,
|
||||
loadError: null,
|
||||
editMode: false,
|
||||
@@ -118,12 +139,21 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.loadConfig();
|
||||
this.loadAvailableWidgets();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['logout']),
|
||||
handleLogout() {
|
||||
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() {
|
||||
this.loading = true;
|
||||
this.loadError = null;
|
||||
@@ -150,13 +180,19 @@ export default {
|
||||
console.error('Dashboard speichern fehlgeschlagen:', e);
|
||||
}
|
||||
},
|
||||
addWidget() {
|
||||
onSelectWidgetType() {
|
||||
const id = this.selectedWidgetTypeId;
|
||||
if (!id) return;
|
||||
const wt = this.widgetTypeOptions.find(w => String(w.id) === String(id));
|
||||
if (wt) {
|
||||
this.widgets.push({
|
||||
id: generateId(),
|
||||
title: 'Neues Widget',
|
||||
endpoint: '/api/termine'
|
||||
title: wt.label,
|
||||
endpoint: wt.endpoint
|
||||
});
|
||||
this.saveConfig();
|
||||
}
|
||||
this.selectedWidgetTypeId = '';
|
||||
},
|
||||
removeWidget(index) {
|
||||
this.widgets.splice(index, 1);
|
||||
@@ -250,14 +286,23 @@ export default {
|
||||
}
|
||||
|
||||
.btn-edit:hover,
|
||||
.btn-add:hover,
|
||||
.btn-done:hover {
|
||||
background: #f1f3f5;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
border-color: #0d6efd;
|
||||
color: #0d6efd;
|
||||
.widget-add-row {
|
||||
display: flex;
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user