Fügt die Funktion zum Drehen von Mitgliedsbildern hinzu. Implementiert die Logik zur Bildrotation in MemberService und aktualisiert die entsprechenden Routen und Frontend-Komponenten, um die Benutzeroberfläche für die Bildbearbeitung zu verbessern. Ermöglicht das Drehen von Bildern über die Mitgliederansicht und aktualisiert die Anzeige nach der Bearbeitung.

This commit is contained in:
Torsten Schulz (local)
2025-10-04 01:59:21 +02:00
parent 0cf2351c79
commit 2b1365339e
5 changed files with 244 additions and 8 deletions

View File

@@ -80,4 +80,25 @@ const updateRatingsFromMyTischtennis = async (req, res) => {
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis };
const rotateMemberImage = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { direction } = req.body;
const { authcode: userToken } = req.headers;
if (!direction || !['left', 'right'].includes(direction)) {
return res.status(400).json({
success: false,
error: 'Ungültige Drehrichtung. Verwenden Sie "left" oder "right".'
});
}
const result = await MemberService.rotateMemberImage(userToken, clubId, memberId, direction);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[rotateMemberImage] - Error:', error);
res.status(500).json({ success: false, error: 'Failed to rotate image' });
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis, rotateMemberImage };

View File

@@ -1,4 +1,4 @@
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis } from '../controllers/memberController.js';
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis, rotateMemberImage } from '../controllers/memberController.js';
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import multer from 'multer';
@@ -14,5 +14,6 @@ router.get('/get/:id/:showAll', authenticate, getClubMembers);
router.post('/set/:id', authenticate, setClubMembers);
router.get('/notapproved/:id', authenticate, getWaitingApprovals);
router.post('/update-ratings/:id', authenticate, updateRatingsFromMyTischtennis);
router.post('/rotate-image/:clubId/:memberId', authenticate, rotateMemberImage);
export default router;

View File

@@ -320,6 +320,56 @@ class MemberService {
};
}
}
async rotateMemberImage(userToken, clubId, memberId, direction) {
try {
await checkAccess(userToken, clubId);
const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
if (!member) {
return { status: 404, response: { success: false, error: 'Member not found in this club' } };
}
const imagePath = path.join('images', 'members', `${memberId}.jpg`);
if (!fs.existsSync(imagePath)) {
return { status: 404, response: { success: false, error: 'Image not found' } };
}
// Read the image
const imageBuffer = await fs.promises.readFile(imagePath);
// Calculate rotation angle (-90 for left, +90 for right)
const rotationAngle = direction === 'left' ? -90 : 90;
// Rotate the image
const rotatedBuffer = await sharp(imageBuffer)
.rotate(rotationAngle)
.jpeg({ quality: 80 })
.toBuffer();
// Save the rotated image
await fs.promises.writeFile(imagePath, rotatedBuffer);
return {
status: 200,
response: {
success: true,
message: `Bild um ${rotationAngle}° gedreht`,
direction: direction,
rotation: rotationAngle
}
};
} catch (error) {
console.error('[rotateMemberImage] - Error:', error);
return {
status: 500,
response: {
success: false,
error: 'Fehler beim Drehen des Bildes: ' + error.message
}
};
}
}
}
export default new MemberService();

View File

