Fügt einen Dialog-Manager hinzu, um die Verwaltung von Dialogen zu ermöglichen. Aktualisiert den Vuex-Store mit neuen Mutationen und Aktionen zur Handhabung von Dialogen. Integriert den MatchReportDialog in ScheduleView.vue und ermöglicht das Öffnen von Spielberichten über die Benutzeroberfläche. Verbessert die Benutzererfahrung durch neue Schaltflächen und CSS-Stile für die Dialoge.
This commit is contained in:
@@ -102,6 +102,10 @@
|
||||
<router-view class="content fade-in"></router-view>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Dialog Manager -->
|
||||
<DialogManager />
|
||||
|
||||
<footer class="app-footer">
|
||||
<div class="footer-content">
|
||||
<router-link to="/impressum" class="footer-link">Impressum</router-link>
|
||||
@@ -116,9 +120,13 @@
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import apiClient from './apiClient.js';
|
||||
import logoUrl from './assets/logo.png';
|
||||
import DialogManager from './components/DialogManager.vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
DialogManager
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedClub: null,
|
||||
|
||||
370
frontend/src/components/DialogManager.vue
Normal file
370
frontend/src/components/DialogManager.vue
Normal file
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div class="dialog-manager">
|
||||
<!-- Minimierte Dialoge in der Fußzeile -->
|
||||
<div class="minimized-dialogs">
|
||||
<button
|
||||
v-for="dialog in minimizedDialogs"
|
||||
:key="dialog.id"
|
||||
@click="restoreDialog(dialog.id)"
|
||||
class="minimized-dialog-button"
|
||||
:title="dialog.title"
|
||||
>
|
||||
{{ dialog.title }}
|
||||
</button>
|
||||
<div v-if="minimizedDialogs.length === 0" class="no-minimized-dialogs">
|
||||
Keine minimierten Dialoge
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktive Dialoge -->
|
||||
<div
|
||||
v-for="dialog in activeDialogs"
|
||||
:key="dialog.id"
|
||||
class="dialog-window"
|
||||
:style="{
|
||||
left: dialog.position.x + 'px',
|
||||
top: dialog.position.y + 'px',
|
||||
zIndex: dialog.zIndex
|
||||
}"
|
||||
@mousedown="bringToFront(dialog.id)"
|
||||
>
|
||||
<div class="dialog-header" @mousedown="startDrag(dialog.id, $event)">
|
||||
<h3 class="dialog-title">{{ dialog.title }}</h3>
|
||||
<div class="dialog-header-actions">
|
||||
<component
|
||||
v-if="dialog.headerActions"
|
||||
:is="getDialogComponent(dialog.headerActions.component)"
|
||||
v-bind="dialog.headerActions.props"
|
||||
:dialog-id="dialog.id"
|
||||
@action="handleHeaderAction"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-controls">
|
||||
<button @click="minimizeDialog(dialog.id)" class="control-btn minimize-btn" title="Minimieren">−</button>
|
||||
<button @click="closeDialog(dialog.id)" class="control-btn close-btn" title="Schließen">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<component :is="getDialogComponent(dialog.component)" v-bind="dialog.props" @close="() => closeDialog(dialog.id)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import MatchReportDialog from './MatchReportDialog.vue';
|
||||
import MatchReportHeaderActions from './MatchReportHeaderActions.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MatchReportDialog,
|
||||
MatchReportHeaderActions
|
||||
},
|
||||
name: 'DialogManager',
|
||||
computed: {
|
||||
...mapGetters(['dialogs', 'minimizedDialogs', 'activeDialogs'])
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['closeDialog', 'minimizeDialog', 'restoreDialog', 'bringDialogToFront']),
|
||||
|
||||
getDialogComponent(componentName) {
|
||||
const components = {
|
||||
'MatchReportDialog': MatchReportDialog,
|
||||
'MatchReportHeaderActions': MatchReportHeaderActions
|
||||
};
|
||||
return components[componentName] || null;
|
||||
},
|
||||
|
||||
bringToFront(dialogId) {
|
||||
this.bringDialogToFront(dialogId);
|
||||
},
|
||||
|
||||
startDrag(dialogId, event) {
|
||||
// Drag & Drop deaktiviert, da Dialog jetzt fest positioniert ist
|
||||
// Dialog wird immer zentriert angezeigt
|
||||
this.bringToFront(dialogId);
|
||||
},
|
||||
|
||||
handleHeaderAction(action) {
|
||||
// Finde den Dialog und die entsprechende Komponente
|
||||
const dialog = this.dialogs.find(d => d.id === action.dialogId);
|
||||
if (!dialog) return;
|
||||
|
||||
// Behandle die verschiedenen Action-Types
|
||||
if (action.type === 'insertPin') {
|
||||
// PIN ins iframe einfügen
|
||||
this.insertPinIntoIframe(action.match);
|
||||
}
|
||||
},
|
||||
|
||||
insertPinIntoIframe(match) {
|
||||
console.log('🔍 PIN-Einfügen gestartet für Match:', match);
|
||||
console.log('📌 Verfügbare PINs:', {
|
||||
homePin: match.homePin,
|
||||
guestPin: match.guestPin
|
||||
});
|
||||
|
||||
// Versuche direkten Zugriff auf die MatchReportDialog-Komponente
|
||||
const matchReportDialogs = document.querySelectorAll('.match-report-dialog');
|
||||
console.log('🖼️ Gefundene MatchReportDialogs:', matchReportDialogs.length);
|
||||
|
||||
if (matchReportDialogs.length > 0) {
|
||||
// Versuche die insertPinManually Methode aufzurufen
|
||||
console.log('🎯 Versuche direkten Zugriff auf MatchReportDialog');
|
||||
|
||||
// Finde das iframe im aktuellen Dialog
|
||||
const iframe = matchReportDialogs[matchReportDialogs.length - 1].querySelector('iframe');
|
||||
if (iframe) {
|
||||
console.log('✅ Iframe gefunden, versuche PIN-Einfügung');
|
||||
|
||||
try {
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
if (iframeDoc) {
|
||||
console.log('✅ Direkter DOM-Zugriff möglich');
|
||||
|
||||
// Suche nach PIN-Feldern
|
||||
const pinSelectors = [
|
||||
'input[type="password"][placeholder*="Vereins"]',
|
||||
'input[type="password"][placeholder*="Spiel-Pin"]',
|
||||
'input[type="password"][placeholder*="PIN"]',
|
||||
'input[type="password"]',
|
||||
'input[placeholder*="Vereins"]',
|
||||
'input[placeholder*="Spiel-Pin"]'
|
||||
];
|
||||
|
||||
let pinField = null;
|
||||
for (const selector of pinSelectors) {
|
||||
pinField = iframeDoc.querySelector(selector);
|
||||
if (pinField) {
|
||||
console.log(`✅ PIN-Feld gefunden mit Selektor: ${selector}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pinField && match.homePin) {
|
||||
console.log('📝 Füge PIN ein:', match.homePin);
|
||||
pinField.value = match.homePin;
|
||||
pinField.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
pinField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
pinField.dispatchEvent(new Event('blur', { bubbles: true }));
|
||||
console.log('✅ PIN erfolgreich eingefügt');
|
||||
return;
|
||||
} else {
|
||||
console.log('❌ PIN-Feld nicht gefunden');
|
||||
console.log('🔍 Verfügbare Input-Felder:', iframeDoc.querySelectorAll('input'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('🚫 Cross-Origin-Zugriff blockiert (erwartet)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: PostMessage verwenden
|
||||
const iframes = document.querySelectorAll('iframe');
|
||||
if (iframes.length > 0) {
|
||||
const iframe = iframes[iframes.length - 1]; // Neuestes iframe
|
||||
|
||||
const message = {
|
||||
action: 'fillPin',
|
||||
pin: match.homePin,
|
||||
timestamp: Date.now(),
|
||||
source: 'trainingstagebuch'
|
||||
};
|
||||
|
||||
console.log('📤 Sende PostMessage:', message);
|
||||
|
||||
const origins = ['https://ttde-apps.liga.nu', 'https://liga.nu', '*'];
|
||||
origins.forEach(origin => {
|
||||
try {
|
||||
iframe.contentWindow.postMessage(message, origin);
|
||||
console.log(`📤 PostMessage an ${origin} gesendet`);
|
||||
} catch (e) {
|
||||
console.log(`❌ PostMessage an ${origin} fehlgeschlagen:`, e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('💡 Alternative: Verwenden Sie den "📋 PIN kopieren" Button');
|
||||
console.log('📋 PIN zum Kopieren:', match.homePin || match.guestPin);
|
||||
},
|
||||
|
||||
handlePostMessage(event) {
|
||||
console.log('📨 PostMessage empfangen:', event);
|
||||
console.log('- Origin:', event.origin);
|
||||
console.log('- Data:', event.data);
|
||||
|
||||
// Nur Nachrichten von nuscore verarbeiten
|
||||
if (event.origin !== 'https://ttde-apps.liga.nu' && event.origin !== 'https://liga.nu') {
|
||||
console.log('🚫 Nachricht von unbekannter Origin ignoriert');
|
||||
return;
|
||||
}
|
||||
|
||||
// Hier können wir auf Antworten von nuscore reagieren
|
||||
if (event.data && event.data.action) {
|
||||
console.log('🎯 Action empfangen:', event.data.action);
|
||||
|
||||
if (event.data.action === 'pinFilled') {
|
||||
console.log('✅ PIN wurde erfolgreich eingefügt');
|
||||
} else if (event.data.action === 'pinError') {
|
||||
console.log('❌ Fehler beim PIN-Einfügen:', event.data.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Event-Listener für PostMessage-Antworten
|
||||
window.addEventListener('message', this.handlePostMessage);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
// Event-Listener entfernen
|
||||
window.removeEventListener('message', this.handlePostMessage);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-manager {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog-window {
|
||||
position: absolute;
|
||||
width: 90vw;
|
||||
height: 90vh;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
left: 5vw !important;
|
||||
top: 5vh !important;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dialog-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dialog-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.minimized-dialogs {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 2000;
|
||||
pointer-events: auto;
|
||||
min-height: 40px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.no-minimized-dialogs {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.minimized-dialog-button {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: background-color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.minimized-dialog-button:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.dialog-window {
|
||||
width: 95vw;
|
||||
height: 95vh;
|
||||
left: 2.5vw !important;
|
||||
top: 2.5vh !important;
|
||||
}
|
||||
|
||||
.minimized-dialogs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.minimized-dialog-button {
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
425
frontend/src/components/MatchReportDialog.vue
Normal file
425
frontend/src/components/MatchReportDialog.vue
Normal file
@@ -0,0 +1,425 @@
|
||||
<template>
|
||||
<div class="match-report-dialog">
|
||||
<div class="report-content">
|
||||
<iframe
|
||||
ref="reportIframe"
|
||||
:src="reportUrl"
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameborder="0"
|
||||
class="report-iframe"
|
||||
@load="onIframeLoad"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MatchReportDialog',
|
||||
props: {
|
||||
match: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
reportUrl() {
|
||||
// Verschiedene URL-Parameter versuchen, die nuscore möglicherweise unterstützt
|
||||
const baseUrl = 'https://ttde-apps.liga.nu/nuliga/nuscore-tt/meetings-list';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Verschiedene Parameter-Namen versuchen
|
||||
params.set('code', this.match.code);
|
||||
params.set('gamecode', this.match.code);
|
||||
params.set('spielcode', this.match.code);
|
||||
params.set('matchcode', this.match.code);
|
||||
|
||||
return `${baseUrl}?${params.toString()}`;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Event-Listener für PostMessage-Antworten
|
||||
window.addEventListener('message', this.handlePostMessage);
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Event-Listener entfernen
|
||||
window.removeEventListener('message', this.handlePostMessage);
|
||||
|
||||
// URL-Überwachung stoppen
|
||||
if (this.urlCheckInterval) {
|
||||
clearInterval(this.urlCheckInterval);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
onIframeLoad() {
|
||||
console.log('🔄 Iframe geladen, URL:', this.$refs.reportIframe?.src);
|
||||
|
||||
// Warte kurz, damit das iframe vollständig geladen ist
|
||||
setTimeout(() => {
|
||||
this.injectContentScript();
|
||||
}, 2000);
|
||||
|
||||
// Überwache URL-Änderungen im iframe
|
||||
this.startUrlMonitoring();
|
||||
},
|
||||
|
||||
injectContentScript() {
|
||||
try {
|
||||
const iframe = this.$refs.reportIframe;
|
||||
if (!iframe || !iframe.contentWindow) {
|
||||
console.log('Iframe noch nicht bereit für Content Script');
|
||||
return;
|
||||
}
|
||||
|
||||
// Content Script als String definieren
|
||||
const contentScript = `
|
||||
(function() {
|
||||
console.log('Content Script geladen');
|
||||
|
||||
// Warte bis die Seite vollständig geladen ist
|
||||
function waitForElement(selector, callback) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
callback(element);
|
||||
} else {
|
||||
setTimeout(() => waitForElement(selector, callback), 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Suche nach dem Input-Feld
|
||||
waitForElement('#gamecode', function(input) {
|
||||
console.log('Input-Feld gefunden:', input);
|
||||
|
||||
// Code einfügen
|
||||
input.value = '${this.match.code}';
|
||||
|
||||
// Events auslösen
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('blur', { bubbles: true }));
|
||||
|
||||
console.log('Code eingefügt:', '${this.match.code}');
|
||||
|
||||
// Suche nach dem Button und klicke ihn
|
||||
setTimeout(() => {
|
||||
const button = document.querySelector('button.btn-primary');
|
||||
if (button) {
|
||||
console.log('Button gefunden, klicke ihn');
|
||||
button.click();
|
||||
} else {
|
||||
console.log('Button nicht gefunden');
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
})();
|
||||
`;
|
||||
|
||||
// Script in das iframe injizieren
|
||||
const script = iframe.contentDocument.createElement('script');
|
||||
script.textContent = contentScript;
|
||||
iframe.contentDocument.head.appendChild(script);
|
||||
|
||||
console.log('Content Script injiziert');
|
||||
|
||||
} catch (error) {
|
||||
console.log('Fehler beim Injizieren des Content Scripts:', error);
|
||||
// Fallback zu PostMessage
|
||||
this.tryPostMessage();
|
||||
}
|
||||
},
|
||||
|
||||
fillGameCode() {
|
||||
try {
|
||||
const iframe = this.$refs.reportIframe;
|
||||
if (!iframe || !iframe.contentWindow) {
|
||||
console.log('Iframe noch nicht bereit');
|
||||
return;
|
||||
}
|
||||
|
||||
// Versuche, das Input-Feld zu finden und zu füllen
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
|
||||
if (iframeDoc) {
|
||||
const gameCodeInput = iframeDoc.getElementById('gamecode');
|
||||
if (gameCodeInput) {
|
||||
// Code in das Input-Feld einfügen
|
||||
gameCodeInput.value = this.match.code;
|
||||
|
||||
// Event auslösen, damit Angular die Änderung erkennt
|
||||
gameCodeInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
gameCodeInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
console.log('Spielcode erfolgreich eingefügt:', this.match.code);
|
||||
|
||||
// Optional: Automatisch den Button klicken
|
||||
setTimeout(() => {
|
||||
this.clickLoadButton(iframeDoc);
|
||||
}, 500);
|
||||
} else {
|
||||
console.log('Input-Feld mit ID "gamecode" nicht gefunden');
|
||||
}
|
||||
} else {
|
||||
console.log('Kein Zugriff auf iframe-Dokument (Cross-Origin)');
|
||||
// Fallback: PostMessage verwenden
|
||||
this.tryPostMessage();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Fehler beim Zugriff auf iframe:', error);
|
||||
// Fallback: PostMessage verwenden
|
||||
this.tryPostMessage();
|
||||
}
|
||||
},
|
||||
|
||||
clickLoadButton(iframeDoc) {
|
||||
try {
|
||||
// Suche nach dem Button (verschiedene Selektoren)
|
||||
const buttonSelectors = [
|
||||
'button.btn-primary',
|
||||
'button[type="button"]',
|
||||
'button:contains("Laden")'
|
||||
];
|
||||
|
||||
let loadButton = null;
|
||||
for (const selector of buttonSelectors) {
|
||||
loadButton = iframeDoc.querySelector(selector);
|
||||
if (loadButton) break;
|
||||
}
|
||||
|
||||
if (loadButton) {
|
||||
loadButton.click();
|
||||
console.log('Laden-Button erfolgreich geklickt');
|
||||
} else {
|
||||
console.log('Laden-Button nicht gefunden');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Fehler beim Klicken des Buttons:', error);
|
||||
}
|
||||
},
|
||||
|
||||
tryPostMessage() {
|
||||
try {
|
||||
const iframe = this.$refs.reportIframe;
|
||||
if (iframe && iframe.contentWindow) {
|
||||
// Sende Nachricht an das iframe
|
||||
iframe.contentWindow.postMessage({
|
||||
action: 'fillGameCode',
|
||||
code: this.match.code
|
||||
}, 'https://ttde-apps.liga.nu');
|
||||
|
||||
console.log('PostMessage gesendet mit Code:', this.match.code);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Fehler beim Senden der PostMessage:', error);
|
||||
}
|
||||
},
|
||||
|
||||
startUrlMonitoring() {
|
||||
console.log('🔍 Starte URL-Überwachung für iframe');
|
||||
console.log('💡 Hinweis: PIN-Einfügung funktioniert am besten nach der Code-Eingabe und Weiterleitung');
|
||||
console.log('📋 Verwenden Sie den "📌 PIN einfügen" Button nach der Weiterleitung zur Meeting-Seite');
|
||||
|
||||
// Einfache Überwachung ohne Cross-Origin-Zugriff
|
||||
this.urlCheckInterval = setInterval(() => {
|
||||
const iframe = this.$refs.reportIframe;
|
||||
if (iframe) {
|
||||
console.log('🔗 Iframe aktiv, bereit für PIN-Einfügung');
|
||||
}
|
||||
}, 10000); // Alle 10 Sekunden
|
||||
|
||||
// Stoppe Überwachung nach 60 Sekunden
|
||||
setTimeout(() => {
|
||||
if (this.urlCheckInterval) {
|
||||
clearInterval(this.urlCheckInterval);
|
||||
console.log('⏰ URL-Überwachung beendet (Timeout)');
|
||||
}
|
||||
}, 60000);
|
||||
},
|
||||
|
||||
extractMeetingId(url) {
|
||||
const match = url.match(/\/meeting\/([a-f0-9-]+)\//);
|
||||
return match ? match[1] : null;
|
||||
},
|
||||
|
||||
attemptPinInsertionAfterRedirect() {
|
||||
console.log('🎯 Versuche PIN-Einfügung (manuell ausgelöst)');
|
||||
|
||||
const iframe = this.$refs.reportIframe;
|
||||
if (!iframe || !this.match.homePin) {
|
||||
console.log('❌ Iframe oder PIN nicht verfügbar');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
|
||||
// Suche nach PIN-Feldern auf der Meeting-Seite
|
||||
const pinSelectors = [
|
||||
'input[type="password"][placeholder*="Vereins"]',
|
||||
'input[type="password"][placeholder*="Spiel-Pin"]',
|
||||
'input[type="password"][placeholder*="PIN"]',
|
||||
'input[type="password"]',
|
||||
'input[placeholder*="Vereins"]',
|
||||
'input[placeholder*="Spiel-Pin"]'
|
||||
];
|
||||
|
||||
let pinField = null;
|
||||
for (const selector of pinSelectors) {
|
||||
pinField = iframeDoc.querySelector(selector);
|
||||
if (pinField) {
|
||||
console.log(`✅ PIN-Feld gefunden mit Selektor: ${selector}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pinField) {
|
||||
console.log('📝 Füge PIN ein:', this.match.homePin);
|
||||
pinField.value = this.match.homePin;
|
||||
pinField.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
pinField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
pinField.dispatchEvent(new Event('blur', { bubbles: true }));
|
||||
console.log('✅ PIN erfolgreich eingefügt');
|
||||
} else {
|
||||
console.log('❌ PIN-Feld nicht gefunden');
|
||||
console.log('🔍 Verfügbare Input-Felder:', iframeDoc.querySelectorAll('input'));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log('🚫 Cross-Origin-Zugriff blockiert (erwartet)');
|
||||
|
||||
// Fallback: PostMessage
|
||||
const message = {
|
||||
action: 'fillPin',
|
||||
pin: this.match.homePin,
|
||||
timestamp: Date.now(),
|
||||
source: 'trainingstagebuch'
|
||||
};
|
||||
|
||||
console.log('📤 Sende PostMessage:', message);
|
||||
iframe.contentWindow.postMessage(message, '*');
|
||||
}
|
||||
},
|
||||
|
||||
// Methode, die vom DialogManager aufgerufen werden kann
|
||||
insertPinManually() {
|
||||
console.log('🎯 PIN-Einfügung manuell ausgelöst');
|
||||
this.attemptPinInsertionAfterRedirect();
|
||||
},
|
||||
|
||||
handlePostMessage(event) {
|
||||
// Nur Nachrichten von nuscore verarbeiten
|
||||
if (event.origin !== 'https://ttde-apps.liga.nu') {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('PostMessage empfangen:', event.data);
|
||||
|
||||
// Hier können wir auf Antworten von nuscore reagieren
|
||||
if (event.data.action === 'codeFilled') {
|
||||
console.log('Code wurde erfolgreich eingefügt');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.match-report-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.match-info {
|
||||
background: #f8f9fa;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.match-info h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #007bff;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.match-info p {
|
||||
margin: 4px 0;
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.code-display {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: #e3f2fd;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-top: 12px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.instructions p {
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: 600;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.instructions ol {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.instructions li {
|
||||
margin: 4px 0;
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.report-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
107
frontend/src/components/MatchReportHeaderActions.vue
Normal file
107
frontend/src/components/MatchReportHeaderActions.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="match-report-header-actions">
|
||||
<!-- <button @click="insertPin" class="header-action-btn" title="PIN automatisch einfügen">
|
||||
📌 PIN einfügen
|
||||
</button>-->
|
||||
<button @click="copyPin" class="header-action-btn copy-button" title="PIN in Zwischenablage kopieren">
|
||||
📋 PIN kopieren
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MatchReportHeaderActions',
|
||||
props: {
|
||||
dialogId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
match: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
insertPin() {
|
||||
this.$emit('action', {
|
||||
type: 'insertPin',
|
||||
dialogId: this.dialogId,
|
||||
match: this.match
|
||||
});
|
||||
},
|
||||
|
||||
async copyPin() {
|
||||
const pin = this.match.homePin || this.match.guestPin;
|
||||
if (!pin) {
|
||||
console.warn('⚠️ Keine PIN verfügbar zum Kopieren');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(pin);
|
||||
console.log('✅ PIN erfolgreich kopiert:', pin);
|
||||
|
||||
// Visuelles Feedback
|
||||
const button = event.target;
|
||||
const originalText = button.textContent;
|
||||
button.textContent = '✅ Kopiert!';
|
||||
button.style.backgroundColor = '#28a745';
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.style.backgroundColor = '';
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Kopieren der PIN:', error);
|
||||
|
||||
// Fallback: Text-Auswahl
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = pin;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
console.log('✅ PIN über Fallback kopiert:', pin);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.match-report-header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: background-color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.header-action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
background: rgba(0, 123, 255, 0.2);
|
||||
border-color: rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
background: rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,8 @@ const store = createStore({
|
||||
this.clubs = [];
|
||||
}
|
||||
})(),
|
||||
dialogs: [], // Array von offenen Dialogen
|
||||
dialogCounter: 0, // Zähler für eindeutige Dialog-IDs
|
||||
},
|
||||
mutations: {
|
||||
setToken(state, token) {
|
||||
@@ -42,6 +44,43 @@ const store = createStore({
|
||||
clearUsername(state) {
|
||||
state.username = '';
|
||||
localStorage.removeItem('username');
|
||||
},
|
||||
// Dialog-Mutations
|
||||
openDialog(state, dialog) {
|
||||
const id = ++state.dialogCounter;
|
||||
const newDialog = {
|
||||
id,
|
||||
...dialog,
|
||||
isMinimized: false,
|
||||
zIndex: 1000 + state.dialogs.length,
|
||||
position: { x: 0, y: 0 } // Position wird durch CSS festgelegt
|
||||
};
|
||||
state.dialogs.push(newDialog);
|
||||
},
|
||||
closeDialog(state, dialogId) {
|
||||
state.dialogs = state.dialogs.filter(dialog => dialog.id !== dialogId);
|
||||
},
|
||||
minimizeDialog(state, dialogId) {
|
||||
const dialog = state.dialogs.find(d => d.id === dialogId);
|
||||
if (dialog) {
|
||||
dialog.isMinimized = true;
|
||||
}
|
||||
},
|
||||
restoreDialog(state, dialogId) {
|
||||
const dialog = state.dialogs.find(d => d.id === dialogId);
|
||||
if (dialog) {
|
||||
dialog.isMinimized = false;
|
||||
// Dialog in den Vordergrund bringen
|
||||
const maxZIndex = Math.max(...state.dialogs.map(d => d.zIndex));
|
||||
dialog.zIndex = maxZIndex + 1;
|
||||
}
|
||||
},
|
||||
bringDialogToFront(state, dialogId) {
|
||||
const dialog = state.dialogs.find(d => d.id === dialogId);
|
||||
if (dialog) {
|
||||
const maxZIndex = Math.max(...state.dialogs.map(d => d.zIndex));
|
||||
dialog.zIndex = maxZIndex + 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@@ -63,6 +102,22 @@ const store = createStore({
|
||||
},
|
||||
setClubs({ commit }, clubs) {
|
||||
commit('setClubsMutation', clubs);
|
||||
},
|
||||
// Dialog-Actions
|
||||
openDialog({ commit }, dialog) {
|
||||
commit('openDialog', dialog);
|
||||
},
|
||||
closeDialog({ commit }, dialogId) {
|
||||
commit('closeDialog', dialogId);
|
||||
},
|
||||
minimizeDialog({ commit }, dialogId) {
|
||||
commit('minimizeDialog', dialogId);
|
||||
},
|
||||
restoreDialog({ commit }, dialogId) {
|
||||
commit('restoreDialog', dialogId);
|
||||
},
|
||||
bringDialogToFront({ commit }, dialogId) {
|
||||
commit('bringDialogToFront', dialogId);
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
@@ -74,7 +129,11 @@ const store = createStore({
|
||||
currentClubName: state => {
|
||||
const club = state.clubs.find(club => club.id === parseInt(state.currentClub));
|
||||
return club ? club.name : '';
|
||||
}
|
||||
},
|
||||
// Dialog-Getters
|
||||
dialogs: state => state.dialogs,
|
||||
minimizedDialogs: state => state.dialogs.filter(dialog => dialog.isMinimized),
|
||||
activeDialogs: state => state.dialogs.filter(dialog => !dialog.isMinimized)
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -49,7 +49,11 @@
|
||||
<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 clickable" @click="copyToClipboard(match.code, 'Code')" :title="'Code kopieren: ' + match.code">{{ match.code }}</span>
|
||||
<span v-if="match.code && selectedLeague && selectedLeague !== ''">
|
||||
<button @click="openMatchReport(match)" class="nuscore-link" title="Spielberichtsbogen öffnen">📊</button>
|
||||
<span class="code-value clickable" @click="copyToClipboard(match.code, 'Code')" :title="'Code kopieren: ' + match.code">{{ match.code }}</span>
|
||||
</span>
|
||||
<span v-else-if="match.code" class="code-value clickable" @click="copyToClipboard(match.code, 'Code')" :title="'Code kopieren: ' + match.code">{{ match.code }}</span>
|
||||
<span v-else class="no-data">-</span>
|
||||
</td>
|
||||
<td class="pin-cell">
|
||||
@@ -85,15 +89,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import apiClient from '../apiClient.js';
|
||||
import PDFGenerator from '../components/PDFGenerator.js';
|
||||
import SeasonSelector from '../components/SeasonSelector.vue';
|
||||
import MatchReportDialog from '../components/MatchReportDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'ScheduleView',
|
||||
components: {
|
||||
SeasonSelector
|
||||
SeasonSelector,
|
||||
MatchReportDialog
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'currentClubName']),
|
||||
@@ -111,6 +117,7 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['openDialog']),
|
||||
openImportModal() {
|
||||
this.showImportModal = true;
|
||||
},
|
||||
@@ -365,6 +372,23 @@ export default {
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
},
|
||||
|
||||
openMatchReport(match) {
|
||||
const title = `${match.homeTeam?.name || 'N/A'} vs ${match.guestTeam?.name || 'N/A'} - ${this.selectedLeague}`;
|
||||
this.openDialog({
|
||||
title,
|
||||
component: 'MatchReportDialog',
|
||||
props: {
|
||||
match
|
||||
},
|
||||
headerActions: {
|
||||
component: 'MatchReportHeaderActions',
|
||||
props: {
|
||||
match
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
// Ligen werden geladen, sobald eine Saison ausgewählt ist
|
||||
@@ -537,6 +561,22 @@ li {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.nuscore-link {
|
||||
text-decoration: none;
|
||||
margin-right: 8px;
|
||||
font-size: 1.2rem;
|
||||
transition: transform 0.2s ease;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.nuscore-link:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.pin-value {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
|
||||
Reference in New Issue
Block a user