Update PDF parsing and document upload handling in team management

Enhanced the PDFParserService to support layout-based text extraction from PDFs using pdfjs-dist, improving parsing accuracy. Updated the team management view to streamline document uploads and parsing processes, removing unnecessary UI elements and consolidating upload logic. Improved error handling and user feedback during document processing, ensuring better user experience and clarity in case of issues.
This commit is contained in:
Torsten Schulz (local)
2025-11-08 10:15:25 +01:00
parent f0e3c6a717
commit d79e71d6d7
8 changed files with 688 additions and 238 deletions

View File

@@ -213,7 +213,7 @@
</div>
</td>
</tr>
</template>
</template>
</tbody>
</table>
</div>
@@ -241,26 +241,26 @@
@delete-note="deleteNote"
@close="closeNotesModal"
/>
<!-- Info Dialog -->
<InfoDialog
v-model="infoDialog.isOpen"
:title="infoDialog.title"
:message="infoDialog.message"
:details="infoDialog.details"
:type="infoDialog.type"
/>
<!-- Confirm Dialog -->
<ConfirmDialog
v-model="confirmDialog.isOpen"
:title="confirmDialog.title"
:message="confirmDialog.message"
:details="confirmDialog.details"
:type="confirmDialog.type"
@confirm="handleConfirmResult(true)"
@cancel="handleConfirmResult(false)"
/>
<!-- Info Dialog -->
<InfoDialog
v-model="infoDialog.isOpen"
:title="infoDialog.title"
:message="infoDialog.message"
:details="infoDialog.details"
:type="infoDialog.type"
/>
<!-- Confirm Dialog -->
<ConfirmDialog
v-model="confirmDialog.isOpen"
:title="confirmDialog.title"
:message="confirmDialog.message"
:details="confirmDialog.details"
:type="confirmDialog.type"
@confirm="handleConfirmResult(true)"
@cancel="handleConfirmResult(false)"
/>
<!-- Member Activities Dialog -->
<MemberActivitiesDialog

View File

