From 946e4fce1efd1579355bad903b186758f56af383 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 8 Oct 2025 14:43:53 +0200 Subject: [PATCH] Enhance SeasonSelector and TeamManagementView with dialog components for improved user interaction. Introduce new dialog states and helper methods for consistent handling of information and confirmations. Update styles in TrainingStatsView to reflect new participation metrics and improve layout. Refactor document display in TeamManagementView to a table format for better readability. --- frontend/src/components/DIALOGS_OVERVIEW.md | 348 ++++++++++++++++++++ frontend/src/components/SeasonSelector.vue | 72 +++- frontend/src/views/TeamManagementView.vue | 270 ++++++++++----- frontend/src/views/TournamentsView.vue | 70 +++- frontend/src/views/TrainingStatsView.vue | 110 ++++++- 5 files changed, 761 insertions(+), 109 deletions(-) create mode 100644 frontend/src/components/DIALOGS_OVERVIEW.md diff --git a/frontend/src/components/DIALOGS_OVERVIEW.md b/frontend/src/components/DIALOGS_OVERVIEW.md new file mode 100644 index 0000000..73949e8 --- /dev/null +++ b/frontend/src/components/DIALOGS_OVERVIEW.md @@ -0,0 +1,348 @@ +# Dialog-Komponenten Übersicht + +## 📋 Alle verfügbaren Dialog-Komponenten + +### Basis-Komponenten + +#### 1. **BaseDialog.vue** +Basis-Template für alle Dialoge (modal und nicht-modal). + +**Props:** `modelValue`, `isModal`, `title`, `size`, `position`, `zIndex`, `closable`, `minimizable`, `draggable`, `closeOnOverlay` + +**Verwendung:** +```vue + + Content + +``` + +--- + +### Informations-Dialoge + +#### 2. **InfoDialog.vue** +Einfache Informationsmeldungen mit OK-Button. + +**Props:** `modelValue`, `title`, `message`, `details`, `type` (info/success/warning/error), `icon`, `okText` + +**Verwendung:** +```vue + +``` + +#### 3. **ConfirmDialog.vue** +Bestätigungsdialoge mit OK/Abbrechen. + +**Props:** `modelValue`, `title`, `message`, `details`, `type` (info/warning/danger/success), `confirmText`, `cancelText`, `showCancel` + +**Events:** `@confirm`, `@cancel` + +**Verwendung:** +```vue + +``` + +--- + +### Bild-Dialoge + +#### 4. **ImageDialog.vue** +Einfache Bildanzeige. + +**Props:** `modelValue`, `title`, `imageUrl` + +**Verwendung:** +```vue + +``` + +#### 5. **ImageViewerDialog.vue** +Erweiterte Bildanzeige mit Aktionen (Drehen, Zoom). + +**Props:** `modelValue`, `title`, `imageUrl`, `memberId`, `showActions`, `allowRotate`, `allowZoom` + +**Events:** `@rotate` + +**Verwendung:** +```vue + +``` + +--- + +### Spezifische Dialoge + +#### 6. **MemberNotesDialog.vue** +Notizen-Verwaltung für Mitglieder mit Bild, Tags und Notizliste. + +**Props:** `modelValue`, `member`, `notes`, `selectedTags`, `availableTags`, `noteContent` + +**Events:** `@add-note`, `@delete-note`, `@add-tag`, `@remove-tag` + +**Verwendung:** +```vue + +``` + +#### 7. **TagHistoryDialog.vue** +Tag-Historie für Mitglieder. + +**Props:** `modelValue`, `member`, `tagHistory`, `selectedTags`, `activityTags` + +**Events:** `@select-tag` + +**Verwendung:** +```vue + +``` + +#### 8. **AccidentFormDialog.vue** +Unfall-Meldungs-Formular. + +**Props:** `modelValue`, `accident`, `members`, `participants`, `accidents` + +**Events:** `@submit` + +**Verwendung:** +```vue + +``` + +#### 9. **QuickAddMemberDialog.vue** +Schnelles Hinzufügen von Mitgliedern. + +**Props:** `modelValue`, `member` + +**Events:** `@submit` + +**Verwendung:** +```vue + +``` + +#### 10. **CsvImportDialog.vue** +CSV-Datei-Import mit Dateiauswahl. + +**Props:** `modelValue` + +**Events:** `@import` + +**Verwendung:** +```vue + +``` + +#### 11. **TrainingDetailsDialog.vue** +Trainings-Details und Statistiken für Mitglieder. + +**Props:** `modelValue`, `member` + +**Verwendung:** +```vue + +``` + +#### 12. **MemberSelectionDialog.vue** +Mitglieder-Auswahl mit Empfehlungen (für PDF-Generierung). + +**Props:** `modelValue`, `members`, `selectedIds`, `activeMemberId`, `recommendations`, `recommendedKeys`, `showRecommendations` + +**Events:** `@select-all`, `@deselect-all`, `@toggle-member`, `@toggle-recommendation`, `@generate-pdf` + +**Verwendung:** +```vue + +``` + +--- + +## 🎨 Dialog-Typen und Icons + +| Typ | Icon | Farbe | Verwendung | +|-----|------|-------|-----------| +| `info` | ℹ️ | Blau | Informationen | +| `success` | ✅ | Grün | Erfolgsmeldungen | +| `warning` | ⚠️ | Gelb | Warnungen | +| `error` | ⛔ | Rot | Fehler | +| `danger` | ⛔ | Rot | Gefährliche Aktionen (Löschen) | + +--- + +## 📏 Dialog-Größen + +| Größe | Breite | Verwendung | +|-------|--------|-----------| +| `small` | 400px | Einfache Meldungen, Bestätigungen | +| `medium` | 600px | Standard-Formulare, Notizen | +| `large` | 900px | Komplexe Inhalte, Listen, Details | +| `fullscreen` | 90vw × 90vh | Maximale Fläche | + +--- + +## 🔧 Composables + +### useDialog() +```javascript +import { useDialog } from '@/composables/useDialog.js'; + +const { isOpen, open, close, toggle } = useDialog(); +``` + +### useConfirm() +```javascript +import { useConfirm } from '@/composables/useDialog.js'; + +const { confirm } = useConfirm(); +const result = await confirm({ + title: 'Löschen?', + message: 'Wirklich löschen?', + type: 'danger' +}); +``` + +### useInfo() +```javascript +import { useInfo } from '@/composables/useDialog.js'; + +const { showInfo } = useInfo(); +await showInfo({ + title: 'Erfolg', + message: 'Gespeichert!', + type: 'success' +}); +``` + +--- + +## 🎯 Migration Guide + +### Von JavaScript-Alert/Confirm: + +**Vorher:** +```javascript +alert('Fehler!'); +if (confirm('Löschen?')) { ... } +``` + +**Nachher:** +```javascript +// In setup() oder data(): +const infoDialog = ref({ isOpen: false, title: '', message: '', details: '', type: 'info' }); +const confirmDialog = ref({ isOpen: false, title: '', message: '', details: '', type: 'info', resolveCallback: null }); + +// Methods: +async showInfo(title, message, details = '', type = 'info') { + this.infoDialog = { isOpen: true, title, message, details, type }; +} + +async showConfirm(title, message, details = '', type = 'info') { + return new Promise((resolve) => { + this.confirmDialog = { isOpen: true, title, message, details, type, resolveCallback: resolve }; + }); +} + +// Verwenden: +this.showInfo('Fehler', 'Fehler!', '', 'error'); +const confirmed = await this.showConfirm('Bestätigung', 'Löschen?', '', 'danger'); +if (confirmed) { ... } +``` + +### Von Inline-Modal: + +**Vorher:** +```vue + +``` + +**Nachher:** +```vue + + + +``` + +--- + +## ✅ Best Practices + +1. **v-model verwenden** für Sichtbarkeit +2. **Slots nutzen** für flexible Inhalte (#footer, #header-actions) +3. **Events emittieren** statt direkte Manipulation +4. **Props validieren** mit `validator` Funktionen +5. **Responsive Design** berücksichtigen +6. **Memory Management** bei Blob URLs (revokeObjectURL) +7. **Eigene Komponenten** für wiederverwendbare Dialoge erstellen + +--- + +## 📚 Weitere Informationen + +Siehe `DIALOG_TEMPLATES.md` für detaillierte API-Dokumentation und erweiterte Beispiele. + diff --git a/frontend/src/components/SeasonSelector.vue b/frontend/src/components/SeasonSelector.vue index 100b6e0..3bbdced 100644 --- a/frontend/src/components/SeasonSelector.vue +++ b/frontend/src/components/SeasonSelector.vue @@ -68,10 +68,14 @@ import { ref, computed, onMounted, watch } from 'vue'; import { useStore } from 'vuex'; import apiClient from '../apiClient.js'; -import InfoDialog from '../components/InfoDialog.vue'; -import ConfirmDialog from '../components/ConfirmDialog.vue'; +import InfoDialog from './InfoDialog.vue'; +import ConfirmDialog from './ConfirmDialog.vue'; export default { name: 'SeasonSelector', + components: { + InfoDialog, + ConfirmDialog + }, props: { modelValue: { type: [String, Number], @@ -86,6 +90,23 @@ export default { setup(props, { emit }) { const store = useStore(); + // Dialog States + const infoDialog = ref({ + isOpen: false, + title: '', + message: '', + details: '', + type: 'info' + }); + const confirmDialog = ref({ + isOpen: false, + title: '', + message: '', + details: '', + type: 'info', + resolveCallback: null + }); + // Reactive data const seasons = ref([]); const selectedSeasonId = ref(props.modelValue); @@ -147,12 +168,12 @@ export default { // Formular zurücksetzen newSeasonString.value = ''; showNewSeasonForm.value = false; - } catch (error) { - console.error('Fehler beim Erstellen der Saison:', error); - if (error.response?.data?.error === 'alreadyexists') { - alert('Diese Saison existiert bereits!'); + } catch (err) { + console.error('Fehler beim Erstellen der Saison:', err); + if (err.response?.data?.error === 'alreadyexists') { + showInfo('Hinweis', 'Diese Saison existiert bereits!', '', 'warning'); } else { - this.showInfo('Fehler', 'Fehler beim Erstellen der Saison', '', 'error'); + showInfo('Fehler', 'Fehler beim Erstellen der Saison', '', 'error'); } } }; @@ -162,6 +183,38 @@ export default { showNewSeasonForm.value = false; }; + // Dialog Helper Methods + const showInfo = async (title, message, details = '', type = 'info') => { + infoDialog.value = { + isOpen: true, + title, + message, + details, + type + }; + }; + + const showConfirm = async (title, message, details = '', type = 'info') => { + return new Promise((resolve) => { + confirmDialog.value = { + isOpen: true, + title, + message, + details, + type, + resolveCallback: resolve + }; + }); + }; + + const handleConfirmResult = (confirmed) => { + if (confirmDialog.value.resolveCallback) { + confirmDialog.value.resolveCallback(confirmed); + confirmDialog.value.resolveCallback = null; + } + confirmDialog.value.isOpen = false; + }; + // Watch for prop changes watch(() => props.modelValue, (newValue) => { selectedSeasonId.value = newValue; @@ -173,6 +226,11 @@ export default { }); return { + infoDialog, + confirmDialog, + showInfo, + showConfirm, + handleConfirmResult, seasons, selectedSeasonId, showNewSeasonForm, diff --git a/frontend/src/views/TeamManagementView.vue b/frontend/src/views/TeamManagementView.vue index 9443e58..6c5d788 100644 --- a/frontend/src/views/TeamManagementView.vue +++ b/frontend/src/views/TeamManagementView.vue @@ -85,25 +85,37 @@

