Fügt die Funktion zum Löschen von vordefinierten Aktivitätsbildern hinzu. Implementiert die Logik in der Datei predefinedActivityImageController.js und aktualisiert die Routen in predefinedActivityRoutes.js. Ergänzt die Benutzeroberfläche in PredefinedActivities.vue um die Möglichkeit, hochgeladene Bilder anzuzeigen und zu löschen.

This commit is contained in:
Torsten Schulz (local)
2025-09-15 23:46:59 +02:00
parent 296939d1a0
commit 92ce64b807
3 changed files with 161 additions and 18 deletions

View File

@@ -50,4 +50,43 @@ export const uploadPredefinedActivityImage = async (req, res) => {
}
};
export const deletePredefinedActivityImage = async (req, res) => {
try {
const { id, imageId } = req.params; // predefinedActivityId, imageId
const { authcode: userToken } = req.headers;
await checkAccess(userToken);
const activity = await PredefinedActivity.findByPk(id);
if (!activity) {
return res.status(404).json({ error: 'Predefined activity not found' });
}
const image = await PredefinedActivityImage.findOne({
where: { id: imageId, predefinedActivityId: id }
});
if (!image) {
return res.status(404).json({ error: 'Image not found' });
}
// Datei vom Dateisystem löschen
if (fs.existsSync(image.imagePath)) {
fs.unlinkSync(image.imagePath);
}
// Datensatz aus der Datenbank löschen
await image.destroy();
// Falls das gelöschte Bild der aktuelle imageLink war, diesen zurücksetzen
if (activity.imageLink === `/api/predefined-activities/${id}/image/${imageId}`) {
activity.imageLink = null;
await activity.save();
}
res.status(200).json({ message: 'Image deleted successfully' });
} catch (error) {
console.error('[deletePredefinedActivityImage] - Error:', error);
res.status(500).json({ error: 'Failed to delete image' });
}
};

View File

@@ -10,7 +10,7 @@ import {
} from '../controllers/predefinedActivityController.js';
import multer from 'multer';
import { authenticate } from '../middleware/authMiddleware.js';
import { uploadPredefinedActivityImage } from '../controllers/predefinedActivityImageController.js';
import { uploadPredefinedActivityImage, deletePredefinedActivityImage } from '../controllers/predefinedActivityImageController.js';
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
import path from 'path';
import fs from 'fs';
@@ -23,6 +23,7 @@ router.get('/', authenticate, getAllPredefinedActivities);
router.get('/:id', authenticate, getPredefinedActivityById);
router.put('/:id', authenticate, updatePredefinedActivity);
router.post('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage);
router.delete('/:id/image/:imageId', authenticate, deletePredefinedActivityImage);
router.get('/search/query', authenticate, searchPredefinedActivities);
router.post('/merge', authenticate, mergePredefinedActivities);
router.post('/deduplicate', authenticate, deduplicatePredefinedActivities);

View File

@@ -54,27 +54,42 @@
<label>Beschreibung
<textarea v-model="editModel.description" rows="4" />
</label>
<label>Bild-Link (optional)
<input type="text" v-model="editModel.imageLink" placeholder="/api/predefined-activities/:id/image/:imageId oder extern" />
</label>
<div class="image-section">
<h4>Bild hinzufügen</h4>
<p class="image-help">Du kannst entweder einen Link zu einem Bild eingeben oder ein Bild hochladen:</p>
<label>Bild-Link (optional)
<input type="text" v-model="editModel.imageLink" placeholder="z.B. https://example.com/bild.jpg oder /api/predefined-activities/:id/image/:imageId" />
</label>
<div class="upload-section">
<label>Oder Bild hochladen:
<input type="file" accept="image/*" @change="onFileChange" />
</label>
<button class="btn-secondary" :disabled="!selectedFile" @click="uploadImage">
{{ editModel.id ? 'Hochladen' : 'Nach Speichern hochladen' }}
</button>
<p v-if="!editModel.id" class="upload-note">
Hinweis: Das Bild wird erst nach dem Speichern der Aktivität hochgeladen.
</p>
</div>
<div class="image-list" v-if="images && images.length">
<h5>Hochgeladene Bilder:</h5>
<div class="image-grid">
<div v-for="img in images" :key="img.id" class="image-item">
<img :src="imageUrl(img)" alt="Activity Image" />
<button class="btn-small btn-danger" @click="deleteImage(img.id)">Löschen</button>
</div>
</div>
</div>
</div>
<div class="actions">
<button type="submit" class="btn-primary">Speichern</button>
<button type="button" class="btn-secondary" @click="cancel">Abbrechen</button>
</div>
</form>
<div v-if="editModel.id" class="images">
<h4>Bild hochladen</h4>
<input type="file" accept="image/*" @change="onFileChange" />
<button class="btn-secondary" :disabled="!selectedFile" @click="uploadImage">Hochladen</button>
<div class="image-list" v-if="images && images.length">
<div v-for="img in images" :key="img.id" class="image-item">
<img :src="imageUrl(img)" alt="Activity Image" />
</div>
</div>
</div>
</div>
</div>
</div>
@@ -165,6 +180,10 @@ export default {
} else {
const r = await apiClient.post('/predefined-activities', this.editModel);
this.editModel = r.data;
// Nach dem Erstellen einer neuen Aktivität, falls ein Bild ausgewählt wurde, hochladen
if (this.selectedFile) {
await this.uploadImage();
}
}
await this.reload();
},
@@ -185,6 +204,18 @@ export default {
await this.select(this.editModel);
this.selectedFile = null;
},
async deleteImage(imageId) {
if (!this.editModel || !this.editModel.id) return;
if (!confirm('Bild wirklich löschen?')) return;
try {
await apiClient.delete(`/predefined-activities/${this.editModel.id}/image/${imageId}`);
// Nach Löschen Details neu laden
await this.select(this.editModel);
} catch (error) {
console.error('Fehler beim Löschen des Bildes:', error);
alert('Fehler beim Löschen des Bildes');
}
},
async deduplicate() {
if (!confirm('Alle Aktivitäten mit identischem Namen werden zusammengeführt. Fortfahren?')) return;
await apiClient.post('/predefined-activities/deduplicate', {});
@@ -249,7 +280,79 @@ select { max-width: 220px; }
label { display: block; margin-bottom: 0.5rem; }
input[type="text"], input[type="number"], textarea { width: 100%; }
.actions { margin-top: 0.75rem; display: flex; gap: 0.5rem; }
.image-list { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem; }
.image-item img { max-height: 120px; border: 1px solid var(--border-color); border-radius: var(--border-radius-small); }
.image-section {
margin: 1rem 0;
padding: 1rem;
background: #f8f9fa;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
}
.image-help {
margin: 0 0 1rem 0;
color: #666;
font-size: 0.9rem;
}
.upload-section {
margin: 1rem 0;
padding: 1rem;
background: white;
border-radius: var(--border-radius-small);
border: 1px solid #ddd;
}
.upload-note {
margin: 0.5rem 0 0 0;
color: #666;
font-size: 0.85rem;
font-style: italic;
}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
margin-top: 0.5rem;
}
.image-item {
position: relative;
border: 1px solid #ddd;
border-radius: var(--border-radius-small);
overflow: hidden;
background: white;
}
.image-item img {
width: 100%;
height: 120px;
object-fit: cover;
display: block;
}
.image-item button {
position: absolute;
top: 0.25rem;
right: 0.25rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.btn-small {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.btn-danger {
background: #dc3545;
color: white;
border: none;
border-radius: var(--border-radius-small);
}
.btn-danger:hover {
background: #c82333;
}
</style>