@@ -139,67 +139,6 @@
</div>
</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 compact">
<span class="section-title">📄 Hochgeladene Dokumente</span>
<table class="document-table compact">
<thead>
<tr>
<th>Dateiname</th>
<th>Typ</th>
<th>Größe</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<tr v-for="document in teamDocuments" :key="document.id" class="document-row">
<td class="document-name">{{ document.originalFileName }}</td>
<td class="document-type">
<span class="type-badge" :class="document.documentType">
{{ document.documentType === 'code_list' ? 'Code-Liste' : 'Pin-Liste' }}
</span>
</td>
<td class="document-size">{{ formatFileSize(document.fileSize) }}</td>
<td 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'"
>
🔍 Parsen
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Automatische Jobs Info für dieses Team -->
<div v-if="getTeamJobInfo(teamToEdit)" class="team-job-info compact">
<span class="section-title">🔄 Automatische Jobs</span>
@@ -443,10 +382,8 @@ export default {
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);
const parsingDocuments = ref({});
// PDF-Dialog Variablen
const showPDFViewer = ref(false);
@@ -594,42 +531,34 @@ export default {
const uploadCodeList = () => {
if (!teamToEdit.value) return;
if (parsingInProgress.value) {
return;
}
// Erstelle ein verstecktes File-Input-Element
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf,.doc,.docx,.txt,.csv';
input.onchange = async (event) => {
const file = event.target.files[0];
if (!file) return;
// 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;
await uploadAndParseDocument(file, 'code_list');
};
input.click();
};
const uploadPinList = () => {
if (!teamToEdit.value) return;
if (parsingInProgress.value) {
return;
}
// Erstelle ein verstecktes File-Input-Element
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf,.doc,.docx,.txt,.csv';
input.onchange = async (event) => {
const file = event.target.files[0];
if (!file) return;
// 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;
await uploadAndParseDocument(file, 'pin_list');
};
input.click();
};
@@ -702,136 +631,153 @@ export default {
}
};
const confirmUploadAndParse = async () => {
if (!pendingUploadFile.value || !teamToEdit.value?.leagueId) {
alert('Team ist keiner Liga zugeordnet!');
const uploadAndParseDocument = async (file, documentType) => {
if (!teamToEdit.value?.leagueId) {
await showInfo(
'Hinweis',
'Dieses Team ist keiner Liga zugeordnet.',
'Bitte ordnen Sie dem Team zuerst eine Liga zu, damit Dokumente verarbeitet werden können.',
'warning'
);
return;
}
if (parsingInProgress.value) {
return;
}
parsingInProgress.value = true;
try {
// Schritt 1: Datei als Team-Dokument hochladen
const formData = new FormData();
formData.append('document', pendingUploadFile.value);
formData.append('documentType', pendingUploadType.value);
formData.append('document', file);
formData.append('documentType', documentType);
const uploadResponse = await apiClient.post(`/team-documents/club-team/${teamToEdit.value.id}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
// Schritt 2: Datei parsen (nur für PDF/TXT-Dateien)
const fileExtension = pendingUploadFile.value.name.toLowerCase().split('.').pop();
const fileExtension = file.name.toLowerCase().split('.').pop();
const documentLabel = documentType === 'code_list' ? 'Code-Liste' : 'Pin-Liste';
if (fileExtension === 'pdf' || fileExtension === 'txt') {
const parseResponse = await apiClient.post(`/team-documents/${uploadResponse.data.id}/parse?leagueid=${teamToEdit.value.leagueId}`);
const { parseResult, saveResult } = parseResponse.data;
let message = `${pendingUploadType.value === 'code_list' ? 'Code-Liste' : 'Pin-Liste'} erfolgreich hochgeladen und geparst!\n\n`;
let message = `${documentLabel} 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`;
message += `Spiele aktualisiert: ${saveResult.updated}`;
if (saveResult.errors.length > 0) {
message += `\nFehler: ${saveResult.errors.length}\n`;
message += `\n\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
let dialogTitle = 'Erfolg';
let dialogType = 'success';
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`;
});
// Fehler-Dialog wenn nichts gefunden wurde
await showInfo('Fehler', message, '', 'error');
dialogTitle = 'Keine Spiele gefunden';
dialogType = 'warning';
if (parseResult.debugInfo) {
message += `\n\nHinweis: Keine Spiele erkannt.\nZeilen im Dokument: ${parseResult.debugInfo.totalLines}`;
}
} else if (saveResult.errors.length > 0) {
// Warnung wenn Spiele gefunden wurden, aber Fehler auftraten
await showInfo('Warnung', message, '', 'warning');
} else {
// Erfolg wenn alles geklappt hat
await showInfo('Erfolg', message, '', 'success');
dialogTitle = 'Warnung';
dialogType = 'warning';
}
await showInfo(dialogTitle, message, '', dialogType);
} else {
// Für andere Dateitypen nur Upload-Bestätigung
await showInfo('Information', `${pendingUploadType.value === 'code_list' ? 'Code-Liste' : 'Pin-Liste'} "${pendingUploadFile.value.name}" wurde erfolgreich hochgeladen!`, '', 'info');
await showInfo('Information', `${documentLabel} "${file.name}" wurde erfolgreich hochgeladen!`, '', 'info');
}
// Dokumente neu laden
await loadTeamDocuments();
} catch (error) {
console.error('Fehler beim Hochladen und Parsen der Datei:', error);
await showInfo('Fehler', 'Fehler beim Hochladen und Parsen der Datei', '', 'error');
const responseData = error?.response?.data || {};
const errorMessage = responseData.message || responseData.error || error.message || 'Fehler beim Hochladen und Parsen der Datei';
await showInfo('Fehler', errorMessage, '', 'error');
} 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 parsePDF = async (document) => {
const team = teams.value.find(t => t.id === document.clubTeamId);
if (!team || !team.leagueId) {
alert('Team ist keiner Liga zugeordnet!');
await showInfo(
'Hinweis',
'Dieses Team ist keiner Liga zugeordnet.',
'Bitte ordnen Sie dem Team zuerst eine Liga zu, um PDF-Dateien zu parsen.',
'warning'
);
return;
}
if (parsingDocuments.value[document.id]) {
return;
}
parsingDocuments.value = {
...parsingDocuments.value,
[document.id]: true
};
try {
const response = await apiClient.post(`/team-documents/${document.id}/parse?leagueid=${team.leagueId}`);
const { parseResult, saveResult } = response.data;
let message = `PDF erfolgreich geparst!\n\n`;
message += `Gefundene Spiele: ${parseResult.matchesFound}\n`;
let 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 += `Spiele aktualisiert: ${saveResult.updated}`;
let dialogTitle = 'Erfolg';
let dialogType = 'success';
if (parseResult.matchesFound === 0) {
dialogTitle = 'Keine Spiele gefunden';
dialogType = 'warning';
if (parseResult.debugInfo) {
message += `\n\nHinweis: Keine Spiele erkannt.\nZeilen im Dokument: ${parseResult.debugInfo.totalLines}`;
}
} else if (saveResult.errors.length > 0) {
dialogTitle = 'Warnung';
dialogType = 'warning';
message += `\n\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`;
}
}
this.showInfo('Fehler', message, '', 'error');
await showInfo(dialogTitle, message, '', dialogType);
await loadTeamDocuments();
} catch (error) {
console.error('Fehler beim Parsen der PDF:', error);
this.showInfo('Fehler', 'Fehler beim Parsen der PDF-Datei', '', 'error');
const responseData = error?.response?.data || {};
const status = error?.response?.status;
let errorMessage = responseData.message || responseData.error || error.message || 'Fehler beim Parsen der PDF-Datei';
let details = '';
if (status === 404 && responseData.error === 'documentnotfound') {
errorMessage = 'Das ausgewählte Dokument wurde nicht gefunden.';
} else if (status === 400 && responseData.error === 'missingleagueid') {
errorMessage = 'Für das ausgewählte Team wurde keine Liga übermittelt.';
} else if (error.code === 'ENOENT' || errorMessage.includes('ENOENT')) {
errorMessage = 'Die PDF-Datei konnte nicht gefunden werden.';
details = 'Bitte laden Sie die Datei erneut hoch und versuchen Sie es noch einmal.';
}
await showInfo('Fehler', errorMessage, details, 'error');
} finally {
const { [document.id]: _ignored, ...rest } = parsingDocuments.value;
parsingDocuments.value = rest;
}
};
@@ -1289,10 +1235,8 @@ export default {
selectedSeasonId,
currentSeason,
teamDocuments,
pendingUploadFile,
pendingUploadType,
showLeagueSelection,
parsingInProgress,
parsingDocuments,
showPDFViewer,
pdfUrl,
pdfDialogTitle,
@@ -1317,9 +1261,6 @@ export default {
uploadPinList,
loadTeamDocuments,
loadAllTeamDocuments,
confirmUploadAndParse,
cancelUpload,
getTeamLeagueName,
parsePDF,
getTeamDocuments,
showPDFDialog,