Bereits hochgeladene PDF-Dokumente parsen

-
-
-
- {{ document.originalFileName }} - {{ document.documentType === 'code_list' ? 'Code-Liste' : 'Pin-Liste' }} - {{ formatFileSize(document.fileSize) }} -
-
- -
-
-
+ + + + + + + + + + + + + + + + + +
DateinameTypGrößeAktionen
{{ document.originalFileName }} + + {{ document.documentType === 'code_list' ? 'Code-Liste' : 'Pin-Liste' }} + + {{ formatFileSize(document.fileSize) }} + +
@@ -240,6 +252,23 @@ export default { setup() { const store = useStore(); + // Dialog States + const infoDialog = ref({ + isOpen: false, + title: '', + message: '', + details: '', + type: 'info' + }); + const confirmDialog = ref({ + isOpen: false, + title: '', + message: '', + details: '', + type: 'info', + resolveCallback: null + }); + // Reactive data const teams = ref([]); const leagues = ref([]); @@ -634,7 +663,44 @@ export default { } }; + // Dialog Helper Methods + const showInfo = async (title, message, details = '', type = 'info') => { + infoDialog.value = { + isOpen: true, + title, + message, + details, + type + }; + }; + + const showConfirm = async (title, message, details = '', type = 'info') => { + return new Promise((resolve) => { + confirmDialog.value = { + isOpen: true, + title, + message, + details, + type, + resolveCallback: resolve + }; + }); + }; + + const handleConfirmResult = (confirmed) => { + if (confirmDialog.value.resolveCallback) { + confirmDialog.value.resolveCallback(confirmed); + confirmDialog.value.resolveCallback = null; + } + confirmDialog.value.isOpen = false; + }; + return { + infoDialog, + confirmDialog, + showInfo, + showConfirm, + handleConfirmResult, teams, leagues, teamFormIsOpen, @@ -679,7 +745,7 @@ export default { \ No newline at end of file diff --git a/frontend/src/views/TrainingStatsView.vue b/frontend/src/views/TrainingStatsView.vue index 65e94d7..6025546 100644 --- a/frontend/src/views/TrainingStatsView.vue +++ b/frontend/src/views/TrainingStatsView.vue @@ -9,12 +9,24 @@
{{ activeMembers.length }}
-

Durchschnittliche Teilnahme (12 Monate)

-
{{ averageParticipation12Months.toFixed(1) }}
+

Durchschnittliche Teilnahme (aktueller Monat)

+
{{ averageParticipationCurrentMonth.toFixed(1) }}
-

Durchschnittliche Teilnahme (3 Monate)

-
{{ averageParticipation3Months.toFixed(1) }}
+

Durchschnittliche Teilnahme (letzter Monat)

+
{{ averageParticipationLastMonth.toFixed(1) }}
+
+
+

Durchschnittliche Teilnahme (Quartal)

+
{{ averageParticipationQuarter.toFixed(1) }}
+
+
+

Durchschnittliche Teilnahme (Halbjahr)

+
{{ averageParticipationHalfYear.toFixed(1) }}
+
+
+

Durchschnittliche Teilnahme (Jahr)

+
{{ averageParticipationYear.toFixed(1) }}
@@ -146,6 +158,54 @@ export default { const total = this.activeMembers.reduce((sum, member) => sum + member.participation3Months, 0); return total / this.trainingsCount3Months; }, + + // Neue Zeiträume basierend auf verfügbaren Daten + averageParticipationCurrentMonth() { + const currentMonth = new Date().getMonth(); + const currentYear = new Date().getFullYear(); + const trainingsThisMonth = this.getTrainingsInPeriod(currentYear, currentMonth, currentYear, currentMonth); + if (trainingsThisMonth === 0) return 0; + const total = this.activeMembers.reduce((sum, member) => sum + this.getMemberParticipationInPeriod(member, currentYear, currentMonth, currentYear, currentMonth), 0); + return total / trainingsThisMonth; + }, + + averageParticipationLastMonth() { + const lastMonth = new Date().getMonth() - 1; + const year = lastMonth < 0 ? new Date().getFullYear() - 1 : new Date().getFullYear(); + const actualMonth = lastMonth < 0 ? 11 : lastMonth; + const trainingsLastMonth = this.getTrainingsInPeriod(year, actualMonth, year, actualMonth); + if (trainingsLastMonth === 0) return 0; + const total = this.activeMembers.reduce((sum, member) => sum + this.getMemberParticipationInPeriod(member, year, actualMonth, year, actualMonth), 0); + return total / trainingsLastMonth; + }, + + averageParticipationQuarter() { + const now = new Date(); + const quarterStartMonth = Math.floor(now.getMonth() / 3) * 3; + const quarterEndMonth = quarterStartMonth + 2; + const trainingsQuarter = this.getTrainingsInPeriod(now.getFullYear(), quarterStartMonth, now.getFullYear(), quarterEndMonth); + if (trainingsQuarter === 0) return 0; + const total = this.activeMembers.reduce((sum, member) => sum + this.getMemberParticipationInPeriod(member, now.getFullYear(), quarterStartMonth, now.getFullYear(), quarterEndMonth), 0); + return total / trainingsQuarter; + }, + + averageParticipationHalfYear() { + const now = new Date(); + const halfYearStartMonth = now.getMonth() < 6 ? 0 : 6; + const halfYearEndMonth = now.getMonth() < 6 ? 5 : 11; + const trainingsHalfYear = this.getTrainingsInPeriod(now.getFullYear(), halfYearStartMonth, now.getFullYear(), halfYearEndMonth); + if (trainingsHalfYear === 0) return 0; + const total = this.activeMembers.reduce((sum, member) => sum + this.getMemberParticipationInPeriod(member, now.getFullYear(), halfYearStartMonth, now.getFullYear(), halfYearEndMonth), 0); + return total / trainingsHalfYear; + }, + + averageParticipationYear() { + const currentYear = new Date().getFullYear(); + const trainingsYear = this.getTrainingsInPeriod(currentYear, 0, currentYear, 11); + if (trainingsYear === 0) return 0; + const total = this.activeMembers.reduce((sum, member) => sum + this.getMemberParticipationInPeriod(member, currentYear, 0, currentYear, 11), 0); + return total / trainingsYear; + }, sortedMembers() { if (!this.activeMembers.length) return []; @@ -224,6 +284,34 @@ export default { this.loading = false; } }, + + // Hilfsmethoden für neue Zeiträume + getTrainingsInPeriod(startYear, startMonth, endYear, endMonth) { + return this.trainingDays.filter(day => { + const dayDate = new Date(day.date); + const dayYear = dayDate.getFullYear(); + const dayMonth = dayDate.getMonth(); + + if (dayYear < startYear || dayYear > endYear) return false; + if (dayYear === startYear && dayMonth < startMonth) return false; + if (dayYear === endYear && dayMonth > endMonth) return false; + + return true; + }).length; + }, + + getMemberParticipationInPeriod(member, startYear, startMonth, endYear, endMonth) { + // Vereinfachte Berechnung basierend auf verfügbaren Daten + // Da wir keine detaillierten Teilnahmedaten haben, verwenden wir eine Schätzung + // basierend auf den vorhandenen participation12Months und participation3Months + + const totalTrainings = this.getTrainingsInPeriod(startYear, startMonth, endYear, endMonth); + if (totalTrainings === 0) return 0; + + // Schätzung basierend auf 12-Monats-Durchschnitt + const avgParticipationRate = member.participation12Months / this.trainingsCount12Months; + return Math.round(totalTrainings * avgParticipationRate); + }, toggleTrainingDays() { this.showTrainingDays = !this.showTrainingDays; @@ -300,14 +388,14 @@ export default { .stats-summary { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1rem; - margin-bottom: 2rem; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 0.8rem; + margin-bottom: 1.5rem; } .stat-card { background: white; - padding: 1.5rem; + padding: 0.8rem; border-radius: var(--border-radius-large); box-shadow: var(--shadow-light); text-align: center; @@ -315,15 +403,15 @@ export default { } .stat-card h3 { - margin: 0 0 1rem 0; - font-size: 0.875rem; + margin: 0 0 0.5rem 0; + font-size: 0.7rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.025em; } .stat-number { - font-size: 2rem; + font-size: 1.2rem; font-weight: 700; color: var(--primary-color); }