Refactor error handling and localization in frontend components

This commit enhances the error handling and user interface of various frontend components by integrating localization support. It updates error messages and titles across multiple views and dialogs to utilize the translation function, ensuring a consistent user experience in different languages. Additionally, it refines the handling of error messages in the MyTischtennis account and member transfer settings, improving clarity and user feedback during operations.
This commit is contained in:
Torsten Schulz (local)
2025-11-16 20:48:31 +01:00
parent 9baa6bae01
commit d10b663dc1
89 changed files with 6012 additions and 1809 deletions

View File

@@ -0,0 +1,125 @@
# Fehlercode-System - Verwendungsanleitung
## Übersicht
Das Fehlercode-System ersetzt hardcodierte deutsche Fehlermeldungen durch strukturierte Fehlercodes, die im Frontend übersetzt werden.
## Backend-Verwendung
### 1. Fehlercode verwenden
```javascript
import HttpError from '../exceptions/HttpError.js';
import { ERROR_CODES, createError } from '../constants/errorCodes.js';
// Einfacher Fehlercode ohne Parameter
throw new HttpError(createError(ERROR_CODES.USER_NOT_FOUND), 404);
// Fehlercode mit Parametern
throw new HttpError(
createError(ERROR_CODES.MEMBER_NOT_FOUND, { memberId: 123 }),
404
);
// Oder direkt:
throw new HttpError(
{ code: ERROR_CODES.MEMBER_NOT_FOUND, params: { memberId: 123 } },
404
);
```
### 2. Legacy-Format (wird weiterhin unterstützt)
```javascript
// Alte Variante funktioniert noch:
throw new HttpError('Benutzer nicht gefunden', 404);
```
## Frontend-Verwendung
### 1. Fehlermeldungen automatisch übersetzen
Die `getSafeErrorMessage`-Funktion erkennt automatisch Fehlercodes:
```javascript
import { getSafeErrorMessage } from '../utils/errorMessages.js';
// In einer Vue-Komponente (Options API)
try {
await apiClient.post('/api/endpoint', data);
} catch (error) {
const message = getSafeErrorMessage(error, this.$t('errors.ERROR_UNKNOWN_ERROR'), this.$t);
await this.showInfo(this.$t('messages.error'), message, '', 'error');
}
// In einer Vue-Komponente (Composition API)
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
try {
await apiClient.post('/api/endpoint', data);
} catch (error) {
const message = getSafeErrorMessage(error, t('errors.ERROR_UNKNOWN_ERROR'), t);
await showInfo(t('messages.error'), message, '', 'error');
}
```
### 2. Dialog-Utils mit Übersetzung
```javascript
import { buildInfoConfig, safeErrorMessage } from '../utils/dialogUtils.js';
// Mit Übersetzungsfunktion
this.infoDialog = buildInfoConfig({
title: this.$t('messages.error'),
message: safeErrorMessage(error, this.$t('errors.ERROR_UNKNOWN_ERROR'), this.$t),
type: 'error'
}, this.$t);
```
## API-Response-Format
### Neues Format (mit Fehlercode):
```json
{
"success": false,
"code": "ERROR_MEMBER_NOT_FOUND",
"params": {
"memberId": 123
},
"error": "ERROR_MEMBER_NOT_FOUND" // Für Rückwärtskompatibilität
}
```
### Legacy-Format (wird weiterhin unterstützt):
```json
{
"success": false,
"message": "Mitglied nicht gefunden",
"error": "Mitglied nicht gefunden"
}
```
## Übersetzungen hinzufügen
1. **Backend**: Fehlercode in `backend/constants/errorCodes.js` definieren
2. **Frontend**: Übersetzung in `frontend/src/i18n/locales/de.json` unter `errors` hinzufügen
Beispiel:
```json
{
"errors": {
"ERROR_MEMBER_NOT_FOUND": "Mitglied nicht gefunden.",
"ERROR_MEMBER_NOT_FOUND_WITH_ID": "Mitglied mit ID {memberId} nicht gefunden."
}
}
```
## Migration bestehender Fehler
1. Hardcodierte Fehlermeldung identifizieren
2. Passenden Fehlercode in `errorCodes.js` finden oder erstellen
3. Backend-Code anpassen: `throw new HttpError(createError(ERROR_CODES.XXX), status)`
4. Übersetzung in `de.json` hinzufügen
5. Frontend-Code muss nicht geändert werden (automatische Erkennung)

