Add dashboard functionality: Integrate dashboardRouter and UserDashboard model, enabling user-specific dashboard configurations. Update LoggedInView to support dynamic widget management, including adding, removing, and saving widget configurations, enhancing user experience and interactivity.

This commit is contained in:
Torsten Schulz (local)
2026-01-29 16:52:54 +01:00
parent 9519846489
commit 8d2db95540
9 changed files with 675 additions and 38 deletions

View File

@@ -20,6 +20,7 @@ import taxiMapRouter from './routers/taxiMapRouter.js';
import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js';
import termineRouter from './routers/termineRouter.js';
import vocabRouter from './routers/vocabRouter.js';
import dashboardRouter from './routers/dashboardRouter.js';
import cors from 'cors';
import './jobs/sessionCleanup.js';
@@ -78,6 +79,7 @@ app.use('/api/friendships', friendshipRouter);
app.use('/api/models', modelsProxyRouter);
app.use('/api/blog', blogRouter);
app.use('/api/termine', termineRouter);
app.use('/api/dashboard', dashboardRouter);
// Serve frontend SPA for non-API routes to support history mode clean URLs
// /models/* nicht statisch ausliefern nur über /api/models (Proxy mit Komprimierung)

View File

@@ -0,0 +1,39 @@
import dashboardService from '../services/dashboardService.js';
function getHashedUserId(req) {
return req.headers?.userid;
}
export default {
async getConfig(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const config = await dashboardService.getConfig(hashedUserId);
res.json(config);
} catch (error) {
console.error('Dashboard getConfig:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
},
async setConfig(req, res) {
const hashedUserId = getHashedUserId(req);
if (!hashedUserId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const config = req.body;
if (!config || typeof config !== 'object') {
return res.status(400).json({ error: 'Invalid config' });
}
try {
const result = await dashboardService.setConfig(hashedUserId, config);
res.json(result);
} catch (error) {
console.error('Dashboard setConfig:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}
};

View File

@@ -5,6 +5,7 @@ import ChatUser from './chat/user.js';
import Room from './chat/room.js';
import User from './community/user.js';
import UserParam from './community/user_param.js';
import UserDashboard from './community/user_dashboard.js';
import UserParamType from './type/user_param.js';
import UserRightType from './type/user_right.js';
import UserRight from './community/user_right.js';
@@ -166,6 +167,9 @@ export default function setupAssociations() {
User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' });
UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasOne(UserDashboard, { foreignKey: 'userId', as: 'dashboard' });
UserDashboard.belongsTo(User, { foreignKey: 'userId', as: 'user' });
UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_value_type' });
UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_type_value' });

View File

@@ -0,0 +1,24 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
import User from './user.js';
const UserDashboard = sequelize.define('user_dashboard', {
userId: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
references: { model: User, key: 'id' }
},
config: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: { widgets: [] }
}
}, {
tableName: 'user_dashboard',
schema: 'community',
underscored: true,
timestamps: false
});
export default UserDashboard;

View File

@@ -6,6 +6,7 @@ import UserParamType from './type/user_param.js';
import UserRightType from './type/user_right.js';
import User from './community/user.js';
import UserParam from './community/user_param.js';
import UserDashboard from './community/user_dashboard.js';
import Login from './logs/login.js';
import UserRight from './community/user_right.js';
import InterestType from './type/interest.js';
@@ -152,6 +153,7 @@ const models = {
UserRightType,
User,
UserParam,
UserDashboard,
Login,
UserRight,
InterestType,

View File

@@ -0,0 +1,10 @@
import { Router } from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import dashboardController from '../controllers/dashboardController.js';
const router = Router();
router.get('/config', authenticate, dashboardController.getConfig.bind(dashboardController));
router.put('/config', authenticate, dashboardController.setConfig.bind(dashboardController));
export default router;

View File

@@ -0,0 +1,37 @@
import BaseService from './BaseService.js';
import UserDashboard from '../models/community/user_dashboard.js';
class DashboardService extends BaseService {
/**
* @param {string} hashedUserId
* @returns {Promise<{ widgets: Array<{ id: string, title: string, endpoint: string }> }>}
*/
async getConfig(hashedUserId) {
const user = await this.getUserByHashedId(hashedUserId);
const row = await UserDashboard.findOne({ where: { userId: user.id } });
const config = row?.config ?? { widgets: [] };
if (!Array.isArray(config.widgets)) config.widgets = [];
return config;
}
/**
* @param {string} hashedUserId
* @param {{ widgets: Array<{ id: string, title: string, endpoint: string }> }} config
*/
async setConfig(hashedUserId, config) {
const user = await this.getUserByHashedId(hashedUserId);
const widgets = Array.isArray(config?.widgets) ? config.widgets : [];
const sanitized = widgets.map(w => ({
id: String(w?.id ?? ''),
title: String(w?.title ?? ''),
endpoint: String(w?.endpoint ?? '')
})).filter(w => w.id && (w.title || w.endpoint));
await UserDashboard.upsert({
userId: user.id,
config: { widgets: sanitized }
}, { conflictFields: ['userId'] });
return { widgets: sanitized };
}
}
export default new DashboardService();