feat(Calendar): integrate CalendarEvent model and enhance calendar functionality
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s
- Added CalendarEvent model to the backend, establishing relationships with the Club model for better event management. - Updated server.js to include calendarEventRoutes, enabling API access for calendar events. - Enhanced CalendarView.vue to support custom event creation and management, improving user interaction with the calendar. - Refactored various components to streamline event handling and improve overall user experience in the calendar interface. - Updated TODO and DEVELOPMENT documentation to reflect new calendar features and architectural decisions.
This commit is contained in:
42
backend/controllers/calendarEventController.js
Normal file
42
backend/controllers/calendarEventController.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import calendarEventService from '../services/calendarEventService.js';
|
||||
import { getSafeErrorMessage } from '../utils/errorUtils.js';
|
||||
|
||||
export const listClubCalendarEvents = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const { year } = req.query;
|
||||
const events = await calendarEventService.listClubEvents(userToken, clubId, year);
|
||||
res.status(200).json(events);
|
||||
} catch (error) {
|
||||
console.error('[listClubCalendarEvents] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Kalender-Events');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const createClubCalendarEvent = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const event = await calendarEventService.createClubEvent(userToken, clubId, req.body);
|
||||
res.status(201).json(event);
|
||||
} catch (error) {
|
||||
console.error('[createClubCalendarEvent] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Speichern des Kalender-Events');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteClubCalendarEvent = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, eventId } = req.params;
|
||||
const result = await calendarEventService.deleteClubEvent(userToken, clubId, eventId);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error('[deleteClubCalendarEvent] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Löschen des Kalender-Events');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
15
backend/migrations/create_calendar_events_table.sql
Normal file
15
backend/migrations/create_calendar_events_table.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE IF NOT EXISTS calendar_events (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
club_id INT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
category VARCHAR(64) NULL,
|
||||
notes TEXT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
KEY idx_calendar_events_club_start (club_id, start_date),
|
||||
CONSTRAINT fk_calendar_events_club
|
||||
FOREIGN KEY (club_id) REFERENCES clubs(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
25
backend/models/CalendarEvent.js
Normal file
25
backend/models/CalendarEvent.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import Club from './Club.js';
|
||||
|
||||
const CalendarEvent = sequelize.define('CalendarEvent', {
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
clubId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: { model: Club, key: 'id' },
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
title: { type: DataTypes.STRING(255), allowNull: false },
|
||||
startDate: { type: DataTypes.DATEONLY, allowNull: false, field: 'start_date' },
|
||||
endDate: { type: DataTypes.DATEONLY, allowNull: false, field: 'end_date' },
|
||||
category: { type: DataTypes.STRING(64), allowNull: true },
|
||||
notes: { type: DataTypes.TEXT, allowNull: true },
|
||||
}, {
|
||||
tableName: 'calendar_events',
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
indexes: [{ fields: ['club_id', 'start_date'] }],
|
||||
});
|
||||
|
||||
export default CalendarEvent;
|
||||
@@ -59,6 +59,7 @@ import MemberTrainingGroup from './MemberTrainingGroup.js';
|
||||
import ClubDisabledPresetGroup from './ClubDisabledPresetGroup.js';
|
||||
import TrainingTime from './TrainingTime.js';
|
||||
import TrainingCancellation from './TrainingCancellation.js';
|
||||
import CalendarEvent from './CalendarEvent.js';
|
||||
import BillingTemplate from './BillingTemplate.js';
|
||||
import BillingTemplateField from './BillingTemplateField.js';
|
||||
import BillingRun from './BillingRun.js';
|
||||
@@ -410,6 +411,8 @@ TrainingGroup.hasMany(TrainingTime, { foreignKey: 'trainingGroupId', as: 'traini
|
||||
TrainingTime.belongsTo(TrainingGroup, { foreignKey: 'trainingGroupId', as: 'trainingGroup' });
|
||||
Club.hasMany(TrainingCancellation, { foreignKey: 'clubId', as: 'trainingCancellations' });
|
||||
TrainingCancellation.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
|
||||
Club.hasMany(CalendarEvent, { foreignKey: 'clubId', as: 'calendarEvents' });
|
||||
CalendarEvent.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
|
||||
|
||||
// Billing
|
||||
Club.hasMany(BillingTemplate, { foreignKey: 'clubId', as: 'billingTemplates' });
|
||||
@@ -488,6 +491,7 @@ export {
|
||||
ClubDisabledPresetGroup,
|
||||
TrainingTime,
|
||||
TrainingCancellation,
|
||||
CalendarEvent,
|
||||
BillingTemplate,
|
||||
BillingTemplateField,
|
||||
BillingRun,
|
||||
|
||||
16
backend/routes/calendarEventRoutes.js
Normal file
16
backend/routes/calendarEventRoutes.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import {
|
||||
createClubCalendarEvent,
|
||||
deleteClubCalendarEvent,
|
||||
listClubCalendarEvents,
|
||||
} from '../controllers/calendarEventController.js';
|
||||
|
||||
const router = express.Router();
|
||||
router.use(authenticate);
|
||||
|
||||
router.get('/:clubId', listClubCalendarEvents);
|
||||
router.post('/:clubId', createClubCalendarEvent);
|
||||
router.delete('/:clubId/:eventId', deleteClubCalendarEvent);
|
||||
|
||||
export default router;
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
|
||||
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis, ClickTtAccount, MyTischtennisUpdateHistory, MyTischtennisFetchLog, ApiLog, MemberTransferConfig, MemberContact, MemberTtrHistory, MemberPlayInterest,
|
||||
MemberOrder, MemberOrderHistory, MemberGroupPhoto, BillingTemplate, BillingTemplateField, BillingRun, BillingDocument, BillingDocumentValue, BillingUserSetting, TrainingCancellation
|
||||
, CalendarEvent
|
||||
} from './models/index.js';
|
||||
import authRoutes from './routes/authRoutes.js';
|
||||
import clubRoutes from './routes/clubRoutes.js';
|
||||
@@ -60,6 +61,7 @@ import memberOrderRoutes from './routes/memberOrderRoutes.js';
|
||||
import memberGroupPhotoRoutes from './routes/memberGroupPhotoRoutes.js';
|
||||
import billingRoutes from './routes/billingRoutes.js';
|
||||
import calendarRoutes from './routes/calendarRoutes.js';
|
||||
import calendarEventRoutes from './routes/calendarEventRoutes.js';
|
||||
import schedulerService from './services/schedulerService.js';
|
||||
import { requestLoggingMiddleware } from './middleware/requestLoggingMiddleware.js';
|
||||
import HttpError from './exceptions/HttpError.js';
|
||||
@@ -310,6 +312,7 @@ app.use('/api/member-orders', memberOrderRoutes);
|
||||
app.use('/api/member-group-photos', memberGroupPhotoRoutes);
|
||||
app.use('/api/billing', billingRoutes);
|
||||
app.use('/api/calendar', calendarRoutes);
|
||||
app.use('/api/calendar-events', calendarEventRoutes);
|
||||
|
||||
// Middleware für dynamischen kanonischen Tag (vor express.static)
|
||||
const setCanonicalTag = (req, res, next) => {
|
||||
@@ -565,6 +568,7 @@ app.use((err, req, res, next) => {
|
||||
await safeSync(BillingUserSetting);
|
||||
await safeSync(ClubTeam);
|
||||
await safeSync(TrainingCancellation);
|
||||
await safeSync(CalendarEvent);
|
||||
await safeSync(TeamDocument);
|
||||
|
||||
// Foreign Keys wieder aktivieren
|
||||
|
||||
59
backend/services/calendarEventService.js
Normal file
59
backend/services/calendarEventService.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Op } from 'sequelize';
|
||||
import CalendarEvent from '../models/CalendarEvent.js';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
class CalendarEventService {
|
||||
async listClubEvents(userToken, clubId, year) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const normalizedYear = this.normalizeYear(year);
|
||||
return await CalendarEvent.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
startDate: { [Op.lte]: `${normalizedYear}-12-31` },
|
||||
endDate: { [Op.gte]: `${normalizedYear}-01-01` },
|
||||
},
|
||||
order: [['startDate', 'ASC'], ['title', 'ASC']],
|
||||
});
|
||||
}
|
||||
|
||||
async createClubEvent(userToken, clubId, payload) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const title = String(payload?.title || '').trim();
|
||||
if (!title) throw new HttpError('Titel fehlt', 400);
|
||||
const startDate = this.normalizeDate(payload?.startDate);
|
||||
const endDate = this.normalizeDate(payload?.endDate || payload?.startDate);
|
||||
if (!startDate || !endDate) throw new HttpError('Ungültiges Datum', 400);
|
||||
if (startDate > endDate) throw new HttpError('Enddatum darf nicht vor dem Startdatum liegen', 400);
|
||||
|
||||
return await CalendarEvent.create({
|
||||
clubId,
|
||||
title,
|
||||
startDate,
|
||||
endDate,
|
||||
category: payload?.category ? String(payload.category).trim().slice(0, 64) : null,
|
||||
notes: payload?.notes ? String(payload.notes).trim() : null,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteClubEvent(userToken, clubId, eventId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const event = await CalendarEvent.findOne({ where: { id: eventId, clubId } });
|
||||
if (!event) throw new HttpError('Event nicht gefunden', 404);
|
||||
await event.destroy();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
normalizeYear(year) {
|
||||
const parsed = Number.parseInt(year, 10);
|
||||
if (Number.isInteger(parsed) && parsed >= 2020 && parsed <= 2100) return parsed;
|
||||
return new Date().getFullYear();
|
||||
}
|
||||
|
||||
normalizeDate(date) {
|
||||
const text = String(date || '').slice(0, 10);
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(text) ? text : null;
|
||||
}
|
||||
}
|
||||
|
||||
export default new CalendarEventService();
|
||||
Reference in New Issue
Block a user