View File

@@ -0,0 +1,120 @@
/**
* Fehlercodes für die API
* Diese Codes werden an das Frontend gesendet und dort übersetzt
*
* Format: { code: string, params?: object }
*
* Beispiel:
* - { code: 'ERROR_USER_NOT_FOUND' }
* - { code: 'ERROR_MEMBER_NOT_FOUND', params: { memberId: 123 } }
* - { code: 'ERROR_VALIDATION_FAILED', params: { field: 'email', value: 'invalid' } }
*/
export const ERROR_CODES = {
// Allgemeine Fehler
INTERNAL_SERVER_ERROR: 'ERROR_INTERNAL_SERVER_ERROR',
UNKNOWN_ERROR: 'ERROR_UNKNOWN_ERROR',
VALIDATION_FAILED: 'ERROR_VALIDATION_FAILED',
NOT_FOUND: 'ERROR_NOT_FOUND',
UNAUTHORIZED: 'ERROR_UNAUTHORIZED',
FORBIDDEN: 'ERROR_FORBIDDEN',
BAD_REQUEST: 'ERROR_BAD_REQUEST',
// Authentifizierung
USER_NOT_FOUND: 'ERROR_USER_NOT_FOUND',
INVALID_PASSWORD: 'ERROR_INVALID_PASSWORD',
LOGIN_FAILED: 'ERROR_LOGIN_FAILED',
SESSION_EXPIRED: 'ERROR_SESSION_EXPIRED',
// MyTischtennis
MYTISCHTENNIS_USER_NOT_FOUND: 'ERROR_MYTISCHTENNIS_USER_NOT_FOUND',
MYTISCHTENNIS_INVALID_PASSWORD: 'ERROR_MYTISCHTENNIS_INVALID_PASSWORD',
MYTISCHTENNIS_LOGIN_FAILED: 'ERROR_MYTISCHTENNIS_LOGIN_FAILED',
MYTISCHTENNIS_ACCOUNT_NOT_LINKED: 'ERROR_MYTISCHTENNIS_ACCOUNT_NOT_LINKED',
MYTISCHTENNIS_PASSWORD_NOT_SAVED: 'ERROR_MYTISCHTENNIS_PASSWORD_NOT_SAVED',
MYTISCHTENNIS_SESSION_EXPIRED: 'ERROR_MYTISCHTENNIS_SESSION_EXPIRED',
MYTISCHTENNIS_NO_PASSWORD_SAVED: 'ERROR_MYTISCHTENNIS_NO_PASSWORD_SAVED',
// Mitglieder
MEMBER_NOT_FOUND: 'ERROR_MEMBER_NOT_FOUND',
MEMBER_ALREADY_EXISTS: 'ERROR_MEMBER_ALREADY_EXISTS',
MEMBER_FIRSTNAME_REQUIRED: 'ERROR_MEMBER_FIRSTNAME_REQUIRED',
MEMBER_LASTNAME_REQUIRED: 'ERROR_MEMBER_LASTNAME_REQUIRED',
// Gruppen
GROUP_NOT_FOUND: 'ERROR_GROUP_NOT_FOUND',
GROUP_NAME_REQUIRED: 'ERROR_GROUP_NAME_REQUIRED',
GROUP_ALREADY_EXISTS: 'ERROR_GROUP_ALREADY_EXISTS',
GROUP_INVALID_PRESET_TYPE: 'ERROR_GROUP_INVALID_PRESET_TYPE',
GROUP_CANNOT_RENAME_PRESET: 'ERROR_GROUP_CANNOT_RENAME_PRESET',
// Turniere
TOURNAMENT_NOT_FOUND: 'ERROR_TOURNAMENT_NOT_FOUND',
TOURNAMENT_NO_DATE: 'ERROR_TOURNAMENT_NO_DATE',
TOURNAMENT_CLASS_NAME_REQUIRED: 'ERROR_TOURNAMENT_CLASS_NAME_REQUIRED',
TOURNAMENT_NO_PARTICIPANTS: 'ERROR_TOURNAMENT_NO_PARTICIPANTS',
TOURNAMENT_NO_VALID_PARTICIPANTS: 'ERROR_TOURNAMENT_NO_VALID_PARTICIPANTS',
TOURNAMENT_NO_TRAINING_DAY: 'ERROR_TOURNAMENT_NO_TRAINING_DAY',
TOURNAMENT_PDF_GENERATION_FAILED: 'ERROR_TOURNAMENT_PDF_GENERATION_FAILED',
TOURNAMENT_SELECT_FIRST: 'ERROR_TOURNAMENT_SELECT_FIRST',
// Trainingstagebuch
DIARY_DATE_NOT_FOUND: 'ERROR_DIARY_DATE_NOT_FOUND',
DIARY_DATE_UPDATED: 'ERROR_DIARY_DATE_UPDATED',
DIARY_NO_PARTICIPANTS: 'ERROR_DIARY_NO_PARTICIPANTS',
DIARY_PDF_GENERATION_FAILED: 'ERROR_DIARY_PDF_GENERATION_FAILED',
DIARY_IMAGE_LOAD_FAILED: 'ERROR_DIARY_IMAGE_LOAD_FAILED',
DIARY_STATS_LOAD_FAILED: 'ERROR_DIARY_STATS_LOAD_FAILED',
DIARY_NO_EXERCISE_DATA: 'ERROR_DIARY_NO_EXERCISE_DATA',
DIARY_ACTIVITY_PARTICIPANTS_UPDATE_FAILED: 'ERROR_DIARY_ACTIVITY_PARTICIPANTS_UPDATE_FAILED',
DIARY_GROUP_ASSIGNMENT_UPDATED: 'SUCCESS_DIARY_GROUP_ASSIGNMENT_UPDATED',
DIARY_GROUP_ASSIGNMENT_UPDATE_FAILED: 'ERROR_DIARY_GROUP_ASSIGNMENT_UPDATE_FAILED',
DIARY_ASSIGN_ALL_PARTICIPANTS_FAILED: 'ERROR_DIARY_ASSIGN_ALL_PARTICIPANTS_FAILED',
DIARY_ASSIGN_GROUP_FAILED: 'ERROR_DIARY_ASSIGN_GROUP_FAILED',
DIARY_PARTICIPANT_ASSIGN_FAILED: 'ERROR_DIARY_PARTICIPANT_ASSIGN_FAILED',
DIARY_PARTICIPANT_GROUP_ASSIGNMENT_UPDATE_FAILED: 'ERROR_DIARY_PARTICIPANT_GROUP_ASSIGNMENT_UPDATE_FAILED',
DIARY_MEMBER_CREATED: 'SUCCESS_DIARY_MEMBER_CREATED',
DIARY_MEMBER_CREATE_FAILED: 'ERROR_DIARY_MEMBER_CREATE_FAILED',
// Team Management
TEAM_NOT_LINKED_TO_LEAGUE: 'ERROR_TEAM_NOT_LINKED_TO_LEAGUE',
TEAM_LINK_TO_LEAGUE_REQUIRED: 'ERROR_TEAM_LINK_TO_LEAGUE_REQUIRED',
TEAM_PDF_LOAD_FAILED: 'ERROR_TEAM_PDF_LOAD_FAILED',
TEAM_STATS_LOAD_FAILED: 'ERROR_TEAM_STATS_LOAD_FAILED',
// Aktivitäten
ACTIVITY_IMAGE_DELETE_FAILED: 'ERROR_ACTIVITY_IMAGE_DELETE_FAILED',
// Offizielle Turniere
OFFICIAL_TOURNAMENT_PDF_UPLOAD_SUCCESS: 'SUCCESS_OFFICIAL_TOURNAMENT_PDF_UPLOAD',
OFFICIAL_TOURNAMENT_PDF_UPLOAD_FAILED: 'ERROR_OFFICIAL_TOURNAMENT_PDF_UPLOAD',
// Vereine
CLUB_NOT_FOUND: 'ERROR_CLUB_NOT_FOUND',
CLUB_ALREADY_EXISTS: 'ERROR_CLUB_ALREADY_EXISTS',
CLUB_NAME_REQUIRED: 'ERROR_CLUB_NAME_REQUIRED',
CLUB_NAME_TOO_SHORT: 'ERROR_CLUB_NAME_TOO_SHORT',
// Mitglieder-Übertragung
MEMBER_TRANSFER_BULK_FAILED: 'ERROR_MEMBER_TRANSFER_BULK_FAILED',
// Training
TRAINING_STATS_LOAD_FAILED: 'ERROR_TRAINING_STATS_LOAD_FAILED',
// Logs
LOG_NOT_FOUND: 'ERROR_LOG_NOT_FOUND',
};
/**
* Erstellt ein Fehler-Objekt mit Code und optionalen Parametern
* @param {string} code - Fehlercode aus ERROR_CODES
* @param {object} params - Optionale Parameter für die Fehlermeldung
* @returns {object} Fehler-Objekt mit code und params
*/
export function createError(code, params = null) {
return {
code,
...(params && { params })
};
}

