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:
Torsten Schulz (local)
2025-10-02 11:44:27 +02:00
parent dbede48d4f
commit cc964da9cf
6 changed files with 1013 additions and 4 deletions

View File

@@ -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,

View 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>

View 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>

View 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>

View File

@@ -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)
},
});

View File

@@ -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;