Add calendar feature: Integrate calendarRouter and CalendarEvent model, enhance CalendarView with API interactions for event management, and update localization files for error handling in both English and German. This update improves the calendar functionality and user experience.
This commit is contained in:
@@ -22,6 +22,7 @@ import termineRouter from './routers/termineRouter.js';
|
||||
import vocabRouter from './routers/vocabRouter.js';
|
||||
import dashboardRouter from './routers/dashboardRouter.js';
|
||||
import newsRouter from './routers/newsRouter.js';
|
||||
import calendarRouter from './routers/calendarRouter.js';
|
||||
import cors from 'cors';
|
||||
import './jobs/sessionCleanup.js';
|
||||
|
||||
@@ -82,6 +83,7 @@ app.use('/api/blog', blogRouter);
|
||||
app.use('/api/termine', termineRouter);
|
||||
app.use('/api/dashboard', dashboardRouter);
|
||||
app.use('/api/news', newsRouter);
|
||||
app.use('/api/calendar', calendarRouter);
|
||||
|
||||
// Serve frontend SPA for non-API routes to support history mode clean URLs
|
||||
// /models/* nicht statisch ausliefern – nur über /api/models (Proxy mit Komprimierung)
|
||||
|
||||
123
backend/controllers/calendarController.js
Normal file
123
backend/controllers/calendarController.js
Normal file
@@ -0,0 +1,123 @@
|
||||
import calendarService from '../services/calendarService.js';
|
||||
|
||||
function getHashedUserId(req) {
|
||||
return req.headers?.userid;
|
||||
}
|
||||
|
||||
export default {
|
||||
/**
|
||||
* GET /api/calendar/events
|
||||
* Get all events for the authenticated user
|
||||
* Query params: startDate, endDate (optional)
|
||||
*/
|
||||
async getEvents(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
const events = await calendarService.getEvents(hashedUserId, { startDate, endDate });
|
||||
res.json(events);
|
||||
} catch (error) {
|
||||
console.error('Calendar getEvents:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/calendar/events/:id
|
||||
* Get a single event by ID
|
||||
*/
|
||||
async getEvent(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const event = await calendarService.getEvent(hashedUserId, req.params.id);
|
||||
res.json(event);
|
||||
} catch (error) {
|
||||
console.error('Calendar getEvent:', error);
|
||||
if (error.message === 'Event not found') {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/calendar/events
|
||||
* Create a new event
|
||||
*/
|
||||
async createEvent(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const eventData = req.body;
|
||||
if (!eventData.title || !eventData.startDate) {
|
||||
return res.status(400).json({ error: 'Title and startDate are required' });
|
||||
}
|
||||
|
||||
const event = await calendarService.createEvent(hashedUserId, eventData);
|
||||
res.status(201).json(event);
|
||||
} catch (error) {
|
||||
console.error('Calendar createEvent:', error);
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT /api/calendar/events/:id
|
||||
* Update an existing event
|
||||
*/
|
||||
async updateEvent(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const eventData = req.body;
|
||||
if (!eventData.title || !eventData.startDate) {
|
||||
return res.status(400).json({ error: 'Title and startDate are required' });
|
||||
}
|
||||
|
||||
const event = await calendarService.updateEvent(hashedUserId, req.params.id, eventData);
|
||||
res.json(event);
|
||||
} catch (error) {
|
||||
console.error('Calendar updateEvent:', error);
|
||||
if (error.message === 'Event not found') {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE /api/calendar/events/:id
|
||||
* Delete an event
|
||||
*/
|
||||
async deleteEvent(req, res) {
|
||||
const hashedUserId = getHashedUserId(req);
|
||||
if (!hashedUserId) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
await calendarService.deleteEvent(hashedUserId, req.params.id);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Calendar deleteEvent:', error);
|
||||
if (error.message === 'Event not found') {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -116,6 +116,7 @@ import VocabCourseProgress from './community/vocab_course_progress.js';
|
||||
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
|
||||
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
|
||||
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
|
||||
import CalendarEvent from './community/calendar_event.js';
|
||||
import Campaign from './match3/campaign.js';
|
||||
import Match3Level from './match3/level.js';
|
||||
import Objective from './match3/objective.js';
|
||||
@@ -1078,5 +1079,9 @@ export default function setupAssociations() {
|
||||
User.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'userId', as: 'grammarExerciseProgress' });
|
||||
VocabGrammarExerciseProgress.belongsTo(VocabGrammarExercise, { foreignKey: 'exerciseId', as: 'exercise' });
|
||||
VocabGrammarExercise.hasMany(VocabGrammarExerciseProgress, { foreignKey: 'exerciseId', as: 'progress' });
|
||||
|
||||
// Calendar associations
|
||||
CalendarEvent.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||
User.hasMany(CalendarEvent, { foreignKey: 'userId', as: 'calendarEvents' });
|
||||
}
|
||||
|
||||
|
||||
86
backend/models/community/calendar_event.js
Normal file
86
backend/models/community/calendar_event.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
|
||||
class CalendarEvent extends Model { }
|
||||
|
||||
CalendarEvent.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'user',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
categoryId: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: 'personal',
|
||||
comment: 'Category key: personal, work, family, health, birthday, holiday, reminder, other'
|
||||
},
|
||||
startDate: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: false
|
||||
},
|
||||
endDate: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: true,
|
||||
comment: 'End date for multi-day events, null means same as startDate'
|
||||
},
|
||||
startTime: {
|
||||
type: DataTypes.TIME,
|
||||
allowNull: true,
|
||||
comment: 'Start time, null for all-day events'
|
||||
},
|
||||
endTime: {
|
||||
type: DataTypes.TIME,
|
||||
allowNull: true,
|
||||
comment: 'End time, null for all-day events'
|
||||
},
|
||||
allDay: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'CalendarEvent',
|
||||
tableName: 'calendar_event',
|
||||
schema: 'community',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{
|
||||
fields: ['user_id']
|
||||
},
|
||||
{
|
||||
fields: ['user_id', 'start_date']
|
||||
},
|
||||
{
|
||||
fields: ['user_id', 'start_date', 'end_date']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default CalendarEvent;
|
||||
@@ -146,6 +146,7 @@ import VocabCourseProgress from './community/vocab_course_progress.js';
|
||||
import VocabGrammarExerciseType from './community/vocab_grammar_exercise_type.js';
|
||||
import VocabGrammarExercise from './community/vocab_grammar_exercise.js';
|
||||
import VocabGrammarExerciseProgress from './community/vocab_grammar_exercise_progress.js';
|
||||
import CalendarEvent from './community/calendar_event.js';
|
||||
|
||||
const models = {
|
||||
SettingsType,
|
||||
@@ -297,6 +298,9 @@ const models = {
|
||||
VocabGrammarExerciseType,
|
||||
VocabGrammarExercise,
|
||||
VocabGrammarExerciseProgress,
|
||||
|
||||
// Calendar
|
||||
CalendarEvent,
|
||||
};
|
||||
|
||||
export default models;
|
||||
|
||||
14
backend/routers/calendarRouter.js
Normal file
14
backend/routers/calendarRouter.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import calendarController from '../controllers/calendarController.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// All routes require authentication
|
||||
router.get('/events', authenticate, calendarController.getEvents);
|
||||
router.get('/events/:id', authenticate, calendarController.getEvent);
|
||||
router.post('/events', authenticate, calendarController.createEvent);
|
||||
router.put('/events/:id', authenticate, calendarController.updateEvent);
|
||||
router.delete('/events/:id', authenticate, calendarController.deleteEvent);
|
||||
|
||||
export default router;
|
||||
169
backend/services/calendarService.js
Normal file
169
backend/services/calendarService.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import CalendarEvent from '../models/community/calendar_event.js';
|
||||
import User from '../models/community/user.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
class CalendarService {
|
||||
/**
|
||||
* Get all calendar events for a user
|
||||
* @param {string} hashedUserId - The user's hashed ID
|
||||
* @param {object} options - Optional filters (startDate, endDate)
|
||||
*/
|
||||
async getEvents(hashedUserId, options = {}) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const where = { userId: user.id };
|
||||
|
||||
// Filter by date range if provided
|
||||
if (options.startDate || options.endDate) {
|
||||
where[Op.or] = [];
|
||||
|
||||
if (options.startDate && options.endDate) {
|
||||
// Events that overlap with the requested range
|
||||
where[Op.or].push({
|
||||
startDate: { [Op.between]: [options.startDate, options.endDate] }
|
||||
});
|
||||
where[Op.or].push({
|
||||
endDate: { [Op.between]: [options.startDate, options.endDate] }
|
||||
});
|
||||
where[Op.or].push({
|
||||
[Op.and]: [
|
||||
{ startDate: { [Op.lte]: options.startDate } },
|
||||
{ endDate: { [Op.gte]: options.endDate } }
|
||||
]
|
||||
});
|
||||
} else if (options.startDate) {
|
||||
where[Op.or].push({ startDate: { [Op.gte]: options.startDate } });
|
||||
where[Op.or].push({ endDate: { [Op.gte]: options.startDate } });
|
||||
} else if (options.endDate) {
|
||||
where[Op.or].push({ startDate: { [Op.lte]: options.endDate } });
|
||||
}
|
||||
}
|
||||
|
||||
const events = await CalendarEvent.findAll({
|
||||
where,
|
||||
order: [['startDate', 'ASC'], ['startTime', 'ASC']]
|
||||
});
|
||||
|
||||
return events.map(e => this.formatEvent(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single event by ID
|
||||
*/
|
||||
async getEvent(hashedUserId, eventId) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const event = await CalendarEvent.findOne({
|
||||
where: { id: eventId, userId: user.id }
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
return this.formatEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new calendar event
|
||||
*/
|
||||
async createEvent(hashedUserId, eventData) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const event = await CalendarEvent.create({
|
||||
userId: user.id,
|
||||
title: eventData.title,
|
||||
description: eventData.description || null,
|
||||
categoryId: eventData.categoryId || 'personal',
|
||||
startDate: eventData.startDate,
|
||||
endDate: eventData.endDate || eventData.startDate,
|
||||
startTime: eventData.allDay ? null : eventData.startTime,
|
||||
endTime: eventData.allDay ? null : eventData.endTime,
|
||||
allDay: eventData.allDay || false
|
||||
});
|
||||
|
||||
return this.formatEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing calendar event
|
||||
*/
|
||||
async updateEvent(hashedUserId, eventId, eventData) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const event = await CalendarEvent.findOne({
|
||||
where: { id: eventId, userId: user.id }
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
await event.update({
|
||||
title: eventData.title,
|
||||
description: eventData.description || null,
|
||||
categoryId: eventData.categoryId || 'personal',
|
||||
startDate: eventData.startDate,
|
||||
endDate: eventData.endDate || eventData.startDate,
|
||||
startTime: eventData.allDay ? null : eventData.startTime,
|
||||
endTime: eventData.allDay ? null : eventData.endTime,
|
||||
allDay: eventData.allDay || false
|
||||
});
|
||||
|
||||
return this.formatEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a calendar event
|
||||
*/
|
||||
async deleteEvent(hashedUserId, eventId) {
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const event = await CalendarEvent.findOne({
|
||||
where: { id: eventId, userId: user.id }
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
throw new Error('Event not found');
|
||||
}
|
||||
|
||||
await event.destroy();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format event for API response
|
||||
*/
|
||||
formatEvent(event) {
|
||||
return {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
categoryId: event.categoryId,
|
||||
startDate: event.startDate,
|
||||
endDate: event.endDate,
|
||||
startTime: event.startTime ? event.startTime.substring(0, 5) : null, // HH:MM format
|
||||
endTime: event.endTime ? event.endTime.substring(0, 5) : null,
|
||||
allDay: event.allDay,
|
||||
createdAt: event.createdAt,
|
||||
updatedAt: event.updatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new CalendarService();
|
||||
Reference in New Issue
Block a user