View File

@@ -1,10 +1,54 @@
/**
* HttpError mit Unterstützung für Fehlercodes
*
* Verwendung:
* - new HttpError('Fehlermeldung', 400) - Legacy, wird weiterhin unterstützt
* - new HttpError({ code: 'ERROR_USER_NOT_FOUND' }, 404) - Mit Fehlercode
* - new HttpError({ code: 'ERROR_MEMBER_NOT_FOUND', params: { memberId: 123 } }, 404) - Mit Parametern
*/
class HttpError extends Error {
constructor(message, statusCode) {
super(message);
constructor(messageOrError, statusCode) {
// Unterstützung für beide Formate:
// 1. Legacy: new HttpError('Fehlermeldung', 400)
// 2. Neu: new HttpError({ code: 'ERROR_CODE', params: {...} }, 400)
if (typeof messageOrError === 'string') {
// Legacy-Format
super(messageOrError);
this.errorCode = null;
this.errorParams = null;
} else if (messageOrError && typeof messageOrError === 'object' && messageOrError.code) {
// Neues Format mit Fehlercode
super(messageOrError.code); // Für Stack-Trace
this.errorCode = messageOrError.code;
this.errorParams = messageOrError.params || null;
} else {
// Fallback
super('Unknown error');
this.errorCode = null;
this.errorParams = null;
}
this.name = this.constructor.name;
this.statusCode = statusCode;
this.statusCode = statusCode || 500;
Error.captureStackTrace(this, this.constructor);
}
/**
* Gibt das Fehler-Objekt für die API-Antwort zurück
* @returns {object} Fehler-Objekt mit code und optional params
*/
toJSON() {
if (this.errorCode) {
return {
code: this.errorCode,
...(this.errorParams && { params: this.errorParams })
};
}
// Legacy: Gib die Nachricht zurück
return {
message: this.message
};
}
}
export default HttpError;

View File

@@ -32,3 +32,4 @@ CREATE TABLE IF NOT EXISTS `member_training_group` (
CONSTRAINT `member_training_group_ibfk_2` FOREIGN KEY (`training_group_id`) REFERENCES `training_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -36,3 +36,4 @@ const MemberTrainingGroup = sequelize.define('MemberTrainingGroup', {
export default MemberTrainingGroup;

View File

@@ -47,3 +47,4 @@ const TrainingGroup = sequelize.define('TrainingGroup', {
export default TrainingGroup;

View File

@@ -167,3 +167,4 @@ migrateMyTischtennisEncryption()
process.exit(1);
});

View File

@@ -52,6 +52,7 @@ import trainingGroupRoutes from './routes/trainingGroupRoutes.js';
import trainingTimeRoutes from './routes/trainingTimeRoutes.js';
import schedulerService from './services/schedulerService.js';
import { requestLoggingMiddleware } from './middleware/requestLoggingMiddleware.js';
import HttpError from './exceptions/HttpError.js';
const app = express();
const port = process.env.PORT || 3005;
@@ -86,12 +87,25 @@ app.use((err, req, res, next) => {
}
const status = err?.statusCode || err?.status || 500;
const message = err?.message || 'Interner Serverfehler';
// Unterstützung für Fehlercodes
let errorResponse;
if (err instanceof HttpError && err.errorCode) {
// Neues Format mit Fehlercode
errorResponse = err.toJSON();
} else {
// Legacy-Format: String-Nachricht
const message = err?.message || 'Interner Serverfehler';
errorResponse = {
message
};
}
const response = {
success: false,
message,
error: message
...errorResponse,
// Für Rückwärtskompatibilität: error-Feld mit Nachricht
error: errorResponse.message || errorResponse.code || 'Interner Serverfehler'
};
if (process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development') {

View File

@@ -44,7 +44,7 @@ class MyTischtennisService {
// Verify user's app password
const user = await User.findByPk(userId);
if (!user) {
throw new HttpError('Benutzer nicht gefunden', 404);
throw new HttpError({ code: 'ERROR_USER_NOT_FOUND' }, 404);
}
let loginResult = null;