Fügt Unterstützung für Team-Dokumente hinzu. Aktualisiert die Backend-Modelle und -Routen, um Team-Dokumente zu verwalten, einschließlich Upload- und Parsing-Funktionen für Code- und Pin-Listen. Ergänzt die Benutzeroberfläche in TeamManagementView.vue zur Anzeige und Verwaltung von Team-Dokumenten sowie zur Integration von PDF-Parsing. Aktualisiert die Match-Modelle, um zusätzliche Felder für Spiel-Codes und PINs zu berücksichtigen.

This commit is contained in:
Torsten Schulz (local)
2025-10-02 09:04:19 +02:00
parent a6493990d3
commit 1c70ca97bb
15 changed files with 1973 additions and 19 deletions

View File

@@ -0,0 +1,233 @@
import multer from 'multer';
import path from 'path';
import TeamDocumentService from '../services/teamDocumentService.js';
import PDFParserService from '../services/pdfParserService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
// Multer-Konfiguration für Datei-Uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/temp/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 10 * 1024 * 1024 // 10MB Limit
},
fileFilter: (req, file, cb) => {
// Erlaube nur PDF, DOC, DOCX, TXT, CSV Dateien
const allowedTypes = /pdf|doc|docx|txt|csv/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('Nur PDF, DOC, DOCX, TXT und CSV Dateien sind erlaubt!'));
}
}
});
export const uploadMiddleware = upload.single('document');
export const uploadDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const { documentType } = req.body;
devLog('[uploadDocument] - Uploading document for club team:', clubTeamId, 'type:', documentType);
const user = await getUserByToken(token);
if (!req.file) {
return res.status(400).json({ error: "nofile" });
}
if (!documentType || !['code_list', 'pin_list'].includes(documentType)) {
return res.status(400).json({ error: "invaliddocumenttype" });
}
const document = await TeamDocumentService.uploadDocument(req.file, clubTeamId, documentType);
devLog('[uploadDocument] - Document uploaded successfully:', document.id);
res.status(201).json(document);
} catch (error) {
console.error('[uploadDocument] - Error:', error);
// Lösche temporäre Datei bei Fehler
if (req.file && req.file.path) {
try {
const fs = await import('fs');
fs.unlinkSync(req.file.path);
} catch (cleanupError) {
console.error('Fehler beim Löschen der temporären Datei:', cleanupError);
}
}
if (error.message === 'Club-Team nicht gefunden') {
return res.status(404).json({ error: "clubteamnotfound" });
}
res.status(500).json({ error: "internalerror" });
}
};
export const getDocuments = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
devLog('[getDocuments] - Getting documents for club team:', clubTeamId);
const user = await getUserByToken(token);
const documents = await TeamDocumentService.getDocumentsByClubTeam(clubTeamId);
devLog('[getDocuments] - Found documents:', documents.length);
res.status(200).json(documents);
} catch (error) {
console.error('[getDocuments] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
devLog('[getDocument] - Getting document:', documentId);
const user = await getUserByToken(token);
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(document);
} catch (error) {
console.error('[getDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const downloadDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
devLog('[downloadDocument] - Downloading document:', documentId);
const user = await getUserByToken(token);
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "notfound" });
}
const filePath = await TeamDocumentService.getDocumentPath(documentId);
if (!filePath) {
return res.status(404).json({ error: "filenotfound" });
}
// Prüfe ob Datei existiert
const fs = await import('fs');
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: "filenotfound" });
}
// Setze Headers für Inline-Anzeige (PDF-Viewer)
res.setHeader('Content-Disposition', `inline; filename="${document.originalFileName}"`);
res.setHeader('Content-Type', document.mimeType);
// Sende die Datei
res.sendFile(filePath);
} catch (error) {
console.error('[downloadDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
devLog('[deleteDocument] - Deleting document:', documentId);
const user = await getUserByToken(token);
const success = await TeamDocumentService.deleteDocument(documentId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "Document deleted successfully" });
} catch (error) {
console.error('[deleteDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const parsePDF = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const { leagueid: leagueId } = req.query;
devLog('[parsePDF] - Parsing PDF document:', documentId, 'league:', leagueId);
const user = await getUserByToken(token);
if (!leagueId) {
return res.status(400).json({ error: "missingleagueid" });
}
// Hole Dokument-Informationen
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "documentnotfound" });
}
// Prüfe ob es eine PDF- oder TXT-Datei ist
if (!document.mimeType.includes('pdf') && !document.mimeType.includes('text/plain')) {
return res.status(400).json({ error: "notapdfortxt" });
}
// Parse PDF
const parseResult = await PDFParserService.parsePDF(document.filePath, document.clubTeam.clubId);
// Speichere Matches in Datenbank
const saveResult = await PDFParserService.saveMatchesToDatabase(parseResult.matches, parseInt(leagueId));
devLog('[parsePDF] - PDF parsed successfully:', {
matchesFound: parseResult.matches.length,
created: saveResult.created,
updated: saveResult.updated,
errors: saveResult.errors.length
});
res.status(200).json({
message: "PDF parsed successfully",
parseResult: {
matchesFound: parseResult.matches.length,
errors: parseResult.errors,
metadata: parseResult.metadata,
debugInfo: parseResult.debugInfo,
allLines: parseResult.allLines,
rawText: parseResult.rawText
},
saveResult: {
created: saveResult.created,
updated: saveResult.updated,
errors: saveResult.errors
}
});
} catch (error) {
console.error('[parsePDF] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

View File

@@ -60,6 +60,21 @@ const Match = sequelize.define('Match', {
},
allowNull: false,
},
code: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Spiel-Code aus PDF-Parsing'
},
homePin: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Pin-Code für Heimteam aus PDF-Parsing'
},
guestPin: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Pin-Code für Gastteam aus PDF-Parsing'
},
}, {
underscored: true,
tableName: 'match',

View File

@@ -0,0 +1,52 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import ClubTeam from './ClubTeam.js';
const TeamDocument = sequelize.define('TeamDocument', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
fileName: {
type: DataTypes.STRING,
allowNull: false,
},
originalFileName: {
type: DataTypes.STRING,
allowNull: false,
},
filePath: {
type: DataTypes.STRING,
allowNull: false,
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: false,
},
mimeType: {
type: DataTypes.STRING,
allowNull: false,
},
documentType: {
type: DataTypes.ENUM('code_list', 'pin_list'),
allowNull: false,
},
clubTeamId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: ClubTeam,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
}, {
underscored: true,
tableName: 'team_document',
timestamps: true,
});
export default TeamDocument;

View File

@@ -20,6 +20,7 @@ import Match from './Match.js';
import League from './League.js';
import Team from './Team.js';
import ClubTeam from './ClubTeam.js';
import TeamDocument from './TeamDocument.js';
import Season from './Season.js';
import Location from './Location.js';
import Group from './Group.js';
@@ -135,6 +136,10 @@ ClubTeam.belongsTo(League, { foreignKey: 'leagueId', as: 'league' });
Season.hasMany(ClubTeam, { foreignKey: 'seasonId', as: 'clubTeams' });
ClubTeam.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
// TeamDocument relationships
ClubTeam.hasMany(TeamDocument, { foreignKey: 'clubTeamId', as: 'documents' });
TeamDocument.belongsTo(ClubTeam, { foreignKey: 'clubTeamId', as: 'clubTeam' });
Match.belongsTo(Location, { foreignKey: 'locationId', as: 'location' });
Location.hasMany(Match, { foreignKey: 'locationId', as: 'matches' });
@@ -246,6 +251,7 @@ export {
League,
Team,
ClubTeam,
TeamDocument,
Group,
GroupActivity,
Tournament,

View File

@@ -0,0 +1,33 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
uploadMiddleware,
uploadDocument,
getDocuments,
getDocument,
downloadDocument,
deleteDocument,
parsePDF
} from '../controllers/teamDocumentController.js';
const router = express.Router();
// Upload eines Dokuments für ein Club-Team
router.post('/club-team/:clubteamid/upload', authenticate, uploadMiddleware, uploadDocument);
// Alle Dokumente für ein Club-Team abrufen
router.get('/club-team/:clubteamid', authenticate, getDocuments);
// Ein spezifisches Dokument abrufen
router.get('/:documentid', authenticate, getDocument);
// Ein Dokument herunterladen
router.get('/:documentid/download', authenticate, downloadDocument);
// Ein Dokument löschen
router.delete('/:documentid', authenticate, deleteDocument);
// PDF parsen und Matches extrahieren
router.post('/:documentid/parse', authenticate, parsePDF);
export default router;

