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.
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user