Bilder hinzugefügt

This commit is contained in:
Torsten Schulz
2024-09-11 17:57:56 +02:00
parent 543a2f9259
commit 3b1021f7c8
7 changed files with 250 additions and 56 deletions

View File

@@ -4,7 +4,6 @@ const getClubMembers = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { id: clubId } = req.params;
console.log('[getClubMembers]', userToken, clubId);
res.status(200).json(await MemberService.getClubMembers(userToken, clubId));
} catch(error) {
console.log('[getClubMembers] - Error: ', error);
@@ -37,4 +36,34 @@ const setClubMembers = async (req, res) => {
res.status(addResult.status || 500).json(addResult.response);
}
export { getClubMembers, getWaitingApprovals, setClubMembers };
const uploadMemberImage = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.uploadMemberImage(userToken, clubId, memberId, req.file.buffer);
res.status(result.status).json(result.message ? { message: result.message } : { error: result.error });
} catch (error) {
console.error('[uploadMemberImage] - Error:', error);
res.status(500).json({ error: 'Failed to upload image' });
}
};
const getMemberImage = async (req, res) => {
console.log('[getMemberImage]');
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
console.log('-------------------->', clubId, memberId, userToken);
const result = await MemberService.getMemberImage(userToken, clubId, memberId);
if (result.status === 200) {
res.sendFile(result.imagePath);
} else {
res.status(result.status).json({ error: result.error });
}
} catch (error) {
console.error('[getMemberImage] - Error:', error);
res.status(500).json({ error: 'Failed to retrieve image' });
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage };

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

View File

@@ -1,9 +1,15 @@
import { getClubMembers, getWaitingApprovals, setClubMembers } from '../controllers/memberController.js';
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage } from '../controllers/memberController.js';
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import multer from 'multer';
const router = express.Router();
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
router.post('/:clubId/image/:memberId', authenticate, upload.single('image'), uploadMemberImage);
router.get('/:clubId/image/:memberId', authenticate, getMemberImage);
router.get('/:id', authenticate, getClubMembers);
router.post('/:id', authenticate, setClubMembers);
router.get('/notapproved/:id', authenticate, getWaitingApprovals);

View File

@@ -1,9 +1,10 @@
import UserClub from "../models/UserClub.js";
import { checkAccess } from "../utils/userUtils.js";
import { getUserByToken } from "../utils/userUtils.js";
import HttpError from "../exceptions/HttpError.js";
import Member from "../models/Member.js";
import { response } from "express";
import path from 'path';
import fs from 'fs';
import sharp from 'sharp';
class MemberService {
async getApprovalRequests(userToken, clubId) {
@@ -79,6 +80,45 @@ class MemberService {
}
}
}
async uploadMemberImage(userToken, clubId, memberId, imageBuffer) {
try {
console.log('------>', userToken, clubId, memberId, imageBuffer);
await checkAccess(userToken, clubId);
const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
if (!member) {
return { status: 404, error: 'Member not found in this club' };
}
const imagePath = path.join('images', 'members', `${memberId}.jpg`);
await sharp(imageBuffer)
.resize(600, 600)
.jpeg({ quality: 80 })
.toFile(imagePath);
return { status: 200, message: 'Image uploaded successfully' };
} catch (error) {
console.error('[uploadMemberImage] - Error:', error);
return { status: 500, error: 'Failed to upload image' };
}
}
async getMemberImage(userToken, clubId, memberId) {
try {
await checkAccess(userToken, clubId);
const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
if (!member) {
return { status: 404, error: 'Member not found in this club' };
}
const imagePath = path.join('images', 'members', `${memberId}.jpg`);
if (!fs.existsSync(imagePath)) {
return { status: 404, error: 'Image not found' };
}
return { status: 200, imagePath: path.resolve(imagePath) };
} catch (error) {
console.error('[getMemberImage] - Error:', error);
return { status: 500, error: 'Failed to retrieve image' };
}
}
}
export default new MemberService();

View File

