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:
233
backend/controllers/teamDocumentController.js
Normal file
233
backend/controllers/teamDocumentController.js
Normal 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" });
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
52
backend/models/TeamDocument.js
Normal file
52
backend/models/TeamDocument.js
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
33
backend/routes/teamDocumentRoutes.js
Normal file
33
backend/routes/teamDocumentRoutes.js
Normal 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;
|
||||
@@ -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')));
|
||||
|
||||
@@ -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: '' },
|
||||
|
||||
690
backend/services/pdfParserService.js
Normal file
690
backend/services/pdfParserService.js
Normal 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;
|
||||
204
backend/services/teamDocumentService.js
Normal file
204
backend/services/teamDocumentService.js
Normal 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;
|
||||
7
backend/test-spielplan.txt
Normal file
7
backend/test-spielplan.txt
Normal 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
|
||||
BIN
backend/uploads/team-documents/7_code_list_1759354257578.pdf
Normal file
BIN
backend/uploads/team-documents/7_code_list_1759354257578.pdf
Normal file
Binary file not shown.
BIN
backend/uploads/team-documents/9_code_list_1759357969975.pdf
Normal file
BIN
backend/uploads/team-documents/9_code_list_1759357969975.pdf
Normal file
Binary file not shown.
BIN
backend/uploads/team-documents/9_pin_list_1759386673266.pdf
Normal file
BIN
backend/uploads/team-documents/9_pin_list_1759386673266.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user