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) }}
-
-
-
-
-
-
+
+
+
+ | Dateiname |
+ Typ |
+ Größe |
+ Aktionen |
+
+
+
+
+ | {{ 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);
}