View File

@@ -6,7 +6,7 @@ import cors from 'cors';
import {
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, ClubTeam, Group,
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, ClubTeam, TeamDocument, Group,
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis
} from './models/index.js';
@@ -36,6 +36,7 @@ import officialTournamentRoutes from './routes/officialTournamentRoutes.js';
import myTischtennisRoutes from './routes/myTischtennisRoutes.js';
import teamRoutes from './routes/teamRoutes.js';
import clubTeamRoutes from './routes/clubTeamRoutes.js';
import teamDocumentRoutes from './routes/teamDocumentRoutes.js';
import seasonRoutes from './routes/seasonRoutes.js';
const app = express();
@@ -84,6 +85,7 @@ app.use('/api/official-tournaments', officialTournamentRoutes);
app.use('/api/mytischtennis', myTischtennisRoutes);
app.use('/api/teams', teamRoutes);
app.use('/api/club-teams', clubTeamRoutes);
app.use('/api/team-documents', teamDocumentRoutes);
app.use('/api/seasons', seasonRoutes);
app.use(express.static(path.join(__dirname, '../frontend/dist')));

View File

@@ -171,6 +171,9 @@ class MatchService {
guestTeamId: match.guestTeamId,
locationId: match.locationId,
leagueId: match.leagueId,
code: match.code,
homePin: match.homePin,
guestPin: match.guestPin,
homeTeam: { name: 'Unbekannt' },
guestTeam: { name: 'Unbekannt' },
location: { name: 'Unbekannt', address: '', city: '', zip: '' },
@@ -228,6 +231,9 @@ class MatchService {
guestTeamId: match.guestTeamId,
locationId: match.locationId,
leagueId: match.leagueId,
code: match.code,
homePin: match.homePin,
guestPin: match.guestPin,
homeTeam: { name: 'Unbekannt' },
guestTeam: { name: 'Unbekannt' },
location: { name: 'Unbekannt', address: '', city: '', zip: '' },

View File

@@ -0,0 +1,690 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pdfParse = require('pdf-parse/lib/pdf-parse.js');
import { Op } from 'sequelize';
import Match from '../models/Match.js';
import Team from '../models/Team.js';
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Location from '../models/Location.js';
import { devLog } from '../utils/logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class PDFParserService {
/**
* Parst eine PDF-Datei und extrahiert Spiel-Daten
* @param {string} filePath - Pfad zur PDF-Datei
* @param {number} clubId - ID des Vereins
* @returns {Promise<Object>} Geparste Spiel-Daten
*/
static async parsePDF(filePath, clubId) {
try {
devLog('[PDFParserService.parsePDF] - Parsing PDF:', filePath);
if (!fs.existsSync(filePath)) {
throw new Error('PDF-Datei nicht gefunden');
}
// Bestimme Dateityp basierend auf Dateiendung
const fileExtension = path.extname(filePath).toLowerCase();
let fileContent;
if (fileExtension === '.pdf') {
// Echte PDF-Parsing
const pdfBuffer = fs.readFileSync(filePath);
const pdfData = await pdfParse(pdfBuffer);
fileContent = pdfData.text;
devLog('[PDFParserService.parsePDF] - PDF parsed, text length:', fileContent.length);
} else {
// Fallback für TXT-Dateien (für Tests)
fileContent = fs.readFileSync(filePath, 'utf8');
devLog('[PDFParserService.parsePDF] - Text file loaded, text length:', fileContent.length);
}
// Parse den Text nach Spiel-Daten
const parsedData = this.extractMatchData(fileContent, clubId);
devLog('[PDFParserService.parsePDF] - Extracted matches:', parsedData.matches.length);
return parsedData;
} catch (error) {
console.error('[PDFParserService.parsePDF] - Error:', error);
throw error;
}
}
/**
* Extrahiert Spiel-Daten aus dem PDF-Text
* @param {string} text - Der extrahierte Text aus der PDF
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Daten mit Matches und Metadaten
*/
static extractMatchData(text, clubId) {
const matches = [];
const errors = [];
const metadata = {
totalLines: 0,
parsedMatches: 0,
errors: 0
};
try {
// Teile Text in Zeilen auf
const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
metadata.totalLines = lines.length;
devLog('[PDFParserService.extractMatchData] - Processing lines:', lines.length);
// Verschiedene Parsing-Strategien je nach PDF-Format
const strategies = [
{ name: 'Standard Format', fn: this.parseStandardFormat },
{ name: 'Table Format', fn: this.parseTableFormat },
{ name: 'List Format', fn: this.parseListFormat }
];
devLog('[PDFParserService.extractMatchData] - Trying parsing strategies...');
for (const strategy of strategies) {
try {
devLog(`[PDFParserService.extractMatchData] - Trying ${strategy.name}...`);
const result = strategy.fn(lines, clubId);
devLog(`[PDFParserService.extractMatchData] - ${strategy.name} found ${result.matches.length} matches`);
if (result.matches.length > 0) {
matches.push(...result.matches);
metadata.parsedMatches += result.matches.length;
devLog(`[PDFParserService.extractMatchData] - Using ${strategy.name} strategy`);
break; // Erste erfolgreiche Strategie verwenden
}
} catch (strategyError) {
devLog(`[PDFParserService.extractMatchData] - ${strategy.name} failed:`, strategyError.message);
errors.push(`Strategy ${strategy.name} failed: ${strategyError.message}`);
}
}
metadata.errors = errors.length;
return {
matches,
errors,
metadata,
rawText: text.substring(0, 1000), // Erste 1000 Zeichen für Debugging
allLines: lines, // Alle Zeilen für Debugging
debugInfo: {
totalTextLength: text.length,
totalLines: lines.length,
firstFewLines: lines.slice(0, 10),
lastFewLines: lines.slice(-5)
}
};
} catch (error) {
console.error('[PDFParserService.extractMatchData] - Error:', error);
throw error;
}
}
/**
* Standard-Format Parser (Datum, Zeit, Heimteam, Gastteam, Code, Pins)
* @param {Array} lines - Textzeilen
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Matches
*/
static parseStandardFormat(lines, clubId) {
const matches = [];
devLog('[PDFParserService.parseStandardFormat] - Starting standard format parsing...');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Suche nach Datum-Pattern (dd.mm.yyyy oder dd/mm/yyyy)
const dateMatch = line.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
if (dateMatch) {
devLog(`[PDFParserService.parseStandardFormat] - Found date in line ${i + 1}: "${line}"`);
// Debug: Zeige die gesamte Zeile mit sichtbaren Whitespaces
const debugLine = line.replace(/\s/g, (match) => {
if (match === ' ') return '·'; // Mittelpunkt für normales Leerzeichen
if (match === '\t') return '→'; // Pfeil für Tab
if (match === '\n') return '↵'; // Enter-Zeichen
if (match === '\r') return '⏎'; // Carriage Return
return `[${match.charCodeAt(0)}]`; // Zeichencode für andere Whitespaces
});
devLog(`[PDFParserService.parseStandardFormat] - Full line with visible whitespaces: "${debugLine}"`);
try {
const [, day, month, year] = dateMatch;
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`);
// Suche nach Zeit-Pattern direkt nach dem Datum (hh:mm) - Format: Wt.dd.mm.yyyyhh:MM
const timeMatch = line.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})(\d{1,2}):(\d{2})/);
let time = null;
if (timeMatch) {
time = `${timeMatch[4].padStart(2, '0')}:${timeMatch[5]}`;
}
devLog(`[PDFParserService.parseStandardFormat] - Parsed date: ${date.toISOString().split('T')[0]}, time: ${time}`);
// Entferne Datum und Zeit vom Anfang der Zeile
const cleanLine = line.replace(/^[A-Za-z]{2}\.(\d{1,2})[./](\d{1,2})[./](\d{4})(\d{1,2}):(\d{2})\s*/, '');
devLog(`[PDFParserService.parseStandardFormat] - Clean line after date/time removal: "${cleanLine}"`);
// Entferne Nummerierung am Anfang (z.B. "(1)")
const cleanLine2 = cleanLine.replace(/^\(\d+\)/, '');
devLog(`[PDFParserService.parseStandardFormat] - Clean line after numbering removal: "${cleanLine2}"`);
// Entferne alle Inhalte in Klammern (z.B. "(J11)")
const cleanLine3 = cleanLine2.replace(/\([^)]*\)/g, '');
devLog(`[PDFParserService.parseStandardFormat] - Clean line after removing brackets: "${cleanLine3}"`);
// Suche nach Code (12 Zeichen) oder PIN (4 Ziffern) am Ende
const codeMatch = cleanLine3.match(/([A-Z0-9]{12})$/);
const pinMatch = cleanLine3.match(/(\d{4})$/);
let code = null;
let homePin = null;
let guestPin = null;
let teamsPart = cleanLine3;
if (codeMatch) {
// Code gefunden (12 Zeichen)
code = codeMatch[1];
teamsPart = cleanLine3.substring(0, cleanLine3.length - code.length).trim();
devLog(`[PDFParserService.parseStandardFormat] - Found code: "${code}"`);
} else if (pinMatch) {
// PIN gefunden (4 Ziffern)
const pin = pinMatch[1];
teamsPart = cleanLine3.substring(0, cleanLine3.length - pin.length).trim();
// PIN gehört zu dem Team, das direkt vor der PIN steht
// Analysiere die Position der PIN in der ursprünglichen Zeile
const pinIndex = cleanLine3.lastIndexOf(pin);
const teamsPartIndex = cleanLine3.indexOf(teamsPart);
// Wenn PIN direkt nach dem Teams-Part steht, gehört sie zur Heimmannschaft
// Wenn PIN zwischen den Teams steht, gehört sie zur Gastmannschaft
if (pinIndex === teamsPartIndex + teamsPart.length) {
// PIN steht direkt nach den Teams -> Heimmannschaft
homePin = pin;
devLog(`[PDFParserService.parseStandardFormat] - Found PIN: "${pin}" -> Home team (at end)`);
} else {
// PIN steht zwischen den Teams -> Gastmannschaft
guestPin = pin;
devLog(`[PDFParserService.parseStandardFormat] - Found PIN: "${pin}" -> Guest team (between teams)`);
}
}
if (code || pinMatch) {
devLog(`[PDFParserService.parseStandardFormat] - Teams part: "${teamsPart}"`);
// Debug: Zeige Whitespaces als lesbare Zeichen
const debugTeamsPart = teamsPart.replace(/\s/g, (match) => {
if (match === ' ') return '·'; // Mittelpunkt für normales Leerzeichen
if (match === '\t') return '→'; // Pfeil für Tab
return `[${match.charCodeAt(0)}]`; // Zeichencode für andere Whitespaces
});
devLog(`[PDFParserService.parseStandardFormat] - Teams part with visible whitespaces: "${debugTeamsPart}"`);
// Neue Strategie: Teile die Zeile durch mehrere Leerzeichen (wie in der Tabelle)
// Die Struktur ist: Heimmannschaft Gastmannschaft Code
const parts = teamsPart.split(/\s{2,}/); // Mindestens 2 Leerzeichen als Trenner
devLog(`[PDFParserService.parseStandardFormat] - Split by multiple spaces:`, parts);
let homeTeamName = '';
let guestTeamName = '';
if (parts.length >= 2) {
homeTeamName = parts[0].trim();
guestTeamName = parts[1].trim();
// Entferne noch verbleibende Klammern aus den Team-Namen
homeTeamName = homeTeamName.replace(/\([^)]*\)/g, '').trim();
guestTeamName = guestTeamName.replace(/\([^)]*\)/g, '').trim();
devLog(`[PDFParserService.parseStandardFormat] - After bracket removal: "${homeTeamName}" vs "${guestTeamName}"`);
// Erkenne römische Ziffern am Ende der Team-Namen
// Römische Ziffern: I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, etc.
const romanNumeralPattern = /\s+(I{1,3}|IV|V|VI{0,3}|IX|X|XI{0,2})$/;
// Prüfe Heimteam auf römische Ziffern
const homeRomanMatch = homeTeamName.match(romanNumeralPattern);
if (homeRomanMatch) {
const romanNumeral = homeRomanMatch[1];
const baseName = homeTeamName.replace(romanNumeralPattern, '').trim();
homeTeamName = `${baseName} ${romanNumeral}`;
devLog(`[PDFParserService.parseStandardFormat] - Home team roman numeral found: "${romanNumeral}" -> "${homeTeamName}"`);
}
// Prüfe Gastteam auf römische Ziffern
const guestRomanMatch = guestTeamName.match(romanNumeralPattern);
if (guestRomanMatch) {
const romanNumeral = guestRomanMatch[1];
const baseName = guestTeamName.replace(romanNumeralPattern, '').trim();
guestTeamName = `${baseName} ${romanNumeral}`;
devLog(`[PDFParserService.parseStandardFormat] - Guest team roman numeral found: "${romanNumeral}" -> "${guestTeamName}"`);
}
devLog(`[PDFParserService.parseStandardFormat] - Final teams: "${homeTeamName}" vs "${guestTeamName}"`);
} else {
// Fallback: Versuche mit einzelnen Leerzeichen zu trennen
devLog(`[PDFParserService.parseStandardFormat] - Fallback: trying single space split`);
// Strategie 1: Suche nach "Harheimer TC" als Heimteam
if (teamsPart.includes('Harheimer TC')) {
const harheimerIndex = teamsPart.indexOf('Harheimer TC');
homeTeamName = 'Harheimer TC';
guestTeamName = teamsPart.substring(harheimerIndex + 'Harheimer TC'.length).trim();
// Entferne Klammern aus Gastteam
guestTeamName = guestTeamName.replace(/\([^)]*\)/g, '').trim();
devLog(`[PDFParserService.parseStandardFormat] - Fallback Strategy 1: "${homeTeamName}" vs "${guestTeamName}"`);
} else {
// Strategie 2: Suche nach Großbuchstaben am Anfang des zweiten Teams
const teamSplitMatch = teamsPart.match(/^([A-Za-z0-9\s\-\.]+?)\s+([A-Z][A-Za-z0-9\s\-\.]+)$/);
if (teamSplitMatch) {
homeTeamName = teamSplitMatch[1].trim();
guestTeamName = teamSplitMatch[2].trim();
devLog(`[PDFParserService.parseStandardFormat] - Fallback Strategy 2: Split teams: "${homeTeamName}" vs "${guestTeamName}"`);
} else {
devLog(`[PDFParserService.parseStandardFormat] - Could not split teams from: "${teamsPart}"`);
continue;
}
}
}
if (homeTeamName && guestTeamName) {
let debugInfo;
if (code) {
debugInfo = `code: "${code}"`;
} else if (homePin && guestPin) {
debugInfo = `homePin: "${homePin}", guestPin: "${guestPin}"`;
} else if (homePin) {
debugInfo = `homePin: "${homePin}"`;
} else if (guestPin) {
debugInfo = `guestPin: "${guestPin}"`;
}
devLog(`[PDFParserService.parseStandardFormat] - Final parsed teams: "${homeTeamName}" vs "${guestTeamName}", ${debugInfo}`);
matches.push({
date: date,
time: time,
homeTeamName: homeTeamName,
guestTeamName: guestTeamName,
code: code,
homePin: homePin,
guestPin: guestPin,
clubId: clubId,
rawLine: line
});
}
} else {
devLog(`[PDFParserService.parseStandardFormat] - Could not find code pattern in: "${cleanLine3}"`);
}
} catch (parseError) {
devLog('[PDFParserService.parseStandardFormat] - Parse error for line:', line, parseError.message);
}
}
}
return { matches };
}
/**
* Tabellen-Format Parser
* @param {Array} lines - Textzeilen
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Matches
*/
static parseTableFormat(lines, clubId) {
const matches = [];
// Suche nach Tabellen-Header
let headerIndex = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].toLowerCase().includes('datum') &&
lines[i].toLowerCase().includes('zeit') &&
lines[i].toLowerCase().includes('heim') &&
lines[i].toLowerCase().includes('gast')) {
headerIndex = i;
break;
}
}
if (headerIndex >= 0) {
// Parse Tabellen-Zeilen
for (let i = headerIndex + 1; i < lines.length; i++) {
const line = lines[i];
const columns = line.split(/\s{2,}|\t/); // Split bei mehreren Leerzeichen oder Tabs
if (columns.length >= 4) {
try {
const dateStr = columns[0];
const timeStr = columns[1];
const homeTeam = columns[2];
const guestTeam = columns[3];
const code = columns[4] || null;
const homePin = columns[5] || null;
const guestPin = columns[6] || null;
// Parse Datum
const dateMatch = dateStr.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
if (dateMatch) {
const [, day, month, year] = dateMatch;
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`);
matches.push({
date: date,
time: timeStr || null,
homeTeamName: homeTeam.trim(),
guestTeamName: guestTeam.trim(),
code: code ? code.trim() : null,
homePin: homePin ? homePin.trim() : null,
guestPin: guestPin ? guestPin.trim() : null,
clubId: clubId,
rawLine: line
});
}
} catch (parseError) {
devLog('[PDFParserService.parseTableFormat] - Parse error for line:', line, parseError.message);
}
}
}
}
return { matches };
}
/**
* Listen-Format Parser
* @param {Array} lines - Textzeilen
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Matches
*/
static parseListFormat(lines, clubId) {
const matches = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Suche nach Nummerierten Listen (1., 2., etc.)
const listMatch = line.match(/^\d+\.\s*(.+)/);
if (listMatch) {
const content = listMatch[1];
// Versuche verschiedene Formate zu parsen
const patterns = [
/(\d{1,2}[./]\d{1,2}[./]\d{4})\s+(\d{1,2}:\d{2})?\s+(.+?)\s+vs?\s+(.+?)(?:\s+code[:\s]*([A-Za-z0-9]+))?(?:\s+home[:\s]*pin[:\s]*([A-Za-z0-9]+))?(?:\s+guest[:\s]*pin[:\s]*([A-Za-z0-9]+))?/i,
/(\d{1,2}[./]\d{1,2}[./]\d{4})\s+(\d{1,2}:\d{2})?\s+(.+?)\s+-\s+(.+?)(?:\s+code[:\s]*([A-Za-z0-9]+))?(?:\s+heim[:\s]*pin[:\s]*([A-Za-z0-9]+))?(?:\s+gast[:\s]*pin[:\s]*([A-Za-z0-9]+))?/i
];
for (const pattern of patterns) {
const match = content.match(pattern);
if (match) {
try {
const [, dateStr, timeStr, homeTeam, guestTeam, code, homePin, guestPin] = match;
// Parse Datum
const dateMatch = dateStr.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
if (dateMatch) {
const [, day, month, year] = dateMatch;
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`);
matches.push({
date: date,
time: timeStr || null,
homeTeamName: homeTeam.trim(),
guestTeamName: guestTeam.trim(),
code: code ? code.trim() : null,
homePin: homePin ? homePin.trim() : null,
guestPin: guestPin ? guestPin.trim() : null,
clubId: clubId,
rawLine: line
});
break; // Erste erfolgreiche Pattern verwenden
}
} catch (parseError) {
devLog('[PDFParserService.parseListFormat] - Parse error for line:', line, parseError.message);
}
}
}
}
}
return { matches };
}
/**
* Speichert geparste Matches in der Datenbank
* @param {Array} matches - Array von Match-Objekten
* @param {number} leagueId - ID der Liga
* @returns {Promise<Object>} Ergebnis der Speicherung
*/
static async saveMatchesToDatabase(matches, leagueId) {
try {
devLog('[PDFParserService.saveMatchesToDatabase] - Saving matches:', matches.length);
const results = {
created: 0,
updated: 0,
errors: []
};
for (const matchData of matches) {
try {
let debugInfo;
if (matchData.code) {
debugInfo = `Code: ${matchData.code}`;
} else if (matchData.homePin && matchData.guestPin) {
debugInfo = `HomePin: ${matchData.homePin}, GuestPin: ${matchData.guestPin}`;
} else if (matchData.homePin) {
debugInfo = `HomePin: ${matchData.homePin}`;
} else if (matchData.guestPin) {
debugInfo = `GuestPin: ${matchData.guestPin}`;
}
devLog(`[PDFParserService.saveMatchesToDatabase] - Processing match: ${matchData.date.toISOString().split('T')[0]} ${matchData.time} - ${matchData.homeTeamName} vs ${matchData.guestTeamName} (${debugInfo})`);
// Lade alle Matches für das Datum und die Liga
console.log('matchData', matchData);
// Konvertiere das Datum zu einem Datum ohne Zeit für den Vergleich
const dateOnly = new Date(matchData.date.getFullYear(), matchData.date.getMonth(), matchData.date.getDate());
const nextDay = new Date(dateOnly);
nextDay.setDate(nextDay.getDate() + 1);
const existingMatches = await Match.findAll({
where: {
date: {
[Op.gte]: dateOnly, // Größer oder gleich dem Datum
[Op.lt]: nextDay // Kleiner als der nächste Tag
},
leagueId: leagueId,
...(matchData.time && { time: matchData.time }) // Füge Zeit hinzu wenn vorhanden
},
include: [
{
model: Team,
as: 'homeTeam',
attributes: ['id', 'name']
},
{
model: Team,
as: 'guestTeam',
attributes: ['id', 'name']
}
]
});
console.log('existingMatches', JSON.parse(JSON.stringify(existingMatches, null, 2)));
const timeFilter = matchData.time ? ` and time ${matchData.time}` : '';
devLog(`[PDFParserService.saveMatchesToDatabase] - Found ${existingMatches.length} existing matches for date ${matchData.date.toISOString().split('T')[0]}${timeFilter} and league ${leagueId}`);
// Debug: Zeige alle gefundenen Matches und lade Teams manuell
for (let i = 0; i < existingMatches.length; i++) {
const match = existingMatches[i];
// Lade Teams manuell
const homeTeam = await Team.findByPk(match.homeTeamId);
const guestTeam = await Team.findByPk(match.guestTeamId);
devLog(`[PDFParserService.saveMatchesToDatabase] - Match ${i + 1}: ${homeTeam?.name || 'Unknown'} vs ${guestTeam?.name || 'Unknown'} (${match.time || 'no time'})`);
// Füge die Teams zum Match-Objekt hinzu
match.homeTeam = homeTeam;
match.guestTeam = guestTeam;
}
// Suche nach dem passenden Match basierend auf Gastmannschaft
const matchingMatch = existingMatches.find(match => {
if (!match.guestTeam) return false;
const guestTeamName = match.guestTeam.name.toLowerCase();
const searchGuestName = matchData.guestTeamName.toLowerCase();
// Exakte Übereinstimmung oder Teilstring-Match
return guestTeamName === searchGuestName ||
guestTeamName.includes(searchGuestName) ||
searchGuestName.includes(guestTeamName);
});
if (matchingMatch) {
devLog(`[PDFParserService.saveMatchesToDatabase] - Found matching match: ${matchingMatch.homeTeam.name} vs ${matchingMatch.guestTeam.name}`);
// Update das bestehende Match mit Code und Pins
// Erstelle Update-Objekt nur mit vorhandenen Feldern
const updateData = {};
if (matchData.code) {
updateData.code = matchData.code;
}
if (matchData.homePin) {
updateData.homePin = matchData.homePin;
}
if (matchData.guestPin) {
updateData.guestPin = matchData.guestPin;
}
await matchingMatch.update(updateData);
results.updated++;
let updateInfo;
if (matchData.code) {
updateInfo = `code: ${matchData.code}`;
} else if (matchData.homePin && matchData.guestPin) {
updateInfo = `homePin: ${matchData.homePin}, guestPin: ${matchData.guestPin}`;
} else if (matchData.homePin) {
updateInfo = `homePin: ${matchData.homePin}`;
} else if (matchData.guestPin) {
updateInfo = `guestPin: ${matchData.guestPin}`;
}
devLog(`[PDFParserService.saveMatchesToDatabase] - Updated match with ${updateInfo}`);
// Lade das aktualisierte Match neu, um die aktuellen Werte zu zeigen
await matchingMatch.reload();
const currentValues = [];
if (matchingMatch.code) currentValues.push(`code: ${matchingMatch.code}`);
if (matchingMatch.homePin) currentValues.push(`homePin: ${matchingMatch.homePin}`);
if (matchingMatch.guestPin) currentValues.push(`guestPin: ${matchingMatch.guestPin}`);
devLog(`[PDFParserService.saveMatchesToDatabase] - Match now has: ${currentValues.join(', ')}`);
} else {
devLog(`[PDFParserService.saveMatchesToDatabase] - No matching match found for guest team: "${matchData.guestTeamName}"`);
// Fallback: Versuche Teams direkt zu finden
const homeTeam = await Team.findOne({
where: {
name: matchData.homeTeamName,
clubId: matchData.clubId
}
});
const guestTeam = await Team.findOne({
where: {
name: matchData.guestTeamName,
clubId: matchData.clubId
}
});
// Debug: Zeige alle verfügbaren Teams für diesen Club
if (!homeTeam || !guestTeam) {
const allTeams = await Team.findAll({
where: { clubId: matchData.clubId },
attributes: ['id', 'name']
});
devLog(`[PDFParserService.saveMatchesToDatabase] - Available teams for club ${matchData.clubId}:`, allTeams.map(t => t.name));
devLog(`[PDFParserService.saveMatchesToDatabase] - Looking for home team: "${matchData.homeTeamName}"`);
devLog(`[PDFParserService.saveMatchesToDatabase] - Looking for guest team: "${matchData.guestTeamName}"`);
// Versuche Fuzzy-Matching für Team-Namen
const homeTeamFuzzy = allTeams.find(t =>
t.name.toLowerCase().includes(matchData.homeTeamName.toLowerCase()) ||
matchData.homeTeamName.toLowerCase().includes(t.name.toLowerCase())
);
const guestTeamFuzzy = allTeams.find(t =>
t.name.toLowerCase().includes(matchData.guestTeamName.toLowerCase()) ||
matchData.guestTeamName.toLowerCase().includes(t.name.toLowerCase())
);
if (homeTeamFuzzy) {
devLog(`[PDFParserService.saveMatchesToDatabase] - Fuzzy match found for home team: "${homeTeamFuzzy.name}"`);
}
if (guestTeamFuzzy) {
devLog(`[PDFParserService.saveMatchesToDatabase] - Fuzzy match found for guest team: "${guestTeamFuzzy.name}"`);
}
}
if (!homeTeam || !guestTeam) {
let errorInfo;
if (matchData.code) {
errorInfo = `Code: ${matchData.code}`;
} else if (matchData.homePin && matchData.guestPin) {
errorInfo = `HomePin: ${matchData.homePin}, GuestPin: ${matchData.guestPin}`;
} else if (matchData.homePin) {
errorInfo = `HomePin: ${matchData.homePin}`;
} else if (matchData.guestPin) {
errorInfo = `GuestPin: ${matchData.guestPin}`;
}
results.errors.push(`Teams nicht gefunden: "${matchData.homeTeamName}" oder "${matchData.guestTeamName}" (Datum: ${matchData.date.toISOString().split('T')[0]}, Zeit: ${matchData.time}, ${errorInfo})`);
continue;
}
// Erstelle neues Match (Fallback)
await Match.create({
date: matchData.date,
time: matchData.time,
homeTeamId: homeTeam.id,
guestTeamId: guestTeam.id,
leagueId: leagueId,
clubId: matchData.clubId,
code: matchData.code,
homePin: matchData.homePin,
guestPin: matchData.guestPin,
locationId: 1 // Default Location, kann später angepasst werden
});
results.created++;
devLog(`[PDFParserService.saveMatchesToDatabase] - Created new match: ${matchData.homeTeamName} vs ${matchData.guestTeamName}`);
}
} catch (matchError) {
console.error('[PDFParserService.saveMatchesToDatabase] - Error:', matchError);
results.errors.push(`Fehler beim Speichern von Match: ${matchData.rawLine} - ${matchError.message}`);
}
}
devLog('[PDFParserService.saveMatchesToDatabase] - Results:', results);
return results;
} catch (error) {
console.error('[PDFParserService.saveMatchesToDatabase] - Error:', error);
throw error;
}
}
}
export default PDFParserService;

View File

@@ -0,0 +1,204 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import TeamDocument from '../models/TeamDocument.js';
import ClubTeam from '../models/ClubTeam.js';
import { devLog } from '../utils/logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class TeamDocumentService {
/**
* Speichert ein hochgeladenes Dokument für ein Club-Team
* @param {Object} file - Das hochgeladene File-Objekt (von multer)
* @param {number} clubTeamId - Die ID des Club-Teams
* @param {string} documentType - Der Typ des Dokuments ('code_list' oder 'pin_list')
* @returns {Promise<TeamDocument>} Das erstellte TeamDocument
*/
static async uploadDocument(file, clubTeamId, documentType) {
try {
devLog('[TeamDocumentService.uploadDocument] - Uploading document:', {
fileName: file.originalname,
clubTeamId,
documentType,
size: file.size
});
// Prüfe ob das Club-Team existiert
const clubTeam = await ClubTeam.findByPk(clubTeamId);
if (!clubTeam) {
throw new Error('Club-Team nicht gefunden');
}
// Generiere einen eindeutigen Dateinamen
const fileExtension = path.extname(file.originalname);
const uniqueFileName = `${clubTeamId}_${documentType}_${Date.now()}${fileExtension}`;
// Zielverzeichnis für Team-Dokumente
const uploadDir = path.join(__dirname, '..', 'uploads', 'team-documents');
// Erstelle Upload-Verzeichnis falls es nicht existiert
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const filePath = path.join(uploadDir, uniqueFileName);
// Verschiebe die Datei vom temporären Verzeichnis zum finalen Speicherort
fs.renameSync(file.path, filePath);
// Lösche alte Dokumente des gleichen Typs für dieses Team
await this.deleteDocumentsByType(clubTeamId, documentType);
// Erstelle Datenbankeintrag
const teamDocument = await TeamDocument.create({
fileName: uniqueFileName,
originalFileName: file.originalname,
filePath: filePath,
fileSize: file.size,
mimeType: file.mimetype,
documentType: documentType,
clubTeamId: clubTeamId
});
devLog('[TeamDocumentService.uploadDocument] - Document uploaded successfully:', teamDocument.id);
return teamDocument;
} catch (error) {
console.error('[TeamDocumentService.uploadDocument] - Error:', error);
throw error;
}
}
/**
* Holt alle Dokumente für ein Club-Team
* @param {number} clubTeamId - Die ID des Club-Teams
* @returns {Promise<Array<TeamDocument>>} Liste der Dokumente
*/
static async getDocumentsByClubTeam(clubTeamId) {
try {
devLog('[TeamDocumentService.getDocumentsByClubTeam] - Getting documents for club team:', clubTeamId);
const documents = await TeamDocument.findAll({
where: { clubTeamId },
order: [['createdAt', 'DESC']]
});
devLog('[TeamDocumentService.getDocumentsByClubTeam] - Found documents:', documents.length);
return documents;
} catch (error) {
console.error('[TeamDocumentService.getDocumentsByClubTeam] - Error:', error);
throw error;
}
}
/**
* Holt ein spezifisches Dokument
* @param {number} documentId - Die ID des Dokuments
* @returns {Promise<TeamDocument|null>} Das Dokument oder null
*/
static async getDocumentById(documentId) {
try {
devLog('[TeamDocumentService.getDocumentById] - Getting document:', documentId);
const document = await TeamDocument.findByPk(documentId, {
include: [{
model: ClubTeam,
as: 'clubTeam',
attributes: ['id', 'name', 'clubId']
}]
});
devLog('[TeamDocumentService.getDocumentById] - Found document:', document ? 'yes' : 'no');
return document;
} catch (error) {
console.error('[TeamDocumentService.getDocumentById] - Error:', error);
throw error;
}
}
/**
* Löscht ein Dokument
* @param {number} documentId - Die ID des Dokuments
* @returns {Promise<boolean>} True wenn gelöscht, sonst false
*/
static async deleteDocument(documentId) {
try {
devLog('[TeamDocumentService.deleteDocument] - Deleting document:', documentId);
const document = await TeamDocument.findByPk(documentId);
if (!document) {
return false;
}
// Lösche die physische Datei
if (fs.existsSync(document.filePath)) {
fs.unlinkSync(document.filePath);
}
// Lösche den Datenbankeintrag
const deletedRows = await TeamDocument.destroy({
where: { id: documentId }
});
devLog('[TeamDocumentService.deleteDocument] - Deleted rows:', deletedRows);
return deletedRows > 0;
} catch (error) {
console.error('[TeamDocumentService.deleteDocument] - Error:', error);
throw error;
}
}
/**
* Löscht alle Dokumente eines bestimmten Typs für ein Club-Team
* @param {number} clubTeamId - Die ID des Club-Teams
* @param {string} documentType - Der Typ des Dokuments
* @returns {Promise<number>} Anzahl der gelöschten Dokumente
*/
static async deleteDocumentsByType(clubTeamId, documentType) {
try {
devLog('[TeamDocumentService.deleteDocumentsByType] - Deleting documents by type:', clubTeamId, documentType);
const documents = await TeamDocument.findAll({
where: { clubTeamId, documentType }
});
let deletedCount = 0;
for (const document of documents) {
// Lösche die physische Datei
if (fs.existsSync(document.filePath)) {
fs.unlinkSync(document.filePath);
}
deletedCount++;
}
// Lösche die Datenbankeinträge
const deletedRows = await TeamDocument.destroy({
where: { clubTeamId, documentType }
});
devLog('[TeamDocumentService.deleteDocumentsByType] - Deleted documents:', deletedRows);
return deletedRows;
} catch (error) {
console.error('[TeamDocumentService.deleteDocumentsByType] - Error:', error);
throw error;
}
}
/**
* Holt den Dateipfad für ein Dokument
* @param {number} documentId - Die ID des Dokuments
* @returns {Promise<string|null>} Der Dateipfad oder null
*/
static async getDocumentPath(documentId) {
try {
const document = await TeamDocument.findByPk(documentId);
return document ? document.filePath : null;
} catch (error) {
console.error('[TeamDocumentService.getDocumentPath] - Error:', error);
throw error;
}
}
}
export default TeamDocumentService;

View File

@@ -0,0 +1,7 @@
Spielplan 2025/2026 - Test Liga
1. 15.01.2025 19:00 Team Alpha vs Team Beta code: ABC123 home pin: PIN001 guest pin: PIN002
2. 22.01.2025 20:00 Team Gamma gegen Team Delta code: DEF456 heim pin: PIN003 gast pin: PIN004
3. 29.01.2025 18:30 Team Epsilon - Team Zeta code: GHI789 home pin: PIN005 guest pin: PIN006
4. 05.02.2025 19:30 Team Alpha vs Team Gamma code: JKL012 home pin: PIN007 guest pin: PIN008
5. 12.02.2025 20:00 Team Beta gegen Team Delta code: MNO345 heim pin: PIN009 gast pin: PIN010

View File

@@ -35,6 +35,9 @@
<th>Heimmannschaft</th>
<th>Gastmannschaft</th>
<th v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">Altersklasse</th>
<th>Code</th>
<th>Heim-PIN</th>
<th>Gast-PIN</th>
</tr>
</thead>
<tbody>
@@ -45,6 +48,18 @@
<td v-html="highlightClubName(match.homeTeam?.name || 'N/A')"></td>
<td v-html="highlightClubName(match.guestTeam?.name || 'N/A')"></td>
<td v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">{{ match.leagueDetails?.name || 'N/A' }}</td>
<td class="code-cell">
<span v-if="match.code" class="code-value">{{ match.code }}</span>
<span v-else class="no-data">-</span>
</td>
<td class="pin-cell">
<span v-if="match.homePin" class="pin-value">{{ match.homePin }}</span>
<span v-else class="no-data">-</span>
</td>
<td class="pin-cell">
<span v-if="match.guestPin" class="pin-value">{{ match.guestPin }}</span>
<span v-else class="no-data">-</span>
</td>
</tr>
</tbody>
</table>
@@ -473,6 +488,39 @@ li {
color: transparent !important;
}
/* Code und PIN Styles */
.code-cell, .pin-cell {
text-align: center;
font-family: 'Courier New', monospace;
font-weight: bold;
min-width: 80px;
}
.code-value {
background: #e3f2fd;
color: #1976d2;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9rem;
display: inline-block;
border: 1px solid #bbdefb;
}
.pin-value {
background: #fff3e0;
color: #f57c00;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9rem;
display: inline-block;
border: 1px solid #ffcc02;
}
.no-data {
color: #999;
font-style: italic;
}
.match-today {
background-color: #fff3cd !important; /* Gelb für heute */
}

View File

@@ -50,12 +50,61 @@
<h4>Team-Dokumente hochladen</h4>
<div class="upload-buttons">
<button @click="uploadCodeList" class="upload-btn code-list-btn">
📋 Code-Liste hochladen
📋 Code-Liste hochladen & parsen
</button>
<button @click="uploadPinList" class="upload-btn pin-list-btn">
🔐 Pin-Liste hochladen
🔐 Pin-Liste hochladen & parsen
</button>
</div>
<!-- Upload-Bestätigung -->
<div v-if="showLeagueSelection" class="upload-confirmation">
<div class="selected-file-info">
<strong>Ausgewählte Datei:</strong> {{ pendingUploadFile?.name }}
<br>
<strong>Typ:</strong> {{ pendingUploadType === 'code_list' ? 'Code-Liste' : 'Pin-Liste' }}
<br>
<strong>Team:</strong> {{ teamToEdit?.name }}
<br>
<strong>Liga:</strong> {{ getTeamLeagueName() }}
</div>
<div class="action-buttons">
<button
@click="confirmUploadAndParse"
:disabled="parsingInProgress"
class="confirm-parse-btn"
>
{{ parsingInProgress ? '⏳ Parse läuft...' : '🚀 Hochladen & Parsen' }}
</button>
<button @click="cancelUpload" class="cancel-parse-btn">
Abbrechen
</button>
</div>
</div>
<!-- PDF-Parsing Bereich für bereits hochgeladene Dokumente -->
<div v-if="teamDocuments.length > 0" class="pdf-parsing-section">
<h4>Bereits hochgeladene PDF-Dokumente parsen</h4>
<div class="document-list">
<div v-for="document in teamDocuments" :key="document.id" class="document-item">
<div class="document-info">
<span class="document-name">{{ document.originalFileName }}</span>
<span class="document-type">{{ document.documentType === 'code_list' ? 'Code-Liste' : 'Pin-Liste' }}</span>
<span class="document-size">{{ formatFileSize(document.fileSize) }}</span>
</div>
<div class="document-actions">
<button
@click="parsePDF(document)"
:disabled="document.mimeType !== 'application/pdf'"
class="parse-btn"
:title="document.mimeType !== 'application/pdf' ? 'Nur PDF-Dateien können geparst werden' : 'PDF parsen und Matches extrahieren'"
>
🔍 PDF parsen
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -102,6 +151,50 @@
<span class="value">{{ formatDate(team.createdAt) }}</span>
</div>
</div>
<!-- PDF-Dokumente Icons -->
<div class="team-documents">
<div class="documents-label">Dokumente:</div>
<div class="document-icons">
<button
v-if="getTeamDocuments(team.id, 'code_list').length > 0"
@click.stop="showPDFDialog(team.id, 'code_list')"
class="document-icon code-list-icon"
title="Code-Liste anzeigen"
>
📋
</button>
<button
v-if="getTeamDocuments(team.id, 'pin_list').length > 0"
@click.stop="showPDFDialog(team.id, 'pin_list')"
class="document-icon pin-list-icon"
title="Pin-Liste anzeigen"
>
🔐
</button>
</div>
</div>
</div>
</div>
</div>
<!-- PDF-Dialog -->
<div v-if="showPDFViewer" class="pdf-dialog-overlay" @click="closePDFDialog">
<div class="pdf-dialog" @click.stop>
<div class="pdf-dialog-header">
<h3>{{ pdfDialogTitle }}</h3>
<button @click="closePDFDialog" class="close-btn"></button>
</div>
<div class="pdf-dialog-content">
<iframe
v-if="pdfUrl"
:src="pdfUrl"
class="pdf-viewer"
frameborder="0"
></iframe>
<div v-else class="no-pdf">
<p>PDF konnte nicht geladen werden.</p>
</div>
</div>
</div>
</div>
@@ -131,6 +224,16 @@ export default {
const newLeagueId = ref('');
const selectedSeasonId = ref(null);
const currentSeason = ref(null);
const teamDocuments = ref([]);
const pendingUploadFile = ref(null);
const pendingUploadType = ref(null);
const showLeagueSelection = ref(false);
const parsingInProgress = ref(false);
// PDF-Dialog Variablen
const showPDFViewer = ref(false);
const pdfUrl = ref('');
const pdfDialogTitle = ref('');
// Computed
const selectedClub = computed(() => store.state.currentClub);
@@ -169,6 +272,9 @@ export default {
const response = await apiClient.get(`/club-teams/club/${selectedClub.value}?seasonid=${selectedSeasonId.value}`);
console.log('TeamManagementView: Loaded club teams:', response.data);
teams.value = response.data;
// Lade alle Team-Dokumente nach dem Laden der Teams
await loadAllTeamDocuments();
} catch (error) {
console.error('Fehler beim Laden der Club-Teams:', error);
}
@@ -228,6 +334,7 @@ export default {
newTeamName.value = team.name;
newLeagueId.value = team.leagueId || '';
teamFormIsOpen.value = true;
loadTeamDocuments();
};
const deleteTeam = async (team) => {
@@ -254,14 +361,12 @@ export default {
const file = event.target.files[0];
if (!file) return;
try {
console.log('Code-Liste hochladen für Team:', teamToEdit.value.name, 'Datei:', file.name);
// TODO: Implementiere Upload-Logik für Code-Liste
alert(`Code-Liste "${file.name}" würde für Team "${teamToEdit.value.name}" hochgeladen werden.`);
} catch (error) {
console.error('Fehler beim Hochladen der Code-Liste:', error);
alert('Fehler beim Hochladen der Code-Liste');
}
// Speichere die Datei und den Typ für späteres Parsing
pendingUploadFile.value = file;
pendingUploadType.value = 'code_list';
// Zeige Liga-Auswahl für Parsing
showLeagueSelection.value = true;
};
input.click();
};
@@ -277,14 +382,12 @@ export default {
const file = event.target.files[0];
if (!file) return;
try {
console.log('Pin-Liste hochladen für Team:', teamToEdit.value.name, 'Datei:', file.name);
// TODO: Implementiere Upload-Logik für Pin-Liste
alert(`Pin-Liste "${file.name}" würde für Team "${teamToEdit.value.name}" hochgeladen werden.`);
} catch (error) {
console.error('Fehler beim Hochladen der Pin-Liste:', error);
alert('Fehler beim Hochladen der Pin-Liste');
}
// Speichere die Datei und den Typ für späteres Parsing
pendingUploadFile.value = file;
pendingUploadType.value = 'pin_list';
// Zeige Liga-Auswahl für Parsing
showLeagueSelection.value = true;
};
input.click();
};
@@ -292,6 +395,181 @@ export default {
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('de-DE');
};
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const loadTeamDocuments = async () => {
if (!teamToEdit.value) return;
try {
console.log('TeamManagementView: Loading documents for team:', teamToEdit.value.id);
const response = await apiClient.get(`/team-documents/club-team/${teamToEdit.value.id}`);
console.log('TeamManagementView: Loaded documents:', response.data);
teamDocuments.value = response.data;
} catch (error) {
console.error('Fehler beim Laden der Team-Dokumente:', error);
}
};
const loadAllTeamDocuments = async () => {
if (!selectedClub.value) return;
try {
console.log('TeamManagementView: Loading all team documents for club:', selectedClub.value);
// Lade alle Dokumente für alle Teams des Clubs
const allDocuments = [];
for (const team of teams.value) {
try {
const response = await apiClient.get(`/team-documents/club-team/${team.id}`);
allDocuments.push(...response.data);
} catch (error) {
console.warn(`Fehler beim Laden der Dokumente für Team ${team.id}:`, error);
}
}
teamDocuments.value = allDocuments;
console.log('TeamManagementView: Loaded all team documents:', allDocuments.length);
} catch (error) {
console.error('Fehler beim Laden aller Team-Dokumente:', error);
}
};
const confirmUploadAndParse = async () => {
if (!pendingUploadFile.value || !teamToEdit.value?.leagueId) {
alert('Team ist keiner Liga zugeordnet!');
return;
}
parsingInProgress.value = true;
try {
console.log('Datei hochladen und parsen:', pendingUploadFile.value.name, 'Typ:', pendingUploadType.value, 'für Liga:', teamToEdit.value.leagueId);
// Schritt 1: Datei als Team-Dokument hochladen
const formData = new FormData();
formData.append('document', pendingUploadFile.value);
formData.append('documentType', pendingUploadType.value);
const uploadResponse = await apiClient.post(`/team-documents/club-team/${teamToEdit.value.id}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
console.log('Datei hochgeladen:', uploadResponse.data);
// Schritt 2: Datei parsen (nur für PDF/TXT-Dateien)
const fileExtension = pendingUploadFile.value.name.toLowerCase().split('.').pop();
if (fileExtension === 'pdf' || fileExtension === 'txt') {
const parseResponse = await apiClient.post(`/team-documents/${uploadResponse.data.id}/parse?leagueid=${teamToEdit.value.leagueId}`);
console.log('Datei erfolgreich geparst:', parseResponse.data);
const { parseResult, saveResult } = parseResponse.data;
let message = `${pendingUploadType.value === 'code_list' ? 'Code-Liste' : 'Pin-Liste'} erfolgreich hochgeladen und geparst!\n\n`;
message += `Gefundene Spiele: ${parseResult.matchesFound}\n`;
message += `Neue Spiele erstellt: ${saveResult.created}\n`;
message += `Spiele aktualisiert: ${saveResult.updated}\n`;
if (saveResult.errors.length > 0) {
message += `\nFehler: ${saveResult.errors.length}\n`;
message += saveResult.errors.slice(0, 3).join('\n');
if (saveResult.errors.length > 3) {
message += `\n... und ${saveResult.errors.length - 3} weitere`;
}
}
// Debug-Informationen anzeigen wenn keine Matches gefunden wurden
if (parseResult.matchesFound === 0) {
message += `\n\n--- DEBUG-INFORMATIONEN ---\n`;
message += `Text-Länge: ${parseResult.debugInfo.totalTextLength} Zeichen\n`;
message += `Zeilen: ${parseResult.debugInfo.totalLines}\n`;
message += `Erste Zeilen:\n`;
parseResult.debugInfo.firstFewLines.forEach((line, index) => {
message += `${index + 1}: "${line}"\n`;
});
message += `\nLetzte Zeilen:\n`;
parseResult.debugInfo.lastFewLines.forEach((line, index) => {
message += `${parseResult.debugInfo.totalLines - 5 + index + 1}: "${line}"\n`;
});
}
alert(message);
} else {
// Für andere Dateitypen nur Upload-Bestätigung
alert(`${pendingUploadType.value === 'code_list' ? 'Code-Liste' : 'Pin-Liste'} "${pendingUploadFile.value.name}" wurde erfolgreich hochgeladen!`);
}
// Dokumente neu laden
await loadTeamDocuments();
} catch (error) {
console.error('Fehler beim Hochladen und Parsen der Datei:', error);
alert('Fehler beim Hochladen und Parsen der Datei');
} finally {
parsingInProgress.value = false;
pendingUploadFile.value = null;
pendingUploadType.value = null;
showLeagueSelection.value = false;
}
};
const cancelUpload = () => {
pendingUploadFile.value = null;
pendingUploadType.value = null;
showLeagueSelection.value = false;
};
const getTeamLeagueName = () => {
if (!teamToEdit.value?.leagueId) return 'Keine Liga zugeordnet';
const league = leagues.value.find(l => l.id === teamToEdit.value.leagueId);
return league ? league.name : 'Unbekannte Liga';
};
const parsePDF = async (document) => {
// Finde das Team für dieses Dokument
const team = teams.value.find(t => t.id === document.clubTeamId);
if (!team || !team.leagueId) {
alert('Team ist keiner Liga zugeordnet!');
return;
}
try {
console.log('PDF parsen:', document.originalFileName, 'für Liga:', team.leagueId);
const response = await apiClient.post(`/team-documents/${document.id}/parse?leagueid=${team.leagueId}`);
console.log('PDF erfolgreich geparst:', response.data);
const { parseResult, saveResult } = response.data;
let message = `PDF erfolgreich geparst!\n\n`;
message += `Gefundene Spiele: ${parseResult.matchesFound}\n`;
message += `Neue Spiele erstellt: ${saveResult.created}\n`;
message += `Spiele aktualisiert: ${saveResult.updated}\n`;
if (saveResult.errors.length > 0) {
message += `\nFehler: ${saveResult.errors.length}\n`;
message += saveResult.errors.slice(0, 3).join('\n');
if (saveResult.errors.length > 3) {
message += `\n... und ${saveResult.errors.length - 3} weitere`;
}
}
alert(message);
} catch (error) {
console.error('Fehler beim Parsen der PDF:', error);
alert('Fehler beim Parsen der PDF-Datei');
}
};
const onSeasonChange = (season) => {
currentSeason.value = season;
@@ -310,6 +588,46 @@ export default {
loadLeagues();
});
// PDF-Dialog Funktionen
const getTeamDocuments = (teamId, documentType) => {
return teamDocuments.value.filter(doc =>
doc.clubTeamId === teamId && doc.documentType === documentType
);
};
const showPDFDialog = async (teamId, documentType) => {
const documents = getTeamDocuments(teamId, documentType);
if (documents.length === 0) return;
const document = documents[0]; // Nehme das erste Dokument
const team = teams.value.find(t => t.id === teamId);
pdfDialogTitle.value = `${team?.name || 'Team'} - ${documentType === 'code_list' ? 'Code-Liste' : 'Pin-Liste'}`;
try {
// Lade das PDF über die API
const response = await apiClient.get(`/team-documents/${document.id}/download`, {
responseType: 'blob'
});
// Erstelle eine URL für das PDF
const blob = new Blob([response.data], { type: 'application/pdf' });
pdfUrl.value = URL.createObjectURL(blob);
showPDFViewer.value = true;
} catch (error) {
console.error('Fehler beim Laden des PDFs:', error);
alert('Fehler beim Laden des PDFs');
}
};
const closePDFDialog = () => {
showPDFViewer.value = false;
if (pdfUrl.value) {
URL.revokeObjectURL(pdfUrl.value);
pdfUrl.value = '';
}
};
return {
teams,
leagues,
@@ -319,6 +637,14 @@ export default {
newLeagueId,
selectedSeasonId,
currentSeason,
teamDocuments,
pendingUploadFile,
pendingUploadType,
showLeagueSelection,
parsingInProgress,
showPDFViewer,
pdfUrl,
pdfDialogTitle,
filteredLeagues,
toggleNewTeam,
resetToNewTeam,
@@ -328,7 +654,17 @@ export default {
deleteTeam,
uploadCodeList,
uploadPinList,
loadTeamDocuments,
loadAllTeamDocuments,
confirmUploadAndParse,
cancelUpload,
getTeamLeagueName,
parsePDF,
getTeamDocuments,
showPDFDialog,
closePDFDialog,
formatDate,
formatFileSize,
onSeasonChange
};
}
@@ -609,4 +945,326 @@ export default {
justify-content: center;
}
}
/* Upload-Bestätigung */
.upload-confirmation {
margin-top: 1.5rem;
padding: 1.5rem;
background: #f8f9fa;
border-radius: var(--border-radius-medium);
border: 2px solid #dee2e6;
}
.selected-file-info {
background: #e9ecef;
padding: 1rem;
border-radius: var(--border-radius-small);
margin-bottom: 1rem;
font-size: 0.9rem;
}
.action-buttons {
display: flex;
gap: 1rem;
align-items: center;
}
.confirm-parse-btn {
background: #4caf50;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: var(--border-radius-small);
font-weight: 600;
cursor: pointer;
transition: var(--transition);
margin-right: 1rem;
}
.confirm-parse-btn:hover:not(:disabled) {
background: #45a049;
}
.confirm-parse-btn:disabled {
background: #cccccc;
cursor: not-allowed;
}
.cancel-parse-btn {
background: #6c757d;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: var(--border-radius-small);
font-weight: 600;
cursor: pointer;
transition: var(--transition);
}
.cancel-parse-btn:hover {
background: #5a6268;
}
.selected-file {
display: block;
margin-top: 0.5rem;
font-size: 0.875rem;
color: #2e7d32;
font-weight: 600;
}
.parse-options {
display: flex;
gap: 1rem;
align-items: end;
flex-wrap: wrap;
}
.parse-options label {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-width: 200px;
}
.parse-options label span {
font-weight: 600;
color: var(--text-color);
}
/* PDF-Parsing Styles */
.pdf-parsing-section {
margin-top: 2rem;
padding: 1.5rem;
background: var(--background-light);
border-radius: var(--border-radius-medium);
border: 1px solid var(--border-color);
}
.pdf-parsing-section h4 {
margin: 0 0 1rem 0;
color: var(--text-color);
font-size: 1.1rem;
}
.document-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.document-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: white;
border-radius: var(--border-radius-small);
border: 1px solid var(--border-color);
}
.document-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.document-name {
font-weight: 600;
color: var(--text-color);
}
.document-type {
font-size: 0.875rem;
color: var(--text-muted);
background: var(--primary-light);
padding: 0.25rem 0.5rem;
border-radius: var(--border-radius-small);
display: inline-block;
width: fit-content;
}
.document-size {
font-size: 0.75rem;
color: var(--text-muted);
}
.document-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.parse-btn {
padding: 0.5rem 1rem;
background: #2196F3;
color: white;
border: none;
border-radius: var(--border-radius-small);
font-weight: 600;
cursor: pointer;
transition: var(--transition);
}
.parse-btn:hover:not(:disabled) {
background: #1976D2;
}
.parse-btn:disabled {
background: var(--text-muted);
cursor: not-allowed;
}
/* Team-Dokumente Styles */
.team-documents {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.documents-label {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.document-icons {
display: flex;
gap: 0.5rem;
}
.document-icon {
background: none;
border: 2px solid var(--border-color);
border-radius: var(--border-radius-small);
padding: 0.5rem;
cursor: pointer;
font-size: 1.25rem;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
min-width: 2.5rem;
height: 2.5rem;
}
.document-icon:hover {
border-color: var(--primary-color);
background: var(--primary-color);
color: white;
transform: translateY(-2px);
}
.code-list-icon:hover {
border-color: #4CAF50;
background: #4CAF50;
}
.pin-list-icon:hover {
border-color: #FF9800;
background: #FF9800;
}
/* PDF-Dialog Styles */
.pdf-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.pdf-dialog {
background: white;
border-radius: var(--border-radius-medium);
width: 90%;
height: 90%;
max-width: 1200px;
max-height: 800px;
display: flex;
flex-direction: column;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.pdf-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
background: var(--background-light);
border-radius: var(--border-radius-medium) var(--border-radius-medium) 0 0;
}
.pdf-dialog-header h3 {
margin: 0;
color: var(--text-primary);
font-size: 1.25rem;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-muted);
padding: 0.25rem;
border-radius: var(--border-radius-small);
transition: var(--transition);
}
.close-btn:hover {
background: var(--background-light);
color: var(--text-primary);
}
.pdf-dialog-content {
flex: 1;
padding: 0;
overflow: hidden;
}
.pdf-viewer {
width: 100%;
height: 100%;
border: none;
}
.no-pdf {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
}
@media (max-width: 768px) {
.document-item {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.document-actions {
width: 100%;
justify-content: space-between;
}
.parse-options {
flex-direction: column;
align-items: stretch;
}
.parse-options label {
min-width: auto;
}
}
</style>