Merge branch 'main' of ssh://tsschulz.de:/home/git/trainingstagebuch

This commit is contained in:
Torsten Schulz (server)
2025-09-01 07:38:48 +00:00
8 changed files with 179 additions and 7 deletions

View File

@@ -116,3 +116,15 @@ const deleteTagFromDiaryDate = async (req, res) => {
export { getDatesForClub, createDateForClub, updateTrainingTimes, addDiaryNote, deleteDiaryNote, addDiaryTag,
addTagToDiaryDate, deleteTagFromDiaryDate };
export const deleteDateForClub = async (req, res) => {
try {
const { clubId, dateId } = req.params;
const { authcode: userToken } = req.headers;
const result = await diaryService.removeDateForClub(userToken, clubId, dateId);
res.status(200).json(result);
} catch (error) {
console.error('[deleteDateForClub] - Error:', error);
res.status(error.statusCode || 500).json({ error: error.message || 'systemerror' });
}
};

View File

@@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"postinstall": "cd ../frontend && npm install && npm run build",
"dev": "nodemon server.js"
"dev": "nodemon server.js",
"cleanup:usertoken": "node ./scripts/cleanupUserTokenKeys.js"
},
"keywords": [],
"author": "",

View File

@@ -8,7 +8,8 @@ import {
deleteDiaryNote,
addDiaryTag,
addTagToDiaryDate,
deleteTagFromDiaryDate
deleteTagFromDiaryDate,
deleteDateForClub,
} from '../controllers/diaryController.js';
const router = express.Router();
@@ -21,5 +22,6 @@ router.delete('/:clubId/tag', authenticate, deleteTagFromDiaryDate);
router.get('/:clubId', authenticate, getDatesForClub);
router.post('/:clubId', authenticate, createDateForClub);
router.put('/:clubId', authenticate, updateTrainingTimes);
router.delete('/:clubId/:dateId', authenticate, deleteDateForClub);
export default router;

View File

@@ -0,0 +1,90 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const dbConfig = {
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'trainingdiary',
};
async function getIndexSummary(connection, table) {
const [rows] = await connection.execute(`SHOW INDEX FROM \`${table}\``);
const summary = rows.reduce((acc, r) => {
const key = r.Key_name;
acc[key] = acc[key] || { unique: r.Non_unique === 0, columns: [] };
acc[key].columns.push(r.Column_name);
return acc;
}, {});
return summary;
}
async function cleanupUserTokenKeys() {
let connection;
const table = 'UserToken';
try {
console.log('Connecting to DB:', dbConfig);
connection = await mysql.createConnection(dbConfig);
console.log(`\nBefore cleanup (indexes on ${table}):`);
let before = await getIndexSummary(connection, table);
Object.entries(before).forEach(([name, info]) => {
console.log(` - ${name} ${info.unique ? '(UNIQUE)' : ''} -> [${info.columns.join(', ')}]`);
});
// Drop all non-PRIMARY indexes on UserToken
const [indexes] = await connection.execute(`SHOW INDEX FROM \`${table}\``);
const keyNames = Array.from(new Set(indexes.map(i => i.Key_name))).filter(k => k !== 'PRIMARY');
for (const keyName of keyNames) {
try {
await connection.execute(`DROP INDEX \`${keyName}\` ON \`${table}\``);
console.log(`Dropped index: ${keyName}`);
} catch (err) {
console.warn(`Could not drop ${keyName}: ${err.code || err.message}`);
}
}
// Re-create minimal, deterministic indexes
// Unique on token (column is 'token')
try {
await connection.execute(`CREATE UNIQUE INDEX \`uniq_UserToken_token\` ON \`${table}\` (\`token\`)`);
console.log('Created UNIQUE index: uniq_UserToken_token (token)');
} catch (err) {
console.warn('Could not create uniq_UserToken_token:', err.code || err.message);
}
// Helpful index on user_id if column exists
try {
const [cols] = await connection.execute(`SHOW COLUMNS FROM \`${table}\` LIKE 'user_id'`);
if (cols && cols.length > 0) {
await connection.execute(`CREATE INDEX \`idx_UserToken_user_id\` ON \`${table}\` (\`user_id\`)`);
console.log('Created INDEX: idx_UserToken_user_id (user_id)');
} else {
console.log('Column user_id not found, skip creating idx_UserToken_user_id');
}
} catch (err) {
console.warn('Could not create idx_UserToken_user_id:', err.code || err.message);
}
console.log(`\nAfter cleanup (indexes on ${table}):`);
const after = await getIndexSummary(connection, table);
Object.entries(after).forEach(([name, info]) => {
console.log(` - ${name} ${info.unique ? '(UNIQUE)' : ''} -> [${info.columns.join(', ')}]`);
});
console.log('\nDone.');
} catch (err) {
console.error('Cleanup failed:', err);
process.exitCode = 1;
} finally {
if (connection) await connection.end();
}
}
cleanupUserTokenKeys();

View File

@@ -151,7 +151,7 @@ app.get('*', (req, res) => {
await TournamentMatch.sync({ alter: true });
await TournamentResult.sync({ alter: true });
await Accident.sync({ alter: true });
await UserToken.sync({ alter: true });
await UserToken.sync();
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);

View File

@@ -20,7 +20,7 @@ class DiaryDateActivityService {
duration: data.duration
});
}
restData.predefinedActivityId = predefinedActivity.id;
restData.predefinedActivityId = predefinedActivity.id;
const maxOrderId = await DiaryDateActivity.max('orderId', {
where: { diaryDateId: data.diaryDateId }
});

