diff --git a/backend/migrations/core/062_add_original_filename.js b/backend/migrations/core/062_add_original_filename.js new file mode 100644 index 00000000..2025a19a --- /dev/null +++ b/backend/migrations/core/062_add_original_filename.js @@ -0,0 +1,23 @@ +/** + * Migration 062: Add original_filename to photos table + * - photos.original_filename: preserves the original filename from upload + * This enables Lightroom integration by exporting filtered filenames + */ + +const { addColumnIfNotExists } = require('../helpers'); + +exports.up = async function(knex) { + console.log('Running migration: 062_add_original_filename'); + + // photos.original_filename (nullable - original filename before renaming) + await addColumnIfNotExists(knex, 'photos', 'original_filename', (table) => { + table.string('original_filename', 512); + }); + + console.log('Migration 062_add_original_filename completed'); +}; + +exports.down = async function(knex) { + console.log('Rollback: 062_add_original_filename'); + // Keep column (safe rollback not removing data). Intentionally no-op. +}; diff --git a/backend/src/routes/adminPhotoExport.js b/backend/src/routes/adminPhotoExport.js index cd723961..0390b5ef 100644 --- a/backend/src/routes/adminPhotoExport.js +++ b/backend/src/routes/adminPhotoExport.js @@ -71,12 +71,12 @@ router.get('/:eventId/filtered', adminAuth, requirePermission('photos.view'), [ // Build filtered query const filterBuilder = new PhotoFilterBuilder( db('photos') - .leftJoin('categories', 'photos.category_id', 'categories.id') + .leftJoin('photo_categories', 'photos.category_id', 'photo_categories.id') .select( 'photos.id', 'photos.filename', 'photos.original_filename', - 'photos.file_path', + 'photos.path', 'photos.average_rating', 'photos.feedback_count', 'photos.like_count', @@ -84,8 +84,8 @@ router.get('/:eventId/filtered', adminAuth, requirePermission('photos.view'), [ 'photos.comment_count', 'photos.width', 'photos.height', - 'photos.created_at', - 'categories.name as category_name' + 'photos.uploaded_at', + 'photo_categories.name as category_name' ), eventId ); diff --git a/backend/src/routes/adminPhotos.js b/backend/src/routes/adminPhotos.js index ff65a6fc..b04046a4 100644 --- a/backend/src/routes/adminPhotos.js +++ b/backend/src/routes/adminPhotos.js @@ -257,6 +257,7 @@ router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), u const photoData = { event_id: parseInt(eventId), filename: newFilename, + original_filename: file.originalname, // Preserve original filename for Lightroom export path: relativePath, thumbnail_path: null, // Will generate after successful commit type: photoType, diff --git a/backend/src/services/photoExportService.js b/backend/src/services/photoExportService.js index c662346d..0f48d245 100644 --- a/backend/src/services/photoExportService.js +++ b/backend/src/services/photoExportService.js @@ -23,13 +23,13 @@ class PhotoExportService { */ async getPhotosWithFeedback(eventId, photoIds = null) { let query = db('photos') - .leftJoin('categories', 'photos.category_id', 'categories.id') + .leftJoin('photo_categories', 'photos.category_id', 'photo_categories.id') .where('photos.event_id', eventId) .select( 'photos.id', 'photos.filename', 'photos.original_filename', - 'photos.file_path', + 'photos.path', 'photos.average_rating', 'photos.feedback_count', 'photos.like_count', @@ -37,9 +37,9 @@ class PhotoExportService { 'photos.comment_count', 'photos.width', 'photos.height', - 'photos.file_size', - 'photos.created_at', - 'categories.name as category_name' + 'photos.size_bytes', + 'photos.uploaded_at', + 'photo_categories.name as category_name' ) .orderBy('photos.filename', 'asc'); @@ -86,7 +86,7 @@ class PhotoExportService { const { filename_format = 'original', separator = 'newline' } = options; const filenames = photos.map(photo => - filename_format === 'original' ? photo.original_filename : photo.filename + filename_format === 'original' ? (photo.original_filename || photo.filename) : photo.filename ); let content; @@ -126,14 +126,14 @@ class PhotoExportService { 'category', 'width', 'height', - 'file_size', - 'created_at' + 'size_bytes', + 'uploaded_at' ]; const rows = photos.map(photo => [ - filename_format === 'original' ? photo.original_filename : photo.filename, + filename_format === 'original' ? (photo.original_filename || photo.filename) : photo.filename, photo.original_filename || '', - photo.average_rating ? photo.average_rating.toFixed(2) : '0.00', + photo.average_rating ? parseFloat(photo.average_rating).toFixed(2) : '0.00', photo.feedback_count || 0, photo.like_count || 0, photo.favorite_count || 0, @@ -141,8 +141,8 @@ class PhotoExportService { photo.category_name || '', photo.width || '', photo.height || '', - photo.file_size || '', - photo.created_at ? new Date(photo.created_at).toISOString() : '' + photo.size_bytes || '', + photo.uploaded_at ? new Date(photo.uploaded_at).toISOString() : '' ]); const csvContent = [ @@ -170,7 +170,7 @@ class PhotoExportService { for (const photo of photos) { const baseFilename = filename_format === 'original' - ? photo.original_filename + ? (photo.original_filename || photo.filename) : photo.filename; const xmpFilename = this.xmpGenerator.getXmpFilename(baseFilename); const xmpContent = this.xmpGenerator.generateXmp(photo, options); @@ -209,21 +209,21 @@ class PhotoExportService { photos: photos.map(photo => ({ id: photo.id, filename: photo.filename, - original_filename: photo.original_filename, + original_filename: photo.original_filename || null, category: photo.category_name || null, rating: { - average: photo.average_rating ? parseFloat(photo.average_rating.toFixed(2)) : 0, + average: photo.average_rating ? parseFloat(parseFloat(photo.average_rating).toFixed(2)) : 0, count: photo.feedback_count || 0 }, likes: photo.like_count || 0, favorites: photo.favorite_count || 0, comments: photo.comment_count || 0, dimensions: { - width: photo.width, - height: photo.height + width: photo.width || null, + height: photo.height || null }, - file_size: photo.file_size, - created_at: photo.created_at + size_bytes: photo.size_bytes || null, + uploaded_at: photo.uploaded_at || null })) }; diff --git a/backend/src/services/photoProcessor.js b/backend/src/services/photoProcessor.js index 7571af7a..3be6a654 100644 --- a/backend/src/services/photoProcessor.js +++ b/backend/src/services/photoProcessor.js @@ -186,6 +186,7 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ const photoData = { event_id: eventId, filename: newFilename, + original_filename: file.originalname, path: relativePath, thumbnail_path: relativeThumbPath, type: photoType, diff --git a/backend/src/services/xmpGenerator.js b/backend/src/services/xmpGenerator.js index 9c23f8d6..5a70989e 100644 --- a/backend/src/services/xmpGenerator.js +++ b/backend/src/services/xmpGenerator.js @@ -76,7 +76,7 @@ class XmpGenerator { * @returns {string} Description XML */ generateDescription(photo) { - const rating = photo.average_rating ? photo.average_rating.toFixed(1) : '0'; + const rating = photo.average_rating ? parseFloat(photo.average_rating).toFixed(1) : '0'; const likes = photo.like_count || 0; const favorites = photo.favorite_count || 0;