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:
@@ -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 };
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
Reference in New Issue
Block a user