View File

@@ -1,4 +1,5 @@
import DiaryDate from '../models/DiaryDates.js';
import DiaryDateActivity from '../models/DiaryDateActivity.js';
import Club from '../models/Club.js';
import DiaryNote from '../models/DiaryNote.js';
import { DiaryTag } from '../models/DiaryTag.js';
@@ -151,6 +152,23 @@ class DiaryService {
await DiaryDateTag.destroy({ where: { tagId } });
}
async removeDateForClub(userToken, clubId, dateId) {
console.log('[DiaryService::removeDateForClub] - Check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryService::removeDateForClub] - Validate date');
const diaryDate = await DiaryDate.findOne({ where: { id: dateId, clubId } });
if (!diaryDate) {
throw new HttpError('Diary entry not found', 404);
}
console.log('[DiaryService::removeDateForClub] - Check for activities');
const activityCount = await DiaryDateActivity.count({ where: { diaryDateId: dateId } });
if (activityCount > 0) {
throw new HttpError('Cannot delete date with activities', 409);
}
console.log('[DiaryService::removeDateForClub] - Delete diary date');
await diaryDate.destroy();
return { ok: true };
}
}
export default new DiaryService();

View File

@@ -8,6 +8,7 @@
<option v-for="entry in dates" :key="entry.id" :value="entry">{{ getFormattedDate(entry.date) }}
</option>
</select>
<button v-if="date && date !== 'new' && trainingPlan.length === 0" class="btn-secondary" @click="deleteCurrentDate">Datum löschen</button>
</label>
</div>
<div v-if="showForm && date === 'new'">
@@ -84,7 +85,7 @@
</div>
</div>
<h3>Trainingsplan</h3>
<div style="overflow-x: auto; overflow-y: visible; position: relative;">
<div style="overflow: visible; position: relative;">
<table>
<thead>
<tr>
@@ -464,6 +465,17 @@ export default {
}
},
async refreshDates(selectId) {
const response = await apiClient.get(`/diary/${this.currentClub}`);
this.dates = response.data.map(entry => ({ id: entry.id, date: entry.date }));
// neueste zuerst
this.dates.sort((a, b) => new Date(b.date) - new Date(a.date));
if (selectId) {
const match = this.dates.find(d => String(d.id) === String(selectId));
if (match) this.date = { id: match.id, date: match.date };
}
},
setCurrentDate() {
const today = new Date().toISOString().split('T')[0];
this.newDate = today;
@@ -517,11 +529,14 @@ export default {
trainingEnd: this.trainingEnd || null,
});
this.dates.push({ id: response.data.id, date: response.data.date });
this.date = { id: response.data.id, date: response.data.date };
// Liste nach Datum sortieren (neueste zuerst)
await this.refreshDates(response.data.id);
this.showForm = false;
this.newDate = '';
this.trainingStart = response.data.trainingStart;
this.trainingEnd = response.data.trainingEnd;
// Direkt auf das leere Tagebuch des neuen Datums wechseln
await this.handleDateChange();
} catch (error) {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
@@ -827,6 +842,13 @@ export default {
async addPlanItem() {
try {
if (this.addNewItem || this.addNewTimeblock) {
// Sicherstellen, dass das ausgewählte Datum existiert
const list = await apiClient.get(`/diary/${this.currentClub}`).then(r => r.data);
if (!list.some(e => String(e.id) === String(this.date?.id))) {
await this.refreshDates();
alert('Ausgewähltes Datum war nicht mehr aktuell. Bitte erneut versuchen.');
return;
}
await apiClient.post(`/diary-date-activities/${this.currentClub}`, {
diaryDateId: this.date.id,
activity: this.addNewTimeblock ? '' : this.newPlanItem.activity,
@@ -850,7 +872,34 @@ export default {
this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data);
this.calculateIntermediateTimes();
} catch (error) {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
const msg = (error && error.response && error.response.data && error.response.data.error) || 'Ein Fehler ist aufgetreten.';
if (msg.toLowerCase().includes('foreign key') || msg.toLowerCase().includes('constraint')) {
await this.refreshDates();
alert('Datum war nicht (mehr) vorhanden. Die Datums-Auswahl wurde aktualisiert. Bitte erneut versuchen.');
} else {
alert(msg);
}
}
},
async deleteCurrentDate() {
if (!this.date || this.date === 'new') return;
if (this.trainingPlan.length > 0) {
alert('Datum kann nicht gelöscht werden: Es sind bereits Aktivitäten vorhanden.');
return;
}
if (!confirm('Dieses Datum wirklich löschen?')) return;
try {
await apiClient.delete(`/diary/${this.currentClub}/${this.date.id}`);
await this.refreshDates();
this.date = null;
this.trainingStart = '';
this.trainingEnd = '';
this.participants = [];
this.trainingPlan = [];
} catch (e) {
const msg = (e && e.response && e.response.data && e.response.data.error) || 'Fehler beim Löschen.';
alert(msg);
}
},