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