const { db, logActivity } = require('../database/db'); const logger = require('../utils/logger'); const { formatBoolean } = require('../utils/dbCompat'); class FeedbackService { /** * Get feedback settings for an event */ async getEventFeedbackSettings(eventId) { try { const settings = await db('event_feedback_settings') .where('event_id', eventId) .first(); if (!settings) { // Return default settings if none exist return { event_id: eventId, feedback_enabled: false, allow_ratings: true, allow_likes: true, allow_comments: false, allow_favorites: true, require_name_email: false, moderate_comments: true, show_feedback_to_guests: true }; } return settings; } catch (error) { logger.error('Error getting feedback settings:', error); throw error; } } /** * Update feedback settings for an event */ async updateEventFeedbackSettings(eventId, settings) { try { const existing = await db('event_feedback_settings') .where('event_id', eventId) .first(); if (existing) { await db('event_feedback_settings') .where('event_id', eventId) .update({ ...settings, updated_at: new Date() }); } else { await db('event_feedback_settings').insert({ event_id: eventId, ...settings, created_at: new Date(), updated_at: new Date() }); } await logActivity('feedback_settings_updated', settings, eventId); return this.getEventFeedbackSettings(eventId); } catch (error) { logger.error('Error updating feedback settings:', error); throw error; } } /** * Submit feedback for a photo */ async submitFeedback(photoId, eventId, feedbackData, guestIdentifier) { try { const { feedback_type, rating, comment_text, guest_name, guest_email, ip_address, user_agent } = feedbackData; // Validate feedback type if (!['rating', 'like', 'comment', 'favorite'].includes(feedback_type)) { throw new Error('Invalid feedback type'); } // Check if similar feedback already exists (prevent duplicates) if (feedback_type !== 'comment') { const existing = await db('photo_feedback') .where({ photo_id: photoId, event_id: eventId, feedback_type, guest_identifier: guestIdentifier }) .first(); if (existing) { if (feedback_type === 'rating' && rating !== existing.rating) { // Update existing rating await db('photo_feedback') .where('id', existing.id) .update({ rating, updated_at: new Date() }); await this.updatePhotoFeedbackStats(photoId); return { id: existing.id, updated: true }; } // For likes and favorites, toggle off if already exists if (feedback_type === 'like' || feedback_type === 'favorite') { await db('photo_feedback') .where('id', existing.id) .delete(); await this.updatePhotoFeedbackStats(photoId); return { removed: true }; } return { id: existing.id, exists: true }; } } // Insert new feedback const [id] = await db('photo_feedback').insert({ photo_id: photoId, event_id: eventId, feedback_type, rating: feedback_type === 'rating' ? rating : null, comment_text: feedback_type === 'comment' ? comment_text : null, guest_name, guest_email, guest_identifier: guestIdentifier, ip_address, user_agent, is_approved: feedback_type !== 'comment' || !feedbackData.moderate_comments, created_at: new Date(), updated_at: new Date() }); // Update photo stats await this.updatePhotoFeedbackStats(photoId); // Log activity await logActivity(`photo_${feedback_type}`, { photo_id: photoId }, eventId); return { id, created: true }; } catch (error) { logger.error('Error submitting feedback:', error); throw error; } } /** * Get feedback for a photo */ async getPhotoFeedback(photoId, options = {}) { try { const query = db('photo_feedback') .where('photo_id', photoId); if (options.feedback_type) { query.where('feedback_type', options.feedback_type); } if (options.approved_only) { query.where('is_approved', true); } if (!options.include_hidden) { query.where('is_hidden', false); } if (options.guest_identifier) { query.where('guest_identifier', options.guest_identifier); } const feedback = await query .orderBy('created_at', 'desc') .select('id', 'feedback_type', 'rating', 'comment_text', 'guest_name', 'created_at'); return feedback; } catch (error) { logger.error('Error getting photo feedback:', error); throw error; } } /** * Get feedback summary for an event */ async getEventFeedbackSummary(eventId) { try { const photos = await db('photos') .where('event_id', eventId) .select('id', 'filename', 'feedback_count', 'like_count', 'average_rating', 'favorite_count') .orderBy('average_rating', 'desc') .orderBy('like_count', 'desc'); const totalStats = await db('photo_feedback') .where('event_id', eventId) .select( db.raw('COUNT(DISTINCT CASE WHEN feedback_type = ? THEN guest_identifier END) as unique_raters', ['rating']), db.raw('COUNT(CASE WHEN feedback_type = ? THEN 1 END) as total_ratings', ['rating']), db.raw('COUNT(CASE WHEN feedback_type = ? THEN 1 END) as total_likes', ['like']), db.raw('COUNT(CASE WHEN feedback_type = ? THEN 1 END) as total_comments', ['comment']), db.raw('COUNT(CASE WHEN feedback_type = ? THEN 1 END) as total_favorites', ['favorite']) ) .first(); return { photos, stats: totalStats }; } catch (error) { logger.error('Error getting feedback summary:', error); throw error; } } /** * Update photo feedback statistics */ async updatePhotoFeedbackStats(photoId) { try { // Get aggregated stats const stats = await db('photo_feedback') .where('photo_id', photoId) .where('is_hidden', false) .select( db.raw('COUNT(CASE WHEN feedback_type = ? AND is_approved = ? THEN 1 END) as comment_count', ['comment', formatBoolean(true)]), db.raw('COUNT(CASE WHEN feedback_type = ? THEN 1 END) as like_count', ['like']), db.raw('COUNT(CASE WHEN feedback_type = ? THEN 1 END) as favorite_count', ['favorite']), db.raw('AVG(CASE WHEN feedback_type = ? THEN rating END) as average_rating', ['rating']), db.raw('COUNT(DISTINCT guest_identifier) as feedback_count') ) .first(); // Update photo table await db('photos') .where('id', photoId) .update({ feedback_count: stats.feedback_count || 0, like_count: stats.like_count || 0, average_rating: stats.average_rating || 0, favorite_count: stats.favorite_count || 0 }); } catch (error) { logger.error('Error updating photo feedback stats:', error); throw error; } } /** * Moderate feedback (approve/hide) */ async moderateFeedback(feedbackId, action, adminId) { try { const updates = { updated_at: new Date() }; if (action === 'approve') { updates.is_approved = true; updates.is_hidden = false; } else if (action === 'hide') { updates.is_hidden = true; } else if (action === 'reject') { updates.is_approved = false; updates.is_hidden = true; } const feedback = await db('photo_feedback') .where('id', feedbackId) .first(); if (!feedback) { throw new Error('Feedback not found'); } await db('photo_feedback') .where('id', feedbackId) .update(updates); // Update photo stats if visibility changed await this.updatePhotoFeedbackStats(feedback.photo_id); // Log moderation action await logActivity('feedback_moderated', { feedback_id: feedbackId, action, admin_id: adminId }, feedback.event_id); return true; } catch (error) { logger.error('Error moderating feedback:', error); throw error; } } /** * Delete feedback */ async deleteFeedback(feedbackId, adminId) { try { const feedback = await db('photo_feedback') .where('id', feedbackId) .first(); if (!feedback) { throw new Error('Feedback not found'); } await db('photo_feedback') .where('id', feedbackId) .delete(); // Update photo stats await this.updatePhotoFeedbackStats(feedback.photo_id); // Log deletion await logActivity('feedback_deleted', { feedback_id: feedbackId, feedback_type: feedback.feedback_type, admin_id: adminId }, feedback.event_id); return true; } catch (error) { logger.error('Error deleting feedback:', error); throw error; } } /** * Get feedback requiring moderation */ async getPendingModeration(eventId = null) { try { let query = db('photo_feedback') .join('photos', 'photo_feedback.photo_id', 'photos.id') .join('events', 'photo_feedback.event_id', 'events.id') .where('photo_feedback.is_approved', false) .where('photo_feedback.is_hidden', false) .where('photo_feedback.feedback_type', 'comment'); if (eventId) { query = query.where('photo_feedback.event_id', eventId); } const pending = await query .select( 'photo_feedback.*', 'photos.filename as photo_filename', 'events.event_name' ) .orderBy('photo_feedback.created_at', 'desc'); return pending; } catch (error) { logger.error('Error getting pending moderation:', error); throw error; } } /** * Export feedback data for an event */ async exportEventFeedback(eventId) { try { const feedback = await db('photo_feedback') .join('photos', 'photo_feedback.photo_id', 'photos.id') .where('photo_feedback.event_id', eventId) .select( 'photos.filename', 'photo_feedback.feedback_type', 'photo_feedback.rating', 'photo_feedback.comment_text', 'photo_feedback.guest_name', 'photo_feedback.guest_email', 'photo_feedback.created_at' ) .orderBy('photos.filename') .orderBy('photo_feedback.created_at'); return feedback; } catch (error) { logger.error('Error exporting feedback:', error); throw error; } } } module.exports = new FeedbackService();