@@ -123,20 +123,28 @@
<div class="modal-content">
<span class="close" @click="closeNotesModal">&times;</span>
<h3>Notizen für {{ selectedMember.firstName }} {{ selectedMember.lastName }}</h3>
<multiselect v-model="selectedMemberTags" :options="availableTags" placeholder="Tags auswählen"
label="name" track-by="id" multiple :close-on-select="true" @tag="addNewTagForMember"
@remove="removeMemberTag" @input="updateMemberTags" :allow-empty="false"
@keydown.enter.prevent="addNewTagForMemberFromInput" />
<div>
<textarea v-model="newNoteContent" placeholder="Neue Notiz" rows="4" cols="30"></textarea>
<button @click="addMemberNote">Hinzufügen</button>
<div class="modal-body">
<div class="modal-left">
<img v-if="selectedMember.imageUrl" :src="selectedMember.imageUrl" alt="Mitgliedsbild"
style="width: 250px; height: 250px; object-fit: cover;" />
</div>
<div class="modal-right">
<multiselect v-model="selectedMemberTags" :options="availableTags" placeholder="Tags auswählen"
label="name" track-by="id" multiple :close-on-select="true" @tag="addNewTagForMember"
@remove="removeMemberTag" @input="updateMemberTags" :allow-empty="false"
@keydown.enter.prevent="addNewTagForMemberFromInput" />
<div>
<textarea v-model="newNoteContent" placeholder="Neue Notiz" rows="4" cols="30"></textarea>
<button @click="addMemberNote">Hinzufügen</button>
</div>
<ul>
<li v-for="note in notes" :key="note.id">
<button @click="deleteNote(note.id)">Löschen</button>
{{ note.content }}
</li>
</ul>
</div>
</div>
<ul>
<li v-for="note in notes" :key="note.id">
<button @click="deleteNote(note.id)">Löschen</button>
{{ note.content }}
</li>
</ul>
</div>
</div>
</div>
@@ -339,8 +347,9 @@ export default {
this.selectedActivityTags = [];
}
},
openNotesModal(member) {
async openNotesModal(member) {
this.selectedMember = member;
await this.loadMemberImage(member);
this.loadMemberNotesAndTags(this.date.id, member.id);
this.showNotesModal = true;
},
@@ -608,6 +617,18 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async loadMemberImage(member) {
try {
const response = await apiClient.get(`/clubmembers/${this.currentClub}/image/${member.id}`, {
responseType: 'blob',
});
const imageUrl = URL.createObjectURL(response.data);
member.imageUrl = imageUrl;
} catch (error) {
console.error("Failed to load member image:", error);
member.imageUrl = null;
}
}
},
async mounted() {
@@ -789,4 +810,21 @@ input[type="number"] {
.drag-handle {
cursor: pointer;
}
.modal-body {
display: flex;
justify-content: space-between;
}
.modal-left {
flex: 0 0 250px;
display: flex;
justify-content: center;
align-items: center;
padding-right: 20px;
}
.modal-right {
flex: 1;
}
</style>

View File

@@ -2,8 +2,11 @@
<div>
<h2>Mitglieder</h2>
<div class="newmember">
<div class="toggle-new-member"><span @click="toggleNewMember"><span class="add">{{ memberFormIsOpen ? '-' :
'+' }}</span>{{ memberToEdit === null ? "Neues Mitglied" : "Mitglied bearbeiten" }}</span>
<div class="toggle-new-member">
<span @click="toggleNewMember">
<span class="add">{{ memberFormIsOpen ? '-' : '+' }}</span>
{{ memberToEdit === null ? "Neues Mitglied" : "Mitglied bearbeiten" }}
</span>
<button v-if="memberToEdit !== null" @click="resetToNewMember">Neues Mitglied anlegen</button>
</div>
<div v-if="memberFormIsOpen" class="new-member-form">
@@ -15,8 +18,13 @@
<label><span>Telefon-Nr.:</span> <input type="text" v-model="newPhone"></label>
<label><span>Email-Adresse:</span> <input type="email" v-model="newEmail"></label>
<label><span>Aktiv:</span> <input type="checkbox" v-model="newActive"></label>
<label><span>Bild:</span> <input type="file" @change="onFileSelected"></label>
<div v-if="memberImagePreview">
<img :src="memberImagePreview" alt="Vorschau des Mitgliedsbildes"
style="max-width: 200px; max-height: 200px;">
</div>
<div>
<button @click="addNewMember">Anlegen</button>
<button @click="addNewMember">{{ memberToEdit ? 'Ändern' : 'Anlegen' }}</button>
<button @click="resetNewMember" v-if="memberToEdit === null">Felder leeren</button>
</div>
</div>
@@ -25,6 +33,7 @@
<table>
<thead>
<tr>
<th>Bild</th>
<th>Name, Vorname</th>
<th>Adresse</th>
<th>Geburtsdatum</th>
@@ -34,17 +43,31 @@
</thead>
<tbody>
<tr v-for="member in members" :key="member.id" @click="editMember(member)">
<td>
<img v-if="member.imageUrl" :src="member.imageUrl" alt="Mitgliedsbild"
style="max-width: 50px; max-height: 50px;"
@click.stop="openImageModal(member.imageUrl)">
</td>
<td>{{ member.lastName }}, {{ member.firstName }}</td>
<td>{{ member.street }}, {{ member.city }}</td>
<td>{{ member.birthDate }}</td>
<td>{{ member.phone }}</td>
<td>{{ member.email }}</td>
<td><button @click.stop="openNotesModal(member)">Notizen</button></td>
<td>
<button @click.stop="openNotesModal(member)">Notizen</button>
</td>
</tr>
</tbody>
</table>
</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>
</div>
<div v-if="showNotesModal" class="modal">
<div class="modal-content">
<span class="close" @click="closeNotesModal">&times;</span>
@@ -65,7 +88,7 @@
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { mapGetters } from 'vuex';
import apiClient from '../apiClient.js';
export default {
@@ -86,9 +109,13 @@ export default {
newEmail: '',
newActive: true,
memberToEdit: null,
memberImage: null,
memberImagePreview: null,
notes: [],
newNoteContent: '',
showNotesModal: false,
showImageModal: false,
selectedImageUrl: null,
}
},
async mounted() {
@@ -98,6 +125,9 @@ export default {
async init() {
const response = await apiClient.get(`/clubmembers/${this.currentClub}`);
this.members = response.data;
this.members.forEach(member => {
this.loadMemberImage(member);
});
},
toggleNewMember() {
this.memberFormIsOpen = !this.memberFormIsOpen;
@@ -111,9 +141,24 @@ export default {
this.newPhone = '';
this.newEmail = '';
this.newActive = true;
this.memberImage = null;
this.memberImagePreview = null;
},
onFileSelected(event) {
const file = event.target.files[0];
this.memberImage = file;
// Bildvorschau erstellen
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
this.memberImagePreview = e.target.result;
};
reader.readAsDataURL(file);
}
},
async addNewMember() {
const response = await apiClient.post(`/clubmembers/${this.currentClub}`, {
const memberData = {
firstname: this.newFirstname,
lastname: this.newLastname,
street: this.newStreet,
@@ -123,8 +168,32 @@ export default {
email: this.newEmail,
active: this.newActive,
id: this.memberToEdit ? this.memberToEdit.id : null,
});
this.members = response.data;
};
let response;
try {
response = await apiClient.post(`/clubmembers/${this.currentClub}`, memberData);
this.members = response.data;
} catch (error) {
console.error("Fehler beim Speichern des Mitglieds:", error);
return;
}
if (this.memberImage) {
const formData = new FormData();
formData.append('image', this.memberImage);
formData.append('clubId', this.currentClub);
try {
await apiClient.post(`/clubmembers/${this.currentClub}/image/${this.memberToEdit.id}`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
} catch (error) {
console.error("Fehler beim Hochladen des Bildes:", error);
}
}
this.resetNewMember();
this.memberFormIsOpen = false;
},
@@ -138,19 +207,21 @@ export default {
this.newPhone = member.phone;
this.newEmail = member.email;
this.newActive = member.active;
this.loadNotes(member);
try {
const response = await apiClient.get(`/clubmembers/${member.id}/image`, {
params: { clubId: this.currentClub },
responseType: 'blob'
});
this.memberImagePreview = URL.createObjectURL(response.data);
} catch (error) {
console.error("Fehler beim Laden des Bildes:", error);
this.memberImagePreview = null;
}
},
resetToNewMember() {
this.memberToEdit = null;
this.newFirstname = '';
this.newLastname = '';
this.newStreet = '';
this.newCity = '';
this.newBirthdate = '01.01.2010';
this.newPhone = '';
this.newEmail = '';
this.newActive = true;
this.memberNotes = [];
this.resetNewMember();
},
async loadNotes(member) {
this.selectedMember = member;
@@ -176,12 +247,32 @@ export default {
this.notes = response.data;
},
openNotesModal(member) {
this.editMember(member);
this.memberToEdit = member;
this.showNotesModal = true;
},
closeNotesModal() {
this.showNotesModal = false;
}
},
openImageModal(imageUrl) {
this.selectedImageUrl = imageUrl;
this.showImageModal = true;
},
closeImageModal() {
this.showImageModal = false;
this.selectedImageUrl = null;
},
async loadMemberImage(member) {
try {
const response = await apiClient.get(`/clubmembers/${this.currentClub}/image/${member.id}`, {
responseType: 'blob',
});
const imageUrl = URL.createObjectURL(response.data);
member.imageUrl = imageUrl;
} catch (error) {
console.error("Failed to load member image:", error);
member.imageUrl = null; // Fallback, falls das Bild nicht geladen werden kann
}
},
}
}
</script>
@@ -247,17 +338,18 @@ table td {
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(200, 200, 200, 0.5);
background-color: rgba(0, 0, 0, 0.8);
}
.modal-content {
background-color: #fefefe;
padding: 20px;
border: 1px solid #555;
width: 50%;
max-width: 500px;
position: relative;
border: 1px solid #888;
width: 80%;
max-width: 800px;
max-height: 80%;
box-shadow: 4px 3px 2px #999;
position: relative;
}
.close {
@@ -271,19 +363,8 @@ table td {
.close:hover,
.close:focus {
color: black;
color: #000;
text-decoration: none;
cursor: pointer;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 0;
/* Kein Padding für Listenelemente */
margin-bottom: 5px;
}
</style>