@@ -571,12 +571,61 @@ export default {
if (this.isAuthenticated && this.currentClub) {
const response = await apiClient.get(`/diary/${this.currentClub}`);
this.dates = response.data.map(entry => ({ id: entry.id, date: entry.date }));
// Automatisch das Datum mit den meisten Einträgen auswählen
await this.autoSelectDateWithEntries();
await this.loadTags();
await this.loadPredefinedActivities();
}
},
async autoSelectDateWithEntries() {
if (this.dates.length === 0) {
this.date = 'new';
return;
}
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD Format
// 1. Zuerst prüfe das heutige Datum
const todayEntry = this.dates.find(date => date.date === today);
if (todayEntry) {
const todayEntries = await this.countEntriesForDate(todayEntry.id);
if (todayEntries > 0) {
this.date = todayEntry;
await this.handleDateChange();
return;
}
}
// 2. Falls heutiges Datum nicht gefunden oder keine Einträge vorhanden
// → Bleibe bei "Neu anlegen"
this.date = 'new';
this.showForm = true;
},
async countEntriesForDate(dateId) {
try {
// Lade Teilnehmer
const participantsResponse = await apiClient.get(`/participants/${dateId}`);
const participantCount = participantsResponse.data.length;
// Lade Aktivitäten
const activitiesResponse = await apiClient.get(`/activities/${dateId}`);
const activityCount = activitiesResponse.data.length;
// Lade Trainingplan
const trainingPlanResponse = await apiClient.get(`/diary-date-activities/${this.currentClub}/${dateId}`);
const trainingPlanCount = trainingPlanResponse.data.length;
// Gesamtanzahl der Einträge
return participantCount + activityCount + trainingPlanCount;
} catch (error) {
console.warn(`Fehler beim Laden der Einträge für Datum ${dateId}:`, error);
return 0;
}
},
async refreshDates(selectId) {
const response = await apiClient.get(`/diary/${this.currentClub}`);

View File

@@ -74,7 +74,7 @@
<td>
<img v-if="member.imageUrl" :src="member.imageUrl" alt="Mitgliedsbild"
style="max-width: 50px; max-height: 50px;"
@click.stop="openImageModal(member.imageUrl)">
@click.stop="openImageModal(member.imageUrl, member.id)">
<span>{{ member.picsInInternetAllowed ? '✓' : '' }}</span>
</td>
<td>{{ member.testMembership ? '*' : '' }}</td>
@@ -107,9 +107,20 @@
</div>
<div v-if="showImageModal" class="modal">
<div class="modal-content">
<span class="close" @click="closeImageModal">&times;</span>
<img :src="selectedImageUrl" alt="Großes Mitgliedsbild" style="max-width: 100%; max-height: 100%;">
<div class="modal-content image-modal-content">
<span class="close" @click="closeImageModal">&times;</span>
<div class="image-container">
<img :src="selectedImageUrl" alt="Großes Mitgliedsbild"
class="modal-image">
<div class="image-actions">
<button @click="rotateImage('left')" class="rotate-btn" title="90° links drehen">
Links
</button>
<button @click="rotateImage('right')" class="rotate-btn" title="90° rechts drehen">
Rechts
</button>
</div>
</div>
</div>
</div>
@@ -163,6 +174,7 @@ export default {
showNotesModal: false,
showImageModal: false,
selectedImageUrl: null,
selectedMemberId: null,
testMembership: false,
showInactiveMembers: false,
newPicsInInternetAllowed: false,
@@ -342,13 +354,37 @@ export default {
closeNotesModal() {
this.showNotesModal = false;
},
openImageModal(imageUrl) {
openImageModal(imageUrl, memberId) {
this.selectedImageUrl = imageUrl;
this.selectedMemberId = memberId;
this.showImageModal = true;
},
closeImageModal() {
this.showImageModal = false;
this.selectedImageUrl = null;
this.selectedMemberId = null;
},
async rotateImage(direction) {
if (!this.selectedMemberId) return;
try {
const response = await apiClient.post(`/clubmembers/rotate-image/${this.currentClub}/${this.selectedMemberId}`, {
direction: direction
});
if (response.data.success) {
// Reload the member's image to show the rotated version
const member = this.members.find(m => m.id === this.selectedMemberId);
if (member) {
await this.reloadMemberImage(member);
}
} else {
alert('Fehler beim Drehen des Bildes: ' + response.data.error);
}
} catch (error) {
console.error('Fehler beim Drehen des Bildes:', error);
alert('Fehler beim Drehen des Bildes: ' + (error.response?.data?.error || error.message));
}
},
async loadMemberImage(member) {
try {
@@ -362,6 +398,32 @@ export default {
member.imageUrl = null;
}
},
async reloadMemberImage(member) {
try {
// Revoke old blob URL to free memory
if (member.imageUrl) {
URL.revokeObjectURL(member.imageUrl);
}
const response = await apiClient.get(`/clubmembers/image/${this.currentClub}/${member.id}`, {
responseType: 'blob',
});
// Create new blob URL with timestamp for cache busting
const imageUrl = URL.createObjectURL(response.data);
member.imageUrl = imageUrl;
// Also update selectedImageUrl if this is the currently viewed member
if (member.id === this.selectedMemberId) {
this.selectedImageUrl = imageUrl;
}
} catch (error) {
console.error('Fehler beim Neuladen des Bildes:', error);
member.imageUrl = null;
}
},
async createPhoneList() {
const activeMembers = this.members.filter(member => member.active);
const pdfGenerator = new PDFGenerator();
@@ -579,4 +641,57 @@ table td {
cursor: not-allowed;
opacity: 0.6;
}
/* Image Modal Styles */
.image-modal-content {
display: flex;
flex-direction: column;
padding: 0;
overflow: hidden;
}
.image-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
padding: 1rem;
}
.modal-image {
max-width: 400px;
max-height: 400px;
object-fit: contain;
flex-shrink: 0;
transition: transform 0.3s ease;
}
.image-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.rotate-btn {
background-color: #007bff;
color: white;
border: none;
padding: 0.75rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.9em;
font-weight: 500;
transition: all 0.2s ease;
min-width: 120px;
}
.rotate-btn:hover {
background-color: #0056b3;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
}
.rotate-btn:active {
transform: translateY(0);
}
</style>