feat: improve user token handling and add club selection clearing button in settings
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s

This commit is contained in:
Torsten Schulz (local)
2026-05-15 16:52:26 +02:00
parent d955577d9a
commit 40bd5e0745
3 changed files with 119 additions and 162 deletions

View File

@@ -1,11 +1,16 @@
import fs from 'fs';
import diaryDateActivityService from '../services/diaryDateActivityService.js';
import { emitActivityChanged } from '../services/socketService.js';
import DiaryDate from '../models/DiaryDates.js';
import { devLog } from '../utils/logger.js';
import { devLog, errorLog } from '../utils/logger.js';
export const createDiaryDateActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
let userToken = req.headers['authcode'] || req.headers['auth-code'] || null;
const authHeader = req.headers['authorization'] || req.headers['Authorization'];
if (!userToken && authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
userToken = authHeader.split(' ')[1];
}
const { clubId } = req.params;
const { diaryDateId, activity, predefinedActivityId, duration, durationText, orderId, isTimeblock, groupId } = req.body;
const activityItem = await diaryDateActivityService.createActivity(userToken, clubId, {
@@ -34,7 +39,11 @@ export const createDiaryDateActivity = async (req, res) => {
export const updateDiaryDateActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
let userToken = req.headers['authcode'] || req.headers['auth-code'] || null;
const authHeader = req.headers['authorization'] || req.headers['Authorization'];
if (!userToken && authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
userToken = authHeader.split(' ')[1];
}
const { clubId, id } = req.params;
const { predefinedActivityId, customActivityName, duration, durationText, orderId, groupId } = req.body; // Add groupId
const updatedActivity = await diaryDateActivityService.updateActivity(userToken, clubId, id, {
@@ -62,7 +71,11 @@ export const updateDiaryDateActivity = async (req, res) => {
export const deleteDiaryDateActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
let userToken = req.headers['authcode'] || req.headers['auth-code'] || null;
const authHeader = req.headers['authorization'] || req.headers['Authorization'];
if (!userToken && authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
userToken = authHeader.split(' ')[1];
}
const { clubId, id } = req.params;
// Hole diaryDateId vor dem Löschen
@@ -88,7 +101,11 @@ export const deleteDiaryDateActivity = async (req, res) => {
export const updateDiaryDateActivityOrder = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
let userToken = req.headers['authcode'] || req.headers['auth-code'] || null;
const authHeader = req.headers['authorization'] || req.headers['Authorization'];
if (!userToken && authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
userToken = authHeader.split(' ')[1];
}
const { clubId, id } = req.params;
const { orderId } = req.body;
const updatedActivity = await diaryDateActivityService.updateActivityOrder(userToken, clubId, id, orderId);
@@ -110,19 +127,35 @@ export const updateDiaryDateActivityOrder = async (req, res) => {
export const getDiaryDateActivities = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
let userToken = req.headers['authcode'] || req.headers['auth-code'] || null;
const authHeader = req.headers['authorization'] || req.headers['Authorization'];
if (!userToken && authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
userToken = authHeader.split(' ')[1];
}
const { clubId, diaryDateId } = req.params;
const activities = await diaryDateActivityService.getActivities(userToken, clubId, diaryDateId);
res.status(200).json(activities);
} catch (error) {
// Fallback-Logging: schreibe Stacktrace in eine Datei, falls STDOUT/STDERR nicht sichtbar ist
try {
const msg = `${new Date().toISOString()} - getDiaryDateActivities error: ${error && error.stack ? error.stack : JSON.stringify(error)}\n`;
fs.appendFileSync('/tmp/diary-activity-error.log', msg);
} catch (e) {
// ignore
}
devLog(error);
errorLog(error);
res.status(500).json({ error: 'Error getting activities' });
}
}
export const addGroupActivity = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
let userToken = req.headers['authcode'] || req.headers['auth-code'] || null;
const authHeader = req.headers['authorization'] || req.headers['Authorization'];
if (!userToken && authHeader && typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
userToken = authHeader.split(' ')[1];
}
const { clubId, diaryDateId, groupId, activity, predefinedActivityId, timeblockId, duration, durationText } = req.body;
const activityItem = await diaryDateActivityService.addGroupActivity(
userToken,

View File

@@ -177,178 +177,88 @@ class DiaryDateActivityService {
async getActivities(userToken, clubId, diaryDateId) {
await checkAccess(userToken, clubId);
// Avoid Sequelize eager-loading includes here to prevent EagerLoadingErrors
// if associations are not initialized in certain environments. Instead,
// load the core DiaryDateActivity rows and fetch related models separately.
const activities = await DiaryDateActivity.findAll({
where: { diaryDateId },
order: [['orderId', 'ASC']],
include: [
{
model: PredefinedActivity,
as: 'predefinedActivity',
include: [
{
model: PredefinedActivityImage,
as: 'images'
}
]
},
{
model: Group,
as: 'planGroup'
},
{
model: GroupActivity,
as: 'groupActivities',
separate: true,
order: [['orderId', 'ASC'], ['id', 'ASC']],
include: [
{
model: Group,
as: 'groupsGroupActivity'
},
{
model: PredefinedActivity,
as: 'groupPredefinedActivity',
include: [
{
model: PredefinedActivityImage,
as: 'images'
}
]
},
],
}
]
});
// Füge imageUrl zu jeder PredefinedActivity hinzu
const activitiesWithImages = await Promise.all(activities.map(async activity => {
// Konvertiere zu JSON und zurück, um alle Eigenschaften zu serialisieren
const activitiesWithImages = [];
for (const activity of activities) {
const activityData = activity.toJSON();
if (activityData.predefinedActivity) {
// Hole alle Images aus der Datenbank
const allImages = await PredefinedActivityImage.findAll({
where: { predefinedActivityId: activityData.predefinedActivity.id },
order: [['createdAt', 'ASC']]
});
// Konvertiere Images zu JSON und parse drawingData falls vorhanden
const imagesWithParsedData = allImages.map(img => {
const imgData = img.toJSON();
if (imgData.drawingData) {
try {
imgData.drawingData = JSON.parse(imgData.drawingData);
} catch (error) {
console.error(`Image ${imgData.id}: Error parsing drawingData:`, error);
// Load predefinedActivity if present
if (activityData.predefinedActivityId) {
const predefined = await PredefinedActivity.findByPk(activityData.predefinedActivityId);
activityData.predefinedActivity = predefined ? predefined.toJSON() : null;
if (activityData.predefinedActivity) {
const allImages = await PredefinedActivityImage.findAll({
where: { predefinedActivityId: activityData.predefinedActivity.id },
order: [['createdAt', 'ASC']]
});
const imagesWithParsedData = allImages.map(img => {
const imgData = img.toJSON();
if (imgData.drawingData) {
try { imgData.drawingData = JSON.parse(imgData.drawingData); } catch (err) { console.error(err); }
}
return imgData;
});
activityData.predefinedActivity.images = imagesWithParsedData;
const firstImage = allImages.length > 0 ? allImages[0] : null;
if (activityData.predefinedActivity.drawingData) {
try { if (typeof activityData.predefinedActivity.drawingData === 'string') activityData.predefinedActivity.drawingData = JSON.parse(activityData.predefinedActivity.drawingData); } catch (err) { console.error(err); }
} else if (firstImage && firstImage.drawingData) {
try { activityData.predefinedActivity.drawingData = JSON.parse(firstImage.drawingData); } catch (err) { console.error(err); }
}
return imgData;
});
// Setze images Array
activityData.predefinedActivity.images = imagesWithParsedData;
const firstImage = allImages.length > 0 ? allImages[0] : null;
// Füge Zeichnungsdaten hinzu, falls vorhanden
// Priorität: 1. drawingData direkt auf PredefinedActivity, 2. drawingData aus firstImage
if (activityData.predefinedActivity.drawingData) {
// drawingData ist bereits vorhanden (aus dem Model)
try {
if (typeof activityData.predefinedActivity.drawingData === 'string') {
activityData.predefinedActivity.drawingData = JSON.parse(activityData.predefinedActivity.drawingData);
}
} catch (error) {
console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData:`, error);
if (firstImage) {
activityData.predefinedActivity.imageUrl = `/api/predefined-activities/${activityData.predefinedActivity.id}/image/${firstImage.id}`;
activityData.predefinedActivity.imageLink = `/api/predefined-activities/${activityData.predefinedActivity.id}/image/${firstImage.id}`;
} else {
activityData.predefinedActivity.imageUrl = `/api/predefined-activities/${activityData.predefinedActivity.id}/image`;
activityData.predefinedActivity.imageLink = `/api/predefined-activities/${activityData.predefinedActivity.id}/image`;
}
} else if (firstImage && firstImage.drawingData) {
try {
activityData.predefinedActivity.drawingData = JSON.parse(firstImage.drawingData);
} catch (error) {
console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData from image:`, error);
}
}
if (firstImage) {
// Füge sowohl imageUrl als auch imageLink mit Image-ID hinzu
activityData.predefinedActivity.imageUrl = `/api/predefined-activities/${activityData.predefinedActivity.id}/image/${firstImage.id}`;
activityData.predefinedActivity.imageLink = `/api/predefined-activities/${activityData.predefinedActivity.id}/image/${firstImage.id}`;
} else {
// Fallback: Verwende den Basis-Pfad ohne Image-ID
activityData.predefinedActivity.imageUrl = `/api/predefined-activities/${activityData.predefinedActivity.id}/image`;
activityData.predefinedActivity.imageLink = `/api/predefined-activities/${activityData.predefinedActivity.id}/image`;
}
}
// Auch für GroupActivities
if (activityData.groupActivities && activityData.groupActivities.length > 0) {
const seenGroupActivityIds = new Set();
activityData.groupActivities = activityData.groupActivities.filter(groupActivity => {
if (!groupActivity || groupActivity.id === undefined || groupActivity.id === null) {
return false;
}
if (seenGroupActivityIds.has(groupActivity.id)) {
return false;
}
seenGroupActivityIds.add(groupActivity.id);
return true;
});
for (const groupActivity of activityData.groupActivities) {
if (groupActivity.groupPredefinedActivity) {
// Hole alle Images aus der Datenbank
const allImages = await PredefinedActivityImage.findAll({
where: { predefinedActivityId: groupActivity.groupPredefinedActivity.id },
order: [['createdAt', 'ASC']]
});
// Konvertiere Images zu JSON und parse drawingData falls vorhanden
const imagesWithParsedData = allImages.map(img => {
const imgData = img.toJSON();
if (imgData.drawingData) {
try {
imgData.drawingData = JSON.parse(imgData.drawingData);
} catch (error) {
console.error(`Image ${imgData.id}: Error parsing drawingData:`, error);
}
}
return imgData;
});
// Setze images Array
groupActivity.groupPredefinedActivity.images = imagesWithParsedData;
const firstImage = allImages.length > 0 ? allImages[0] : null;
// Füge Zeichnungsdaten hinzu, falls vorhanden
// Priorität: 1. drawingData direkt auf PredefinedActivity, 2. drawingData aus firstImage
if (groupActivity.groupPredefinedActivity.drawingData) {
try {
if (typeof groupActivity.groupPredefinedActivity.drawingData === 'string') {
groupActivity.groupPredefinedActivity.drawingData = JSON.parse(groupActivity.groupPredefinedActivity.drawingData);
}
} catch (error) {
console.error(`GroupActivity ${groupActivity.groupPredefinedActivity.id}: Error parsing drawingData:`, error);
}
} else if (firstImage && firstImage.drawingData) {
try {
groupActivity.groupPredefinedActivity.drawingData = JSON.parse(firstImage.drawingData);
} catch (error) {
console.error(`GroupActivity ${groupActivity.groupPredefinedActivity.id}: Error parsing drawingData from image:`, error);
}
}
if (firstImage) {
groupActivity.groupPredefinedActivity.imageUrl = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image/${firstImage.id}`;
groupActivity.groupPredefinedActivity.imageLink = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image/${firstImage.id}`;
// Load groupActivities separately
const groupActivities = await GroupActivity.findAll({
where: { diaryDateActivity: activityData.id },
order: [['orderId', 'ASC'], ['id', 'ASC']]
});
const groupActivitiesData = [];
for (const ga of groupActivities) {
const gad = ga.toJSON();
if (gad.groupId) {
const g = await Group.findByPk(gad.groupId);
gad.groupsGroupActivity = g ? g.toJSON() : null;
}
if (gad.customActivity) {
const gp = await PredefinedActivity.findByPk(gad.customActivity);
if (gp) {
const gpJson = gp.toJSON();
const gpImages = await PredefinedActivityImage.findAll({ where: { predefinedActivityId: gpJson.id }, order: [['createdAt','ASC']] });
gpJson.images = gpImages.map(i => { const ii = i.toJSON(); try { if (ii.drawingData) ii.drawingData = JSON.parse(ii.drawingData); } catch (e){}; return ii; });
const firstImg = gpImages[0];
if (firstImg) {
gpJson.imageUrl = `/api/predefined-activities/${gpJson.id}/image/${firstImg.id}`;
gpJson.imageLink = `/api/predefined-activities/${gpJson.id}/image/${firstImg.id}`;
} else {
groupActivity.groupPredefinedActivity.imageUrl = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image`;
groupActivity.groupPredefinedActivity.imageLink = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image`;
gpJson.imageUrl = `/api/predefined-activities/${gpJson.id}/image`;
gpJson.imageLink = `/api/predefined-activities/${gpJson.id}/image`;
}
gad.groupPredefinedActivity = gpJson;
}
}
groupActivitiesData.push(gad);
}
return activityData;
}));
activityData.groupActivities = groupActivitiesData;
activitiesWithImages.push(activityData);
}
return activitiesWithImages;
}

View File

@@ -5804,7 +5804,21 @@ private fun SettingsScreen(
.navigationBarsPadding()
.padding(horizontal = ScreenHorizontalPadding, vertical = 16.dp),
) {
Button(
onClick = {
dependencies.applicationScope.launch {
dependencies.clubManager.clearSelection()
}
},
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.heightIn(min = 56.dp),
) {
Text(tr("club.change", "Verein wechseln"), style = MaterialTheme.typography.h6)
}
Header(tr("mobile.more", "Mehr"))
DetailLine(tr("mobile.user", "Benutzer"), authState.username ?: "-")
DetailLine("Backend", dependencies.apiConfig.baseUrl)
DetailLine("Club", clubState.clubs.firstOrNull { it.id == clubState.currentClubId }?.name ?: "-")