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:
@@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user