diff --git a/backend/controllers/memberController.js b/backend/controllers/memberController.js
index 23b20cb..36ddbc1 100644
--- a/backend/controllers/memberController.js
+++ b/backend/controllers/memberController.js
@@ -59,7 +59,9 @@ const getMemberImage = async (req, res) => {
try {
const { clubId, memberId, imageId } = req.params;
const { authcode: userToken } = req.headers;
- const result = await MemberService.getMemberImage(userToken, clubId, memberId, imageId || null);
+ // Support "latest" as imageId to get the latest image
+ const actualImageId = imageId === 'latest' ? null : (imageId || null);
+ const result = await MemberService.getMemberImage(userToken, clubId, memberId, actualImageId);
if (result.status === 200) {
res.sendFile(result.imagePath);
} else {
@@ -121,8 +123,20 @@ const generateMemberGallery = async (req, res) => {
const { clubId } = req.params;
const { authcode: userToken } = req.headers;
const size = parseInt(req.query.size) || 200; // Default: 200x200
+ const format = req.query.format || 'image'; // 'image' or 'json'
const result = await MemberService.generateMemberGallery(userToken, clubId, size);
if (result.status === 200) {
+ if (format === 'json') {
+ // Return member information for interactive gallery
+ return res.status(200).json({
+ members: result.galleryEntries.map(entry => ({
+ memberId: entry.memberId,
+ firstName: entry.firstName,
+ lastName: entry.lastName,
+ fullName: entry.fullName
+ }))
+ });
+ }
res.setHeader('Content-Type', 'image/png');
res.setHeader('Cache-Control', 'no-store');
return res.status(200).send(result.buffer);
diff --git a/backend/services/memberService.js b/backend/services/memberService.js
index bf817a7..8f2da14 100644
--- a/backend/services/memberService.js
+++ b/backend/services/memberService.js
@@ -282,9 +282,10 @@ class MemberService {
}
if (!imageRecord) {
+ // Get latest image (highest sortOrder, then highest id)
imageRecord = await MemberImage.findOne({
where: { memberId },
- order: [['sortOrder', 'ASC'], ['id', 'ASC']]
+ order: [['sortOrder', 'DESC'], ['id', 'DESC']]
});
}
@@ -1124,7 +1125,10 @@ class MemberService {
galleryEntries.push({
filePath,
- fullName: `${member.firstName || ''} ${member.lastName || ''}`.trim() || member.lastName || member.firstName || 'Unbekannt'
+ fullName: `${member.firstName || ''} ${member.lastName || ''}`.trim() || member.lastName || member.firstName || 'Unbekannt',
+ memberId: member.id,
+ firstName: member.firstName || '',
+ lastName: member.lastName || ''
});
}
@@ -1194,7 +1198,8 @@ class MemberService {
return {
status: 200,
- buffer
+ buffer,
+ galleryEntries // Für interaktive Galerie
};
} catch (error) {
console.error('[generateMemberGallery] - Error:', error);
diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue
index 49ff314..a254c63 100644
--- a/frontend/src/views/DiaryView.vue
+++ b/frontend/src/views/DiaryView.vue
@@ -504,7 +504,7 @@
-
- Galerie wird erstellt…
-
-
![Mitglieder-Galerie]()
+
Galerie wird geladen…
+
+
+
![]()
+
Kein Bild
+
{{ member.fullName }}
+
- {{ galleryError || 'Keine Galerie verfügbar.' }}
+ {{ galleryError || 'Keine Mitglieder mit Bildern gefunden.' }}
@@ -676,6 +694,7 @@ export default {
galleryImageUrl: null,
galleryError: '',
gallerySize: 200,
+ galleryMembers: [],
editShowDropdown: false,
editSearchResults: [],
editSearchForId: null,
@@ -799,32 +818,108 @@ export default {
return;
}
this.showGalleryDialog = true;
- await this.loadGallery();
+ await this.loadGalleryMembers();
},
- async loadGallery() {
+ updateGallerySizeBasedOnCount() {
+ const count = this.galleryMembers.length;
+ if (count < 11) {
+ this.gallerySize = 200;
+ } else if (count < 22) {
+ this.gallerySize = 150;
+ } else {
+ this.gallerySize = 100;
+ }
+ },
+ async loadGalleryMembers() {
if (!this.currentClub || this.galleryLoading) {
return;
}
this.galleryLoading = true;
this.galleryError = '';
+ this.galleryImageUrl = null;
+ this.revokeGalleryImage();
try {
- const response = await apiClient.get(`/clubmembers/gallery/${this.currentClub}?size=${this.gallerySize}`, { responseType: 'blob' });
- this.revokeGalleryImage();
- this.galleryImageUrl = URL.createObjectURL(response.data);
+ const response = await apiClient.get(`/clubmembers/gallery/${this.currentClub}?format=json&size=${this.gallerySize}`);
+ const members = response.data.members || [];
+
+ // Setze Größe basierend auf Anzahl der Mitglieder (vor dem Laden der Bilder)
+ this.galleryMembers = members; // Temporär setzen für count
+ this.updateGallerySizeBasedOnCount();
+
+ // Lade Bilder als Blobs und erstelle ObjectURLs
+ this.galleryMembers = await Promise.all(members.map(async (member) => {
+ try {
+ const imageUrl = await this.loadMemberImageAsBlob(member.memberId);
+ console.log(`[loadGalleryMembers] Member ${member.memberId} (${member.fullName}): imageUrl =`, imageUrl ? 'OK' : 'null');
+ return {
+ ...member,
+ imageUrl: imageUrl || null
+ };
+ } catch (error) {
+ console.error(`[loadGalleryMembers] Fehler beim Laden des Bildes für Mitglied ${member.memberId}:`, error);
+ return {
+ ...member,
+ imageUrl: null
+ };
+ }
+ }));
+ console.log('[loadGalleryMembers] Geladene Mitglieder:', this.galleryMembers.map(m => ({ id: m.memberId, name: m.fullName, hasImage: !!m.imageUrl })));
} catch (error) {
- console.error('Fehler beim Erstellen der Mitglieder-Galerie:', error);
- this.galleryError = error?.response?.data?.error || 'Galerie konnte nicht erstellt werden.';
- this.showInfo('Fehler', 'Galerie konnte nicht erstellt werden.', this.galleryError, 'error');
+ console.error('Fehler beim Laden der Galerie:', error);
+ this.galleryError = error?.response?.data?.error || 'Galerie konnte nicht geladen werden.';
+ this.galleryMembers = [];
} finally {
this.galleryLoading = false;
}
},
+ async loadMemberImageAsBlob(memberId) {
+ try {
+ const response = await apiClient.get(`/clubmembers/image/${this.currentClub}/${memberId}`, { responseType: 'blob' });
+ if (!response.data || response.data.size === 0) {
+ console.warn(`[loadMemberImageAsBlob] Leeres Blob für Mitglied ${memberId}`);
+ return null;
+ }
+ const objectUrl = URL.createObjectURL(response.data);
+ console.log(`[loadMemberImageAsBlob] ObjectURL erstellt für Mitglied ${memberId}:`, objectUrl.substring(0, 50) + '...');
+ return objectUrl;
+ } catch (error) {
+ // 404 ist OK - Mitglied hat kein Bild
+ if (error?.response?.status === 404) {
+ console.log(`[loadMemberImageAsBlob] Kein Bild für Mitglied ${memberId} (404)`);
+ return null;
+ }
+ console.error(`[loadMemberImageAsBlob] Fehler beim Laden des Bildes für Mitglied ${memberId}:`, error);
+ return null;
+ }
+ },
async regenerateGallery() {
- await this.loadGallery();
+ await this.loadGalleryMembers();
},
closeGalleryDialog() {
this.showGalleryDialog = false;
this.revokeGalleryImage();
+ // Revoke all object URLs
+ this.galleryMembers.forEach(member => {
+ if (member.imageUrl) {
+ URL.revokeObjectURL(member.imageUrl);
+ }
+ });
+ this.galleryMembers = [];
+ },
+ async handleGalleryMemberClick(member) {
+ if (!this.date || this.date === 'new') {
+ return;
+ }
+ console.log('[handleGalleryMemberClick] Clicked member:', member);
+ await this.toggleParticipant(member.memberId);
+ },
+ handleImageError(member) {
+ console.error(`[handleImageError] Bild konnte nicht geladen werden für Mitglied ${member.memberId} (${member.fullName}), URL:`, member.imageUrl);
+ // Setze imageUrl auf null, damit der Placeholder angezeigt wird
+ member.imageUrl = null;
+ },
+ handleImageLoad(member) {
+ console.log(`[handleImageLoad] Bild erfolgreich geladen für Mitglied ${member.memberId} (${member.fullName})`);
},
hasActivityVisual(pa) {
@@ -2510,6 +2605,8 @@ h3 {
gap: 12px;
background: #f9f9f9;
flex-shrink: 0;
+ position: relative;
+ z-index: 10;
}
.gallery-controls label {
@@ -2557,6 +2654,96 @@ h3 {
color: var(--text-color, #333);
}
+.gallery-members-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, max-content);
+ gap: 0;
+ padding: 0;
+ width: 100%;
+ position: relative;
+ z-index: 1;
+ align-items: start;
+ box-sizing: border-box;
+ justify-items: start;
+}
+
+.gallery-member-item {
+ display: block;
+ cursor: pointer;
+ padding: 0;
+ border: none;
+ border-radius: 0;
+ transition: all 0.2s ease;
+ margin: 0;
+ box-sizing: border-box;
+ overflow: hidden;
+ position: relative;
+}
+
+.gallery-member-item:hover {
+ background-color: transparent;
+}
+
+.gallery-member-item.is-participant {
+ background-color: transparent;
+ border: none;
+}
+
+.gallery-member-image {
+ object-fit: cover;
+ border-radius: 0;
+ margin: 0;
+ pointer-events: none; /* Prevent image from blocking clicks on parent */
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ max-width: 100%;
+ max-height: 100%;
+ border: none !important;
+}
+
+.gallery-member-name {
+ font-size: 12px;
+ text-align: center;
+ color: #ff6b6b;
+ font-weight: 500;
+ margin: 0;
+ padding: 4px 8px;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ width: 100%;
+ box-sizing: border-box;
+ background-color: rgba(0, 0, 0, 0.5) !important;
+ z-index: 10;
+}
+
+.gallery-member-item.is-participant .gallery-member-name {
+ color: #51cf66;
+}
+
+.gallery-member-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #f0f0f0;
+ border-radius: 0;
+ color: #999;
+ font-size: 12px;
+ margin: 0;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+}
+
.column:first-child {
flex: 1;
overflow: visible;
diff --git a/frontend/src/views/ScheduleView.vue b/frontend/src/views/ScheduleView.vue
index d4d085d..f7602a5 100644
--- a/frontend/src/views/ScheduleView.vue
+++ b/frontend/src/views/ScheduleView.vue
@@ -5,6 +5,14 @@
+
{{ hoveredMatch.location.name || 'N/A' }}
{{ hoveredMatch.location.address || 'N/A' }}
@@ -173,6 +181,46 @@
:details="confirmDialog.details" :type="confirmDialog.type" @confirm="handleConfirmResult(true)"
@cancel="handleConfirmResult(false)" />
+
+
+
+
+
+
+
+
+
+
+
+
Galerie wird geladen…
+
+
+
![]()
+
{{ member.fullName }}
+
+
+
+ {{ galleryError || 'Keine Mitglieder mit Bildern gefunden.' }}
+
+
+
+
m.hasPlayed)
.map(m => m.id);
+ console.log('[savePlayerSelection] Saving players:', { playersReady, playersPlanned, playersPlayed, matchId: match.id });
+
try {
- await apiClient.patch(`/matches/${match.id}/players`, {
+ const response = await apiClient.patch(`/matches/${match.id}/players`, {
playersReady,
playersPlanned,
playersPlayed
});
+ console.log('[savePlayerSelection] API response:', response);
// Update local match data
match.playersReady = playersReady;
match.playersPlanned = playersPlanned;
match.playersPlayed = playersPlayed;
- await this.showInfo('Erfolg', 'Spielerauswahl gespeichert', '', 'success');
- this.closePlayerSelectionDialog();
+ // Update all members in the list to reflect the current state
+ this.playerSelectionDialog.members.forEach(m => {
+ m.isReady = playersReady.includes(m.id);
+ m.isPlanned = playersPlanned.includes(m.id);
+ m.hasPlayed = playersPlayed.includes(m.id);
+ });
+
+ if (closeDialog) {
+ await this.showInfo('Erfolg', 'Spielerauswahl gespeichert', '', 'success');
+ this.closePlayerSelectionDialog();
+ }
} catch (error) {
console.error('Error saving player selection:', error);
await this.showInfo('Fehler', 'Fehler beim Speichern der Spielerauswahl', '', 'error');
}
},
+ // Gallery Methods
+ async openGalleryDialog() {
+ if (!this.playerSelectionDialog.match) {
+ await this.showInfo('Hinweis', 'Bitte wählen Sie zuerst ein Spiel aus', '', 'info');
+ return;
+ }
+ this.showGalleryDialog = true;
+ await this.loadGalleryMembers();
+ },
+
+ async loadGalleryMembers() {
+ if (!this.currentClub || this.galleryLoading) {
+ return;
+ }
+ this.galleryLoading = true;
+ this.galleryError = '';
+ try {
+ const response = await apiClient.get(`/clubmembers/gallery/${this.currentClub}?format=json&size=${this.gallerySize}`);
+ this.galleryMembers = response.data.members || [];
+ } catch (error) {
+ console.error('Fehler beim Laden der Galerie:', error);
+ this.galleryError = error?.response?.data?.error || 'Galerie konnte nicht geladen werden.';
+ this.galleryMembers = [];
+ } finally {
+ this.galleryLoading = false;
+ }
+ },
+
+ closeGalleryDialog() {
+ this.showGalleryDialog = false;
+ this.galleryMembers = [];
+ },
+
+ getMemberImageUrl(memberId) {
+ const token = this.$store.getters.token;
+ const backendBaseUrl = import.meta.env.VITE_BACKEND || 'http://localhost:3005';
+ // Get primary image (latest will be handled by backend)
+ return `${backendBaseUrl}/api/clubmembers/image/${this.currentClub}/${memberId}?authcode=${token}`;
+ },
+
+ isMemberReady(memberId) {
+ const match = this.playerSelectionDialog.match;
+ if (!match || !match.playersReady) {
+ return false;
+ }
+ return match.playersReady.includes(memberId);
+ },
+
+ async toggleMemberReady(member) {
+ console.log('[toggleMemberReady] Called with member:', member);
+ const match = this.playerSelectionDialog.match;
+ if (!match) {
+ console.error('[toggleMemberReady] No match selected');
+ return;
+ }
+ console.log('[toggleMemberReady] Match:', match.id);
+
+ // Find member in playerSelectionDialog.members
+ let memberInList = this.playerSelectionDialog.members.find(m => m.id === member.memberId);
+ if (!memberInList) {
+ // Member not in list yet, add it
+ try {
+ const response = await apiClient.get(`/clubmembers/get/${this.currentClub}/true`);
+ const allMembers = response.data;
+ const foundMember = allMembers.find(m => m.id === member.memberId);
+ if (foundMember) {
+ // Check if member is already marked as ready
+ const isCurrentlyReady = match.playersReady?.includes(member.memberId) || false;
+ memberInList = {
+ ...foundMember,
+ isReady: isCurrentlyReady,
+ isPlanned: match.playersPlanned?.includes(member.memberId) || false,
+ hasPlayed: match.playersPlayed?.includes(member.memberId) || false
+ };
+ this.playerSelectionDialog.members.push(memberInList);
+ } else {
+ console.error('Member not found:', member.memberId);
+ return;
+ }
+ } catch (error) {
+ console.error('Error loading member:', error);
+ return;
+ }
+ }
+
+ // Toggle ready status
+ const wasReady = memberInList.isReady;
+ memberInList.isReady = !wasReady;
+
+ // Update match.playersReady immediately
+ if (!match.playersReady) {
+ match.playersReady = [];
+ }
+ const index = match.playersReady.indexOf(member.memberId);
+ if (wasReady && index > -1) {
+ match.playersReady.splice(index, 1);
+ } else if (!wasReady && index === -1) {
+ match.playersReady.push(member.memberId);
+ }
+
+ // Auto-save (don't close dialog if it's open)
+ console.log('[toggleMemberReady] Calling savePlayerSelection');
+ await this.savePlayerSelection(false);
+ console.log('[toggleMemberReady] savePlayerSelection completed');
+ },
+
...mapActions(['openDialog']),
openImportModal() {
this.showImportModal = true;
@@ -1263,6 +1435,99 @@ li {
font-weight: 500;
}
+/* Gallery Styles */
+.gallery-dialog-content {
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ min-height: 60vh;
+ max-height: 70vh;
+ overflow: auto;
+}
+
+.gallery-controls {
+ width: 100%;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border-color, #ddd);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ background: #f9f9f9;
+ flex-shrink: 0;
+}
+
+.gallery-controls label {
+ font-weight: 500;
+ color: var(--text-color, #333);
+}
+
+.gallery-controls select {
+ padding: 6px 12px;
+ border: 1px solid var(--border-color, #ddd);
+ border-radius: 4px;
+ font-size: 14px;
+ background: white;
+ cursor: pointer;
+}
+
+.gallery-controls select:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.gallery-members-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ gap: 16px;
+ padding: 16px;
+}
+
+.gallery-member-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: pointer;
+ padding: 8px;
+ border: 2px solid transparent;
+ border-radius: 8px;
+ transition: all 0.2s ease;
+}
+
+.gallery-member-item:hover {
+ background-color: #f0f0f0;
+ border-color: #007bff;
+}
+
+.gallery-member-item.is-ready {
+ border-color: #28a745;
+ background-color: #d4edda;
+}
+
+.gallery-member-image {
+ width: 100%;
+ height: auto;
+ aspect-ratio: 1;
+ object-fit: cover;
+ border-radius: 4px;
+ margin-bottom: 8px;
+ pointer-events: none; /* Prevent image from blocking clicks on parent */
+}
+
+.gallery-member-name {
+ font-size: 12px;
+ text-align: center;
+ color: var(--text-color, #333);
+ font-weight: 500;
+}
+
+.gallery-loading,
+.gallery-error {
+ padding: 20px;
+ text-align: center;
+ font-size: 1rem;
+ color: var(--text-color, #333);
+}
+
.checkbox-cell {
text-align: center;
width: 100px;