Update server port and enhance participant management features
Changed the server port from 3000 to 3005 for local development. Enhanced the participant management functionality by adding a new endpoint to update participant group assignments, including error handling for non-existent participants. Updated the participant model to include a groupId reference, and modified the participant retrieval logic to include group information. Additionally, improved the frontend API client to accommodate the new backend structure and added filtering options in the MembersView for better user experience.
This commit is contained in:
@@ -4,7 +4,10 @@ import { devLog } from '../utils/logger.js';
|
||||
export const getParticipants = async (req, res) => {
|
||||
try {
|
||||
const { dateId } = req.params;
|
||||
const participants = await Participant.findAll({ where: { diaryDateId: dateId } });
|
||||
const participants = await Participant.findAll({
|
||||
where: { diaryDateId: dateId },
|
||||
attributes: ['id', 'diaryDateId', 'memberId', 'groupId', 'notes', 'createdAt', 'updatedAt']
|
||||
});
|
||||
res.status(200).json(participants);
|
||||
} catch (error) {
|
||||
devLog(error);
|
||||
@@ -12,6 +15,32 @@ export const getParticipants = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const updateParticipantGroup = async (req, res) => {
|
||||
try {
|
||||
const { dateId, memberId } = req.params;
|
||||
const { groupId } = req.body;
|
||||
|
||||
const participant = await Participant.findOne({
|
||||
where: {
|
||||
diaryDateId: dateId,
|
||||
memberId: memberId
|
||||
}
|
||||
});
|
||||
|
||||
if (!participant) {
|
||||
return res.status(404).json({ error: 'Teilnehmer nicht gefunden' });
|
||||
}
|
||||
|
||||
participant.groupId = groupId || null;
|
||||
await participant.save();
|
||||
|
||||
res.status(200).json(participant);
|
||||
} catch (error) {
|
||||
devLog(error);
|
||||
res.status(500).json({ error: 'Fehler beim Aktualisieren der Teilnehmer-Gruppenzuordnung' });
|
||||
}
|
||||
};
|
||||
|
||||
export const addParticipant = async (req, res) => {
|
||||
try {
|
||||
const { diaryDateId, memberId } = req.body;
|
||||
|
||||
9
backend/migrations/add_group_id_to_participants.sql
Normal file
9
backend/migrations/add_group_id_to_participants.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- Migration: Add group_id to participants table
|
||||
-- This allows assigning participants to groups for training organization
|
||||
|
||||
ALTER TABLE participants
|
||||
ADD COLUMN group_id INTEGER NULL REFERENCES "group"(id) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- Add index for better query performance
|
||||
CREATE INDEX IF NOT EXISTS idx_participants_group_id ON participants(group_id);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import Member from './Member.js';
|
||||
import DiaryDate from './DiaryDates.js';
|
||||
import Group from './Group.js';
|
||||
import { encryptData, decryptData } from '../utils/encrypt.js';
|
||||
|
||||
const Participant = sequelize.define('Participant', {
|
||||
@@ -27,6 +28,16 @@ const Participant = sequelize.define('Participant', {
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
groupId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: Group,
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.STRING(4096),
|
||||
allowNull: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import express from 'express';
|
||||
import { getParticipants, addParticipant, removeParticipant } from '../controllers/participantController.js';
|
||||
import { getParticipants, addParticipant, removeParticipant, updateParticipantGroup } from '../controllers/participantController.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -7,5 +7,6 @@ const router = express.Router();
|
||||
router.get('/:dateId', authenticate, getParticipants);
|
||||
router.post('/add', authenticate, addParticipant);
|
||||
router.post('/remove', authenticate, removeParticipant);
|
||||
router.put('/:dateId/:memberId/group', authenticate, updateParticipantGroup);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -43,7 +43,7 @@ import permissionRoutes from './routes/permissionRoutes.js';
|
||||
import schedulerService from './services/schedulerService.js';
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
const port = process.env.PORT || 3005;
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios from 'axios';
|
||||
import store from './store';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: `${import.meta.env.VITE_BACKEND}/api`,
|
||||
baseURL: `${import.meta.env.VITE_BACKEND || 'http://localhost:3005'}/api`,
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use(config => {
|
||||
|
||||
@@ -848,6 +848,17 @@ export default {
|
||||
this.participants = response.data.map(participant => participant.memberId);
|
||||
// Map für memberId -> participantId speichern
|
||||
this.participantMapByMemberId = response.data.reduce((map, p) => { map[p.memberId] = p.id; return map; }, {});
|
||||
// Map für memberId -> groupId speichern und mit Reaktivität initialisieren
|
||||
this.memberGroupsMap = {};
|
||||
response.data.forEach(p => {
|
||||
if (p.groupId) {
|
||||
if (this.$set) {
|
||||
this.$set(this.memberGroupsMap, p.memberId, p.groupId);
|
||||
} else {
|
||||
this.memberGroupsMap[p.memberId] = p.groupId;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async loadActivities(dateId) {
|
||||
@@ -1391,12 +1402,19 @@ export default {
|
||||
},
|
||||
async createGroups() {
|
||||
try {
|
||||
// Erstelle die gewünschte Anzahl Gruppen mit Namen 1 bis X
|
||||
for (let i = 1; i <= this.newGroupCount; i++) {
|
||||
// Bestimme Startnummer basierend auf vorhandenen Gruppen
|
||||
const existingNumbers = (this.groups || [])
|
||||
.map(g => parseInt((g.name || '').trim(), 10))
|
||||
.filter(n => Number.isFinite(n));
|
||||
const startNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1;
|
||||
|
||||
// Erstelle die gewünschte Anzahl Gruppen mit fortlaufender Nummerierung
|
||||
for (let i = 0; i < this.newGroupCount; i++) {
|
||||
const groupNumber = startNumber + i;
|
||||
const form = {
|
||||
clubid: this.currentClub,
|
||||
dateid: this.date.id,
|
||||
name: i.toString(),
|
||||
name: groupNumber.toString(),
|
||||
lead: '', // Leiter wird leer gelassen
|
||||
}
|
||||
await apiClient.post('/group', form);
|
||||
@@ -1933,14 +1951,25 @@ export default {
|
||||
|
||||
async updateMemberGroup(memberId, groupId) {
|
||||
try {
|
||||
// Hier würde normalerweise ein API-Call gemacht werden
|
||||
// Für jetzt speichern wir es nur lokal
|
||||
this.memberGroupsMap[memberId] = groupId || '';
|
||||
const selectedGroupId = groupId || '';
|
||||
|
||||
// TODO: API-Call zum Speichern der Teilnehmer-Gruppenzuordnung
|
||||
// await apiClient.put(`/participants/${this.date.id}/${memberId}/group`, { groupId });
|
||||
// Verwende Vue.set für Reaktivität (Vue 2)
|
||||
if (this.$set) {
|
||||
this.$set(this.memberGroupsMap, memberId, selectedGroupId);
|
||||
} else {
|
||||
// Vue 3 oder Fallback
|
||||
this.memberGroupsMap = {
|
||||
...this.memberGroupsMap,
|
||||
[memberId]: selectedGroupId
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`Teilnehmer ${memberId} wurde Gruppe ${groupId} zugewiesen`);
|
||||
// API-Call zum Speichern der Teilnehmer-Gruppenzuordnung
|
||||
await apiClient.put(`/participants/${this.date.id}/${memberId}/group`, {
|
||||
groupId: selectedGroupId || null
|
||||
});
|
||||
|
||||
console.log(`Teilnehmer ${memberId} wurde Gruppe ${selectedGroupId} zugewiesen`);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Teilnehmer-Gruppenzuordnung:', error);
|
||||
this.showInfo('Fehler', 'Fehler beim Aktualisieren der Teilnehmer-Gruppenzuordnung', '', 'error');
|
||||
|
||||
@@ -98,7 +98,7 @@ export default {
|
||||
...mapActions(['login']),
|
||||
async executeLogin() {
|
||||
try {
|
||||
const response = await axios.post(`${import.meta.env.VITE_BACKEND}/api/auth/login`, { email: this.email, password: this.password }, {
|
||||
const response = await axios.post(`${import.meta.env.VITE_BACKEND || 'http://localhost:3005'}/api/auth/login`, { email: this.email, password: this.password }, {
|
||||
timeout: 5000,
|
||||
});
|
||||
await this.login({ token: response.data.token, username: this.email });
|
||||
|
||||
@@ -68,11 +68,41 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" v-model="showInactiveMembers">
|
||||
<span>Inaktive Mitglieder anzeigen</span>
|
||||
</label>
|
||||
<div class="filters-section">
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" v-model="showInactiveMembers">
|
||||
<span>Inaktive Mitglieder anzeigen</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="filter-controls">
|
||||
<div class="filter-group">
|
||||
<label>Altersklasse:</label>
|
||||
<select v-model="selectedAgeGroup" class="filter-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="adult">Erwachsene (20+)</option>
|
||||
<option value="J19">J19 (19 und jünger)</option>
|
||||
<option value="J17">J17 (17 und jünger)</option>
|
||||
<option value="J15">J15 (15 und jünger)</option>
|
||||
<option value="J13">J13 (13 und jünger)</option>
|
||||
<option value="J11">J11 (11 und jünger)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Geschlecht:</label>
|
||||
<select v-model="selectedGender" class="filter-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="female">Weiblich</option>
|
||||
<option value="male">Männlich</option>
|
||||
<option value="diverse">Divers</option>
|
||||
<option value="unknown">Unbekannt</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button @click="clearFilters" class="btn-clear-filters">Filter zurücksetzen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<table>
|
||||
@@ -90,7 +120,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="member in members" :key="member.id">
|
||||
<template v-for="member in filteredMembers" :key="member.id">
|
||||
<tr v-if="member.active || showInactiveMembers" class="member-row" :class="{ 'row-inactive': !member.active, 'row-test': member.testMembership }" @click="editMember(member)">
|
||||
<td>
|
||||
<img v-if="member.imageUrl" :src="member.imageUrl" alt="Mitgliedsbild"
|
||||
@@ -237,6 +267,32 @@ export default {
|
||||
|
||||
inactiveMembersCount() {
|
||||
return this.members.filter(member => !member.active).length;
|
||||
},
|
||||
|
||||
filteredMembers() {
|
||||
return this.members.filter(member => {
|
||||
// Inaktive Mitglieder Filter
|
||||
if (!member.active && !this.showInactiveMembers) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Altersklasse Filter
|
||||
if (this.selectedAgeGroup) {
|
||||
const age = this.getAge(member.birthDate);
|
||||
if (!this.matchesAgeGroup(age, this.selectedAgeGroup)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Geschlecht Filter
|
||||
if (this.selectedGender) {
|
||||
if (member.gender !== this.selectedGender) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -283,7 +339,9 @@ export default {
|
||||
isUpdatingRatings: false,
|
||||
showMemberInfo: false,
|
||||
showActivitiesModal: false,
|
||||
selectedMemberForActivities: null
|
||||
selectedMemberForActivities: null,
|
||||
selectedAgeGroup: '',
|
||||
selectedGender: ''
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -618,6 +676,60 @@ export default {
|
||||
} finally {
|
||||
this.isUpdatingRatings = false;
|
||||
}
|
||||
},
|
||||
|
||||
getAge(birthDate) {
|
||||
if (!birthDate) return null;
|
||||
|
||||
const birth = new Date(birthDate);
|
||||
if (isNaN(birth.getTime())) return null;
|
||||
|
||||
const today = new Date();
|
||||
const age = today.getFullYear() - birth.getFullYear();
|
||||
const monthDiff = today.getMonth() - birth.getMonth();
|
||||
|
||||
// Korrektur für Geburtstag in diesem Jahr
|
||||
return monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())
|
||||
? age - 1
|
||||
: age;
|
||||
},
|
||||
|
||||
matchesAgeGroup(age, ageGroup) {
|
||||
if (age === null) return false;
|
||||
|
||||
// Tischtennis-Logik: Altersklassen basieren auf Jahrgängen mit Stichtag 1. Januar
|
||||
// J19 = 19 Jahre und jünger am 1. Januar des aktuellen Jahres
|
||||
// J17 = 17 Jahre und jünger am 1. Januar des aktuellen Jahres
|
||||
// etc.
|
||||
|
||||
switch (ageGroup) {
|
||||
case 'adult':
|
||||
return age > 19; // Erwachsene = älter als 19
|
||||
|
||||
case 'J19':
|
||||
return age <= 19; // J19 = 19 und jünger
|
||||
|
||||
case 'J17':
|
||||
return age <= 17; // J17 = 17 und jünger
|
||||
|
||||
case 'J15':
|
||||
return age <= 15; // J15 = 15 und jünger
|
||||
|
||||
case 'J13':
|
||||
return age <= 13; // J13 = 13 und jünger
|
||||
|
||||
case 'J11':
|
||||
return age <= 11; // J11 = 11 und jünger
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
clearFilters() {
|
||||
this.selectedAgeGroup = '';
|
||||
this.selectedGender = '';
|
||||
this.showInactiveMembers = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -948,4 +1060,78 @@ table td {
|
||||
.btn-activities:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
/* Filter Styles */
|
||||
.filters-section {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-group .checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-group .checkbox-item input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
font-size: 0.9em;
|
||||
min-width: 120px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.btn-clear-filters {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-clear-filters:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -358,7 +358,7 @@ export default {
|
||||
this.selectedFile = e.target.files && e.target.files[0] ? e.target.files[0] : null;
|
||||
},
|
||||
imageUrl(img) {
|
||||
return `http://localhost:3000/api/predefined-activities/${this.editModel.id}/image/${img.id}`;
|
||||
return `/api/predefined-activities/${this.editModel.id}/image/${img.id}`;
|
||||
},
|
||||
async uploadImage() {
|
||||
if (!this.editModel || !this.editModel.id || !this.selectedFile) {
|
||||
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
|
||||
async register() {
|
||||
try {
|
||||
await axios.post(`${import.meta.env.VITE_BACKEND}/api/auth/register`, { email: this.email, password: this.password });
|
||||
await axios.post(`${import.meta.env.VITE_BACKEND || 'http://localhost:3005'}/api/auth/register`, { email: this.email, password: this.password });
|
||||
alert('Registration successful! Please check your email to activate your account.');
|
||||
} catch (error) {
|
||||
alert('Registrierung fehlgeschlagen');
|
||||
|
||||
Reference in New Issue
Block a user