diff --git a/src/controllers/feedbackController.js b/src/controllers/feedbackController.js index e4872ae..6b28d88 100644 --- a/src/controllers/feedbackController.js +++ b/src/controllers/feedbackController.js @@ -1,8 +1,177 @@ +const mongoose = require('mongoose'); const Feedback = require('../models/Feedback'); const Ride = require('../models/Ride'); const RideRequest = require('../models/RideRequest'); const { User } = require('../models/user'); +// ============================================================ +// 🔧 HELPER FUNCTIONS (definite prima per essere disponibili) +// ============================================================ + +/** + * Converti userId in ObjectId in modo sicuro + */ +const toObjectId = (id) => { + if (!id) return null; + + if (id instanceof mongoose.Types.ObjectId) { + return id; + } + + if (typeof id === 'object' && id._id) { + return new mongoose.Types.ObjectId(id._id.toString()); + } + + return new mongoose.Types.ObjectId(id.toString()); +}; + +/** + * Ottieni statistiche feedback per un utente + */ +const getStatsForUser = async (idapp, userId) => { + try { + const userObjectId = toObjectId(userId); + + if (!userObjectId) { + return { + averageRating: 0, + totalFeedback: 0, + asDriver: 0, + asPassenger: 0, + distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }, + }; + } + + const result = await Feedback.aggregate([ + { + $match: { + idapp, + toUserId: userObjectId, + }, + }, + { + $group: { + _id: null, + averageRating: { $avg: '$rating' }, + totalFeedback: { $sum: 1 }, + asDriver: { + $sum: { $cond: [{ $eq: ['$role', 'driver'] }, 1, 0] }, + }, + asPassenger: { + $sum: { $cond: [{ $eq: ['$role', 'passenger'] }, 1, 0] }, + }, + rating5: { $sum: { $cond: [{ $eq: ['$rating', 5] }, 1, 0] } }, + rating4: { $sum: { $cond: [{ $eq: ['$rating', 4] }, 1, 0] } }, + rating3: { $sum: { $cond: [{ $eq: ['$rating', 3] }, 1, 0] } }, + rating2: { $sum: { $cond: [{ $eq: ['$rating', 2] }, 1, 0] } }, + rating1: { $sum: { $cond: [{ $eq: ['$rating', 1] }, 1, 0] } }, + }, + }, + ]); + + if (!result || result.length === 0) { + return { + averageRating: 0, + totalFeedback: 0, + asDriver: 0, + asPassenger: 0, + distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }, + }; + } + + const stats = result[0]; + + return { + averageRating: stats.averageRating + ? Math.round(stats.averageRating * 10) / 10 + : 0, + totalFeedback: stats.totalFeedback || 0, + asDriver: stats.asDriver || 0, + asPassenger: stats.asPassenger || 0, + distribution: { + 1: stats.rating1 || 0, + 2: stats.rating2 || 0, + 3: stats.rating3 || 0, + 4: stats.rating4 || 0, + 5: stats.rating5 || 0, + }, + }; + } catch (error) { + console.error('Errore calcolo stats feedback:', error); + return { + averageRating: 0, + totalFeedback: 0, + asDriver: 0, + asPassenger: 0, + distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }, + }; + } +}; + +/** + * Calcola la distribuzione dei rating per un utente + */ +const getRatingDistribution = async (idapp, userId) => { + try { + const userObjectId = toObjectId(userId); + + if (!userObjectId) { + return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; + } + + const result = await Feedback.aggregate([ + { + $match: { + idapp, + toUserId: userObjectId, + }, + }, + { + $group: { + _id: '$rating', + count: { $sum: 1 }, + }, + }, + { $sort: { _id: -1 } }, + ]); + + const distribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; + + result.forEach((r) => { + if (r._id >= 1 && r._id <= 5) { + distribution[r._id] = r.count; + } + }); + + return distribution; + } catch (error) { + console.error('Errore calcolo distribuzione:', error); + return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; + } +}; + +/** + * Aggiorna la media rating nel profilo utente + */ +const updateUserRating = async (idapp, userId) => { + try { + const stats = await getStatsForUser(idapp, userId); + + await User.findByIdAndUpdate(userId, { + $set: { + 'profile.driverProfile.averageRating': stats.averageRating, + 'profile.driverProfile.totalFeedback': stats.totalFeedback, + }, + }); + } catch (error) { + console.error('Errore aggiornamento rating utente:', error); + } +}; + +// ============================================================ +// 📝 CONTROLLER FUNCTIONS +// ============================================================ + /** * @desc Crea un feedback per un viaggio * @route POST /api/viaggi/feedback @@ -22,7 +191,7 @@ const createFeedback = async (req, res) => { pros, cons, tags, - isPublic + isPublic, } = req.body; const fromUserId = req.user._id; @@ -31,7 +200,7 @@ const createFeedback = async (req, res) => { if (!idapp || !rideId || !toUserId || !role || !rating) { return res.status(400).json({ success: false, - message: 'Campi obbligatori: idapp, rideId, toUserId, role, rating' + message: 'Campi obbligatori: idapp, rideId, toUserId, role, rating', }); } @@ -39,29 +208,29 @@ const createFeedback = async (req, res) => { if (rating < 1 || rating > 5) { return res.status(400).json({ success: false, - message: 'Il rating deve essere tra 1 e 5' + message: 'Il rating deve essere tra 1 e 5', }); } - // Verifica che il ride esista e sia completato + // Verifica che il ride esista const ride = await Ride.findById(rideId); if (!ride) { return res.status(404).json({ success: false, - message: 'Viaggio non trovato' + message: 'Viaggio non trovato', }); } // Verifica che l'utente abbia partecipato al viaggio const wasDriver = ride.userId.toString() === fromUserId.toString(); - const wasPassenger = ride.confirmedPassengers.some( - p => p.userId.toString() === fromUserId.toString() + const wasPassenger = ride.confirmedPassengers?.some( + (p) => p.userId.toString() === fromUserId.toString() ); if (!wasDriver && !wasPassenger) { return res.status(403).json({ success: false, - message: 'Non hai partecipato a questo viaggio' + message: 'Non hai partecipato a questo viaggio', }); } @@ -69,7 +238,7 @@ const createFeedback = async (req, res) => { if (fromUserId.toString() === toUserId.toString()) { return res.status(400).json({ success: false, - message: 'Non puoi valutare te stesso' + message: 'Non puoi valutare te stesso', }); } @@ -77,13 +246,13 @@ const createFeedback = async (req, res) => { const existingFeedback = await Feedback.findOne({ rideId, fromUserId, - toUserId + toUserId, }); if (existingFeedback) { return res.status(400).json({ success: false, - message: 'Hai già lasciato un feedback per questo utente in questo viaggio' + message: 'Hai già lasciato un feedback per questo utente in questo viaggio', }); } @@ -94,7 +263,8 @@ const createFeedback = async (req, res) => { fromUserId, toUserId, role, - rating + rating, + isVerified: ride.status === 'completed', }; if (rideRequestId) feedbackData.rideRequestId = rideRequestId; @@ -105,11 +275,6 @@ const createFeedback = async (req, res) => { if (tags) feedbackData.tags = tags; if (isPublic !== undefined) feedbackData.isPublic = isPublic; - // Verifica automatica se il viaggio è completato - if (ride.status === 'completed') { - feedbackData.isVerified = true; - } - const feedback = new Feedback(feedbackData); await feedback.save(); @@ -119,7 +284,7 @@ const createFeedback = async (req, res) => { // Aggiorna flag nella richiesta se presente if (rideRequestId) { await RideRequest.findByIdAndUpdate(rideRequestId, { - feedbackGiven: true + $set: { feedbackGiven: true }, }); } @@ -129,15 +294,14 @@ const createFeedback = async (req, res) => { res.status(201).json({ success: true, message: 'Feedback inviato con successo!', - data: feedback + data: feedback, }); - } catch (error) { console.error('Errore creazione feedback:', error); res.status(500).json({ success: false, message: 'Errore nella creazione del feedback', - error: error.message + error: error.message, }); } }; @@ -155,14 +319,14 @@ const getUserFeedback = async (req, res) => { if (!idapp) { return res.status(400).json({ success: false, - message: 'idapp è obbligatorio' + message: 'idapp è obbligatorio', }); } const query = { idapp, toUserId: userId, - isPublic: true + isPublic: true, }; if (role) { @@ -179,7 +343,7 @@ const getUserFeedback = async (req, res) => { .skip(skip) .limit(parseInt(limit)), Feedback.countDocuments(query), - getStatsForUser(idapp, userId) + getStatsForUser(idapp, userId), ]); res.json({ @@ -190,16 +354,15 @@ const getUserFeedback = async (req, res) => { page: parseInt(page), limit: parseInt(limit), total, - pages: Math.ceil(total / parseInt(limit)) - } + pages: Math.ceil(total / parseInt(limit)), + }, }); - } catch (error) { console.error('Errore recupero feedbacks:', error); res.status(500).json({ success: false, message: 'Errore nel recupero dei feedback', - error: error.message + error: error.message, }); } }; @@ -212,34 +375,27 @@ const getUserFeedback = async (req, res) => { const getUserFeedbackStats = async (req, res) => { try { const { userId } = req.params; - const idapp = req.user.idapp; + const { idapp } = req.query; if (!idapp) { return res.status(400).json({ success: false, - message: 'idapp è obbligatorio' + message: 'idapp è obbligatorio', }); } - const [stats, distribution] = await Promise.all([ - getStatsForUser(idapp, userId), - getRatingDistribution(idapp, userId) - ]); + const stats = await getStatsForUser(idapp, userId); res.json({ success: true, - data: { - ...stats, - distribution - } + data: stats, }); - } catch (error) { console.error('Errore recupero stats:', error); res.status(500).json({ success: false, message: 'Errore nel recupero delle statistiche', - error: error.message + error: error.message, }); } }; @@ -247,35 +403,33 @@ const getUserFeedbackStats = async (req, res) => { /** * @desc Ottieni i feedback per un viaggio * @route GET /api/viaggi/feedback/ride/:rideId - * @access Public (con info limitate) / Private (info complete) + * @access Public/Private */ const getRideFeedback = async (req, res) => { try { const { rideId } = req.params; - const idapp = req.user.idapp; + const { idapp } = req.query; const userId = req.user?._id; if (!idapp) { return res.status(400).json({ success: false, - message: 'idapp è obbligatorio' + message: 'idapp è obbligatorio', }); } - // Verifica che il ride esista const ride = await Ride.findById(rideId); if (!ride) { return res.status(404).json({ success: false, - message: 'Viaggio non trovato' + message: 'Viaggio non trovato', }); } - // Query base per feedback pubblici - const query = { - idapp, + const query = { + idapp, rideId, - isPublic: true + isPublic: true, }; const feedbacks = await Feedback.find(query) @@ -283,31 +437,28 @@ const getRideFeedback = async (req, res) => { .populate('toUserId', 'username name surname profile.img') .sort({ createdAt: -1 }); - // Se l'utente è autenticato e ha partecipato, mostra info aggiuntive let pendingFeedbacks = []; let myFeedbacks = []; if (userId) { const wasDriver = ride.userId.toString() === userId.toString(); - const wasPassenger = ride.confirmedPassengers.some( - p => p.userId.toString() === userId.toString() + const wasPassenger = ride.confirmedPassengers?.some( + (p) => p.userId.toString() === userId.toString() ); if (wasDriver || wasPassenger) { myFeedbacks = feedbacks.filter( - f => f.fromUserId._id.toString() === userId.toString() + (f) => f.fromUserId._id.toString() === userId.toString() ); if (wasDriver) { - // Il conducente deve dare feedback ai passeggeri - const feedbackGivenTo = myFeedbacks.map(f => f.toUserId._id.toString()); - pendingFeedbacks = ride.confirmedPassengers - .filter(p => !feedbackGivenTo.includes(p.userId.toString())) - .map(p => ({ userId: p.userId, role: 'passenger' })); + const feedbackGivenTo = myFeedbacks.map((f) => f.toUserId._id.toString()); + pendingFeedbacks = (ride.confirmedPassengers || []) + .filter((p) => !feedbackGivenTo.includes(p.userId.toString())) + .map((p) => ({ userId: p.userId, role: 'passenger' })); } else { - // Il passeggero deve dare feedback al conducente const hasGivenToDriver = myFeedbacks.some( - f => f.toUserId._id.toString() === ride.userId.toString() + (f) => f.toUserId._id.toString() === ride.userId.toString() ); if (!hasGivenToDriver) { pendingFeedbacks.push({ userId: ride.userId, role: 'driver' }); @@ -321,16 +472,15 @@ const getRideFeedback = async (req, res) => { data: { feedbacks, pendingFeedbacks, - myFeedbacks - } + myFeedbacks, + }, }); - } catch (error) { console.error('Errore recupero feedbacks viaggio:', error); res.status(500).json({ success: false, message: 'Errore', - error: error.message + error: error.message, }); } }; @@ -339,34 +489,31 @@ const getRideFeedback = async (req, res) => { * @desc Verifica se l'utente può lasciare un feedback * @route GET /api/viaggi/feedback/can-leave/:rideId/:toUserId * @access Private - * @note NUOVA FUNZIONE - Era mancante! */ const canLeaveFeedback = async (req, res) => { try { const { rideId, toUserId } = req.params; - const idapp = req.user.idapp; + const { idapp } = req.query; const fromUserId = req.user._id; if (!idapp) { return res.status(400).json({ success: false, - message: 'idapp è obbligatorio' + message: 'idapp è obbligatorio', }); } - // Verifica che il ride esista const ride = await Ride.findById(rideId); if (!ride) { return res.status(404).json({ success: false, - message: 'Viaggio non trovato' + message: 'Viaggio non trovato', }); } - // Verifica che l'utente abbia partecipato al viaggio const wasDriver = ride.userId.toString() === fromUserId.toString(); - const wasPassenger = ride.confirmedPassengers.some( - p => p.userId.toString() === fromUserId.toString() + const wasPassenger = ride.confirmedPassengers?.some( + (p) => p.userId.toString() === fromUserId.toString() ); if (!wasDriver && !wasPassenger) { @@ -374,26 +521,24 @@ const canLeaveFeedback = async (req, res) => { success: true, data: { canLeave: false, - reason: 'Non hai partecipato a questo viaggio' - } + reason: 'Non hai partecipato a questo viaggio', + }, }); } - // Verifica che non stia valutando se stesso if (fromUserId.toString() === toUserId.toString()) { return res.json({ success: true, data: { canLeave: false, - reason: 'Non puoi valutare te stesso' - } + reason: 'Non puoi valutare te stesso', + }, }); } - // Verifica che toUserId abbia partecipato al viaggio const toUserWasDriver = ride.userId.toString() === toUserId.toString(); - const toUserWasPassenger = ride.confirmedPassengers.some( - p => p.userId.toString() === toUserId.toString() + const toUserWasPassenger = ride.confirmedPassengers?.some( + (p) => p.userId.toString() === toUserId.toString() ); if (!toUserWasDriver && !toUserWasPassenger) { @@ -401,28 +546,26 @@ const canLeaveFeedback = async (req, res) => { success: true, data: { canLeave: false, - reason: 'L\'utente destinatario non ha partecipato a questo viaggio' - } + reason: "L'utente destinatario non ha partecipato a questo viaggio", + }, }); } - // Verifica che il viaggio sia completato if (ride.status !== 'completed') { return res.json({ success: true, data: { canLeave: false, reason: 'Il viaggio non è ancora stato completato', - rideStatus: ride.status - } + rideStatus: ride.status, + }, }); } - // Verifica che non esista già un feedback const existingFeedback = await Feedback.findOne({ rideId, fromUserId, - toUserId + toUserId, }); if (existingFeedback) { @@ -431,45 +574,40 @@ const canLeaveFeedback = async (req, res) => { data: { canLeave: false, reason: 'Hai già lasciato un feedback per questo utente in questo viaggio', - existingFeedbackId: existingFeedback._id - } + existingFeedbackId: existingFeedback._id, + }, }); } - // Determina il ruolo del destinatario const toUserRole = toUserWasDriver ? 'driver' : 'passenger'; - - // Recupera info utente destinatario - const toUser = await User.findById(toUserId) - .select('username name surname profile.img'); + const toUser = await User.findById(toUserId).select('username name surname profile.img'); res.json({ success: true, data: { canLeave: true, - toUser: { + toUser: toUser ? { _id: toUser._id, username: toUser.username, name: toUser.name, surname: toUser.surname, - img: toUser.profile?.img - }, + img: toUser.profile?.img, + } : null, toUserRole, ride: { _id: ride._id, departure: ride.departure, destination: ride.destination, - dateTime: ride.dateTime - } - } + dateTime: ride.dateTime, + }, + }, }); - } catch (error) { console.error('Errore verifica canLeaveFeedback:', error); res.status(500).json({ success: false, message: 'Errore nella verifica', - error: error.message + error: error.message, }); } }; @@ -485,10 +623,10 @@ const respondToFeedback = async (req, res) => { const { text } = req.body; const userId = req.user._id; - if (!text) { + if (!text || !text.trim()) { return res.status(400).json({ success: false, - message: 'Il testo della risposta è obbligatorio' + message: 'Il testo della risposta è obbligatorio', }); } @@ -497,45 +635,41 @@ const respondToFeedback = async (req, res) => { if (!feedback) { return res.status(404).json({ success: false, - message: 'Feedback non trovato' + message: 'Feedback non trovato', }); } - // Solo chi ha ricevuto il feedback può rispondere if (feedback.toUserId.toString() !== userId.toString()) { return res.status(403).json({ success: false, - message: 'Non sei autorizzato a rispondere a questo feedback' + message: 'Non sei autorizzato a rispondere a questo feedback', }); } - // Verifica che non abbia già risposto - if (feedback.response && feedback.response.text) { + if (feedback.response?.text) { return res.status(400).json({ success: false, - message: 'Hai già risposto a questo feedback' + message: 'Hai già risposto a questo feedback', }); } - // Aggiungi la risposta feedback.response = { - text, - createdAt: new Date() + text: text.trim(), + createdAt: new Date(), }; await feedback.save(); res.json({ success: true, message: 'Risposta aggiunta', - data: feedback + data: feedback, }); - } catch (error) { console.error('Errore risposta feedback:', error); res.status(500).json({ success: false, message: 'Errore', - error: error.message + error: error.message, }); } }; @@ -555,29 +689,25 @@ const markAsHelpful = async (req, res) => { if (!feedback) { return res.status(404).json({ success: false, - message: 'Feedback non trovato' + message: 'Feedback non trovato', }); } - // Inizializza helpful se non esiste if (!feedback.helpful) { feedback.helpful = { count: 0, users: [] }; } - // Verifica se l'utente ha già segnato come utile const userIdStr = userId.toString(); const alreadyMarked = feedback.helpful.users.some( - u => u.toString() === userIdStr + (u) => u.toString() === userIdStr ); if (alreadyMarked) { - // Rimuovi il voto feedback.helpful.users = feedback.helpful.users.filter( - u => u.toString() !== userIdStr + (u) => u.toString() !== userIdStr ); feedback.helpful.count = Math.max(0, feedback.helpful.count - 1); } else { - // Aggiungi il voto feedback.helpful.users.push(userId); feedback.helpful.count += 1; } @@ -587,18 +717,17 @@ const markAsHelpful = async (req, res) => { res.json({ success: true, message: alreadyMarked ? 'Voto rimosso' : 'Feedback segnato come utile', - data: { + data: { helpfulCount: feedback.helpful.count, - isHelpful: !alreadyMarked - } + isHelpful: !alreadyMarked, + }, }); - } catch (error) { console.error('Errore mark helpful:', error); res.status(500).json({ success: false, message: 'Errore', - error: error.message + error: error.message, }); } }; @@ -614,10 +743,10 @@ const reportFeedback = async (req, res) => { const { reason } = req.body; const userId = req.user._id; - if (!reason) { + if (!reason || !reason.trim()) { return res.status(400).json({ success: false, - message: 'La motivazione è obbligatoria' + message: 'La motivazione è obbligatoria', }); } @@ -626,35 +755,31 @@ const reportFeedback = async (req, res) => { if (!feedback) { return res.status(404).json({ success: false, - message: 'Feedback non trovato' + message: 'Feedback non trovato', }); } - // Inizializza reports se non esiste if (!feedback.reports) { feedback.reports = []; } - // Verifica se l'utente ha già segnalato const alreadyReported = feedback.reports.some( - r => r.userId.toString() === userId.toString() + (r) => r.userId.toString() === userId.toString() ); if (alreadyReported) { return res.status(400).json({ success: false, - message: 'Hai già segnalato questo feedback' + message: 'Hai già segnalato questo feedback', }); } - // Aggiungi la segnalazione feedback.reports.push({ userId, - reason, - createdAt: new Date() + reason: reason.trim(), + createdAt: new Date(), }); - // Se ci sono troppe segnalazioni, nascondi automaticamente if (feedback.reports.length >= 3) { feedback.isPublic = false; feedback.hiddenReason = 'Nascosto automaticamente per multiple segnalazioni'; @@ -664,15 +789,14 @@ const reportFeedback = async (req, res) => { res.json({ success: true, - message: 'Feedback segnalato. Lo esamineremo al più presto.' + message: 'Feedback segnalato. Lo esamineremo al più presto.', }); - } catch (error) { console.error('Errore segnalazione:', error); res.status(500).json({ success: false, message: 'Errore', - error: error.message + error: error.message, }); } }; @@ -690,7 +814,7 @@ const getMyGivenFeedback = async (req, res) => { if (!idapp) { return res.status(400).json({ success: false, - message: 'idapp è obbligatorio' + message: 'idapp è obbligatorio', }); } @@ -703,7 +827,7 @@ const getMyGivenFeedback = async (req, res) => { .sort({ createdAt: -1 }) .skip(skip) .limit(parseInt(limit)), - Feedback.countDocuments({ idapp, fromUserId: userId }) + Feedback.countDocuments({ idapp, fromUserId: userId }), ]); res.json({ @@ -713,16 +837,15 @@ const getMyGivenFeedback = async (req, res) => { page: parseInt(page), limit: parseInt(limit), total, - pages: Math.ceil(total / parseInt(limit)) - } + pages: Math.ceil(total / parseInt(limit)), + }, }); - } catch (error) { console.error('Errore recupero feedback dati:', error); res.status(500).json({ success: false, message: 'Errore', - error: error.message + error: error.message, }); } }; @@ -740,7 +863,7 @@ const getMyReceivedFeedback = async (req, res) => { if (!idapp) { return res.status(400).json({ success: false, - message: 'idapp è obbligatorio' + message: 'idapp è obbligatorio', }); } @@ -754,7 +877,7 @@ const getMyReceivedFeedback = async (req, res) => { .skip(skip) .limit(parseInt(limit)), Feedback.countDocuments({ idapp, toUserId: userId }), - getStatsForUser(idapp, userId) + getStatsForUser(idapp, userId), ]); res.json({ @@ -765,162 +888,44 @@ const getMyReceivedFeedback = async (req, res) => { page: parseInt(page), limit: parseInt(limit), total, - pages: Math.ceil(total / parseInt(limit)) - } + pages: Math.ceil(total / parseInt(limit)), + }, }); - } catch (error) { console.error('Errore recupero feedback ricevuti:', error); res.status(500).json({ success: false, message: 'Errore', - error: error.message + error: error.message, }); } }; - -// ============================================================ -// 🔧 HELPER FUNCTIONS -// ============================================================ - -/** - * Calcola le statistiche feedback per un utente - */ -async function getStatsForUser(idapp, userId) { - try { - const result = await Feedback.aggregate([ - { - $match: { - idapp, - toUserId: typeof userId === 'string' - ? require('mongoose').Types.ObjectId(userId) - : userId - } - }, - { - $group: { - _id: null, - averageRating: { $avg: '$rating' }, - totalFeedback: { $sum: 1 }, - asDriver: { - $sum: { $cond: [{ $eq: ['$role', 'driver'] }, 1, 0] } - }, - asPassenger: { - $sum: { $cond: [{ $eq: ['$role', 'passenger'] }, 1, 0] } - } - } - } - ]); - - if (result.length === 0) { - return { - averageRating: 0, - totalFeedback: 0, - asDriver: 0, - asPassenger: 0 - }; - } - - return { - averageRating: Math.round(result[0].averageRating * 10) / 10, - totalFeedback: result[0].totalFeedback, - asDriver: result[0].asDriver, - asPassenger: result[0].asPassenger - }; - } catch (error) { - console.error('Errore calcolo stats:', error); - return { - averageRating: 0, - totalFeedback: 0, - asDriver: 0, - asPassenger: 0 - }; - } -} - -/** - * Calcola la distribuzione dei rating per un utente - */ -async function getRatingDistribution(idapp, userId) { - try { - const result = await Feedback.aggregate([ - { - $match: { - idapp, - toUserId: typeof userId === 'string' - ? require('mongoose').Types.ObjectId(userId) - : userId - } - }, - { - $group: { - _id: '$rating', - count: { $sum: 1 } - } - }, - { $sort: { _id: -1 } } - ]); - - // Crea distribuzione completa 1-5 - const distribution = {}; - for (let i = 1; i <= 5; i++) { - distribution[i] = 0; - } - - result.forEach(r => { - distribution[r._id] = r.count; - }); - - return distribution; - } catch (error) { - console.error('Errore calcolo distribuzione:', error); - return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; - } -} - -/** - * Aggiorna la media rating nel profilo utente - */ -async function updateUserRating(idapp, userId) { - try { - const stats = await getStatsForUser(idapp, userId); - - await User.findByIdAndUpdate(userId, { - 'profile.driverProfile.averageRating': stats.averageRating, - 'profile.driverProfile.totalFeedback': stats.totalFeedback - }); - } catch (error) { - console.error('Errore aggiornamento rating utente:', error); - } -} - - // ============================================================ // 📤 EXPORTS // ============================================================ module.exports = { - // Funzioni principali (nomi corretti per le routes) + // Controller functions createFeedback, - getUserFeedback, // GET /feedback/user/:userId - getUserFeedbackStats, // GET /feedback/user/:userId/stats - getRideFeedback, // GET /feedback/ride/:rideId - canLeaveFeedback, // GET /feedback/can-leave/:rideId/:toUserId ← NUOVA! - respondToFeedback, // POST /feedback/:id/response - reportFeedback, // POST /feedback/:id/report - markAsHelpful, // POST /feedback/:id/helpful - getMyGivenFeedback, // GET /feedback/my/given - getMyReceivedFeedback, // GET /feedback/my/received - - // Alias per compatibilità (vecchi nomi) + getUserFeedback, + getUserFeedbackStats, + getRideFeedback, + canLeaveFeedback, + respondToFeedback, + reportFeedback, + markAsHelpful, + getMyGivenFeedback, + getMyReceivedFeedback, + + // Alias per compatibilità getFeedbacksForUser: getUserFeedback, getFeedbacksForRide: getRideFeedback, getMyGivenFeedbacks: getMyGivenFeedback, getMyReceivedFeedbacks: getMyReceivedFeedback, - - // Helper functions (esportate per uso in altri moduli) + + // Helper functions getStatsForUser, getRatingDistribution, - updateUserRating + updateUserRating, }; \ No newline at end of file diff --git a/src/controllers/rideController.js b/src/controllers/rideController.js index 64b29b9..29fb38c 100644 --- a/src/controllers/rideController.js +++ b/src/controllers/rideController.js @@ -1,6 +1,8 @@ const Ride = require('../models/Ride'); const { User } = require('../models/user'); const RideRequest = require('../models/RideRequest'); +const Feedback = require('../models/Feedback'); +const Chat = require('../models/Chat'); /** * @desc Crea un nuovo viaggio (offerta o richiesta) diff --git a/src/controllers/rideRequestController.js b/src/controllers/rideRequestController.js index 56c65cb..7025683 100644 --- a/src/controllers/rideRequestController.js +++ b/src/controllers/rideRequestController.js @@ -1,8 +1,21 @@ +const mongoose = require('mongoose'); const RideRequest = require('../models/RideRequest'); const Ride = require('../models/Ride'); const Chat = require('../models/Chat'); const Message = require('../models/Message'); +/** + * Helper per convertire ID in ObjectId + */ +const toObjectId = (id) => { + if (!id) return null; + if (id instanceof mongoose.Types.ObjectId) return id; + if (typeof id === 'object' && id._id) { + return new mongoose.Types.ObjectId(id._id.toString()); + } + return new mongoose.Types.ObjectId(id.toString()); +}; + /** * @desc Crea una richiesta di passaggio * @route POST /api/viaggi/requests @@ -11,7 +24,6 @@ const Message = require('../models/Message'); const createRequest = async (req, res) => { try { const { - idapp, rideId, message, pickupPoint, @@ -29,6 +41,7 @@ const createRequest = async (req, res) => { contribution, } = req.body; + const idapp = req.body.idapp || req.query.idapp || req.user?.idapp; const passengerId = req.user._id; // Validazione @@ -72,10 +85,10 @@ const createRequest = async (req, res) => { // Verifica disponibilità posti const seats = seatsRequested || 1; - if (ride.type === 'offer' && ride.passengers.available < seats) { + if (ride.type === 'offer' && ride.passengers?.available < seats) { return res.status(400).json({ success: false, - message: `Posti insufficienti. Disponibili: ${ride.passengers.available}`, + message: `Posti insufficienti. Disponibili: ${ride.passengers?.available || 0}`, }); } @@ -116,22 +129,40 @@ const createRequest = async (req, res) => { await rideRequest.populate('rideId', 'departure destination dateTime'); // Crea o recupera la chat tra passeggero e conducente - const chat = await Chat.findOrCreateDirect(idapp, passengerId, ride.userId, rideId); + let chat; + let chatId = null; + + try { + chat = await Chat.findOrCreateDirect(idapp, passengerId, ride.userId, rideId); + chatId = chat._id; - // Invia messaggio automatico nella chat - if (message) { - const chatMessage = new Message({ - idapp, - chatId: chat._id, - senderId: passengerId, - text: message, - type: 'ride_request', - metadata: { - rideId, - rideRequestId: rideRequest._id, - }, - }); - await chatMessage.save(); + // Invia messaggio automatico nella chat + if (message) { + const chatMessage = new Message({ + idapp, + chatId: chat._id, + senderId: passengerId, + text: message, + type: 'ride_request', + metadata: { + rideId, + rideRequestId: rideRequest._id, + }, + }); + await chatMessage.save(); + + // Aggiorna lastMessage della chat + chat.lastMessage = { + text: message.substring(0, 100), + senderId: passengerId, + timestamp: new Date(), + type: 'ride_request', + }; + await chat.save(); + } + } catch (chatError) { + console.error('Errore creazione chat:', chatError); + // Non bloccare la creazione della richiesta se la chat fallisce } // TODO: Inviare notifica push al conducente @@ -140,7 +171,7 @@ const createRequest = async (req, res) => { success: true, message: 'Richiesta di passaggio inviata!', data: rideRequest, - chatId: chat._id, + chatId, }); } catch (error) { console.error('Errore creazione richiesta:', error); @@ -160,9 +191,17 @@ const createRequest = async (req, res) => { const getRequestsForRide = async (req, res) => { try { const { rideId } = req.params; - const { idapp, status } = req.query; + const { status } = req.query; + const idapp = req.query.idapp || req.user?.idapp; const userId = req.user._id; + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio', + }); + } + // Verifica che l'utente sia il proprietario del ride const ride = await Ride.findById(rideId); if (!ride) { @@ -213,7 +252,8 @@ const getRequestsForRide = async (req, res) => { const getMyRequests = async (req, res) => { try { const userId = req.user._id; - const { idapp, status, page = 1, limit = 20 } = req.query; + const { status, page = 1, limit = 20 } = req.query; + const idapp = req.query.idapp || req.user?.idapp; if (!idapp) { return res.status(400).json({ @@ -272,7 +312,7 @@ const getMyRequests = async (req, res) => { const getPendingRequests = async (req, res) => { try { const userId = req.user._id; - const idapp = req.user.idapp; + const idapp = req.query.idapp || req.user?.idapp; if (!idapp) { return res.status(400).json({ @@ -307,15 +347,23 @@ const getPendingRequests = async (req, res) => { /** * @desc Accetta una richiesta di passaggio - * @route PUT /api/viaggi/requests/:id/accept + * @route POST /api/viaggi/requests/:id/accept * @access Private (solo conducente) */ const acceptRequest = async (req, res) => { try { const { id } = req.params; - const { responseMessage, idapp } = req.body; + const { responseMessage } = req.body; + const idapp = req.body.idapp || req.query.idapp || req.user?.idapp; const userId = req.user._id; + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio', + }); + } + const request = await RideRequest.findById(id).populate('rideId'); if (!request) { @@ -343,7 +391,14 @@ const acceptRequest = async (req, res) => { // Verifica disponibilità posti const ride = request.rideId; - if (ride.passengers.available < request.seatsRequested) { + if (!ride) { + return res.status(404).json({ + success: false, + message: 'Viaggio associato non trovato', + }); + } + + if ((ride.passengers?.available || 0) < request.seatsRequested) { return res.status(400).json({ success: false, message: 'Posti non più disponibili', @@ -357,6 +412,10 @@ const acceptRequest = async (req, res) => { await request.save(); // Aggiorna il ride con il passeggero + if (!ride.confirmedPassengers) { + ride.confirmedPassengers = []; + } + ride.confirmedPassengers.push({ userId: request.passengerId, seats: request.seatsRequested, @@ -364,22 +423,59 @@ const acceptRequest = async (req, res) => { dropoffPoint: request.dropoffPoint || ride.destination, confirmedAt: new Date(), }); - await ride.updateAvailableSeats(); + + // Aggiorna posti disponibili + if (typeof ride.updateAvailableSeats === 'function') { + await ride.updateAvailableSeats(); + } else { + // Fallback manuale + const totalConfirmed = ride.confirmedPassengers.reduce( + (sum, p) => sum + (p.seats || 1), + 0 + ); + if (ride.passengers) { + ride.passengers.available = Math.max(0, (ride.passengers.total || 0) - totalConfirmed); + } + await ride.save(); + } // Invia messaggio nella chat - const chat = await Chat.findOrCreateDirect(idapp, userId, request.passengerId, ride._id); - const chatMessage = new Message({ - idapp, - chatId: chat._id, - senderId: userId, - text: responseMessage || '✅ Richiesta accettata! Ci vediamo al punto di partenza.', - type: 'ride_accepted', - metadata: { - rideId: ride._id, - rideRequestId: request._id, - }, - }); - await chatMessage.save(); + try { + const chat = await Chat.findOrCreateDirect(idapp, userId, request.passengerId, ride._id); + const chatMessage = new Message({ + idapp, + chatId: chat._id, + senderId: userId, + text: responseMessage || '✅ Richiesta accettata! Ci vediamo al punto di partenza.', + type: 'ride_accepted', + metadata: { + rideId: ride._id, + rideRequestId: request._id, + }, + }); + await chatMessage.save(); + + // Aggiorna lastMessage + chat.lastMessage = { + text: (responseMessage || '✅ Richiesta accettata!').substring(0, 100), + senderId: userId, + timestamp: new Date(), + type: 'ride_accepted', + }; + + // Incrementa unread per il passeggero + if (!chat.unreadCount) { + chat.unreadCount = new Map(); + } + const passengerIdStr = request.passengerId.toString(); + const current = chat.unreadCount.get(passengerIdStr) || 0; + chat.unreadCount.set(passengerIdStr, current + 1); + chat.markModified('unreadCount'); + + await chat.save(); + } catch (chatError) { + console.error('Errore invio messaggio chat:', chatError); + } // TODO: Inviare notifica push al passeggero @@ -403,15 +499,23 @@ const acceptRequest = async (req, res) => { /** * @desc Rifiuta una richiesta di passaggio - * @route PUT /api/viaggi/requests/:id/reject + * @route POST /api/viaggi/requests/:id/reject * @access Private (solo conducente) */ const rejectRequest = async (req, res) => { try { const { id } = req.params; - const { responseMessage, idapp } = req.body; + const { responseMessage } = req.body; + const idapp = req.body.idapp || req.query.idapp || req.user?.idapp; const userId = req.user._id; + if (!idapp) { + return res.status(400).json({ + success: false, + message: 'idapp è obbligatorio', + }); + } + const request = await RideRequest.findById(id); if (!request) { @@ -444,19 +548,41 @@ const rejectRequest = async (req, res) => { await request.save(); // Invia messaggio nella chat - const chat = await Chat.findOrCreateDirect(idapp, userId, request.passengerId, request.rideId); - const chatMessage = new Message({ - idapp, - chatId: chat._id, - senderId: userId, - text: responseMessage || '❌ Mi dispiace, non posso accettare questa richiesta.', - type: 'ride_rejected', - metadata: { - rideId: request.rideId, - rideRequestId: request._id, - }, - }); - await chatMessage.save(); + try { + const chat = await Chat.findOrCreateDirect(idapp, userId, request.passengerId, request.rideId); + const chatMessage = new Message({ + idapp, + chatId: chat._id, + senderId: userId, + text: responseMessage || '❌ Mi dispiace, non posso accettare questa richiesta.', + type: 'ride_rejected', + metadata: { + rideId: request.rideId, + rideRequestId: request._id, + }, + }); + await chatMessage.save(); + + // Aggiorna lastMessage e unread + chat.lastMessage = { + text: (responseMessage || '❌ Richiesta rifiutata').substring(0, 100), + senderId: userId, + timestamp: new Date(), + type: 'ride_rejected', + }; + + if (!chat.unreadCount) { + chat.unreadCount = new Map(); + } + const passengerIdStr = request.passengerId.toString(); + const current = chat.unreadCount.get(passengerIdStr) || 0; + chat.unreadCount.set(passengerIdStr, current + 1); + chat.markModified('unreadCount'); + + await chat.save(); + } catch (chatError) { + console.error('Errore invio messaggio chat:', chatError); + } // TODO: Inviare notifica push al passeggero @@ -476,8 +602,8 @@ const rejectRequest = async (req, res) => { }; /** - * @desc Cancella una richiesta (dal passeggero) - * @route PUT /api/viaggi/requests/:id/cancel + * @desc Cancella una richiesta (dal passeggero o conducente) + * @route POST /api/viaggi/requests/:id/cancel * @access Private */ const cancelRequest = async (req, res) => { @@ -517,11 +643,23 @@ const cancelRequest = async (req, res) => { // Se era accettata, rimuovi il passeggero dal ride if (request.status === 'accepted') { const ride = await Ride.findById(request.rideId); - if (ride) { + if (ride && ride.confirmedPassengers) { ride.confirmedPassengers = ride.confirmedPassengers.filter( (p) => p.userId.toString() !== request.passengerId.toString() ); - await ride.updateAvailableSeats(); + + if (typeof ride.updateAvailableSeats === 'function') { + await ride.updateAvailableSeats(); + } else { + const totalConfirmed = ride.confirmedPassengers.reduce( + (sum, p) => sum + (p.seats || 1), + 0 + ); + if (ride.passengers) { + ride.passengers.available = Math.max(0, (ride.passengers.total || 0) - totalConfirmed); + } + await ride.save(); + } } } @@ -578,8 +716,11 @@ const getRequestById = async (req, res) => { } // Verifica che l'utente sia coinvolto - const isPassenger = request.passengerId._id.toString() === userId.toString(); - const isDriver = request.driverId._id.toString() === userId.toString(); + const passengerId = request.passengerId?._id || request.passengerId; + const driverId = request.driverId?._id || request.driverId; + + const isPassenger = passengerId?.toString() === userId.toString(); + const isDriver = driverId?.toString() === userId.toString(); if (!isPassenger && !isDriver) { return res.status(403).json({ @@ -611,9 +752,13 @@ const getReceivedRequests = async (req, res) => { try { const driverId = req.user._id; const { status, page = 1, limit = 20 } = req.query; + const idapp = req.query.idapp || req.user?.idapp; // Build query const query = { driverId }; + if (idapp) { + query.idapp = idapp; + } if (status) { query.status = status; } @@ -622,22 +767,28 @@ const getReceivedRequests = async (req, res) => { // Fetch requests const requests = await RideRequest.find(query) - .populate('passengerId', 'name surname profile.img') // <-- FIX: passengerId invece di userId - .populate('rideId', 'departure destination departureDate availableSeats') + .populate('passengerId', 'username name surname profile.img profile.driverProfile.averageRating') + .populate('rideId', 'departure destination dateTime passengers') .sort({ createdAt: -1 }) .skip(skip) .limit(parseInt(limit)) .lean(); // Get counts by status + const driverObjectId = toObjectId(driverId); + const matchQuery = { driverId: driverObjectId }; + if (idapp) { + matchQuery.idapp = idapp; + } + const counts = await RideRequest.aggregate([ - { $match: { driverId } }, + { $match: matchQuery }, { $group: { _id: '$status', - count: { $sum: 1 } - } - } + count: { $sum: 1 }, + }, + }, ]); // Format counts @@ -647,11 +798,11 @@ const getReceivedRequests = async (req, res) => { rejected: 0, cancelled: 0, expired: 0, - completed: 0 + completed: 0, }; - counts.forEach(c => { - if (statusCounts.hasOwnProperty(c._id)) { + counts.forEach((c) => { + if (Object.prototype.hasOwnProperty.call(statusCounts, c._id)) { statusCounts[c._id] = c.count; } }); @@ -668,20 +819,20 @@ const getReceivedRequests = async (req, res) => { page: parseInt(page), limit: parseInt(limit), total, - pages: Math.ceil(total / parseInt(limit)) - } - } + pages: Math.ceil(total / parseInt(limit)), + }, + }, }); - } catch (error) { console.error('Error fetching received requests:', error); res.status(500).json({ success: false, message: 'Errore nel recupero delle richieste ricevute', - error: error.message + error: error.message, }); } }; + /** * @desc Ottieni richieste inviate (io come passeggero) * @route GET /api/viaggi/requests/sent @@ -691,9 +842,13 @@ const getSentRequests = async (req, res) => { try { const userId = req.user._id; const { status, page = 1, limit = 20 } = req.query; + const idapp = req.query.idapp || req.user?.idapp; - // Build query - FIX: usa passengerId invece di userId + // Build query const query = { passengerId: userId }; + if (idapp) { + query.idapp = idapp; + } if (status) { query.status = status; } @@ -702,29 +857,59 @@ const getSentRequests = async (req, res) => { // Fetch requests const requests = await RideRequest.find(query) - .populate('driverId', 'name surname profile.img') - .populate('rideId', 'departure destination departureDate availableSeats currentPassengers') + .populate('driverId', 'username name surname profile.img profile.driverProfile.averageRating') + .populate('rideId', 'departure destination dateTime passengers') .sort({ createdAt: -1 }) .skip(skip) .limit(parseInt(limit)) .lean(); // Enrich with ride info - const enrichedRequests = requests.map(request => { - const req = { ...request }; - - // Add ride info if rideId is populated - if (req.rideId) { - req.rideInfo = { - departure: req.rideId.departure?.city || req.rideId.departure, - destination: req.rideId.destination?.city || req.rideId.destination, - departureDate: req.rideId.departureDate, - availableSeats: req.rideId.availableSeats, - currentPassengers: req.rideId.currentPassengers || 0 + const enrichedRequests = requests.map((request) => { + const enriched = { ...request }; + + if (enriched.rideId) { + enriched.rideInfo = { + departure: enriched.rideId.departure?.city || enriched.rideId.departure, + destination: enriched.rideId.destination?.city || enriched.rideId.destination, + dateTime: enriched.rideId.dateTime, + availableSeats: enriched.rideId.passengers?.available || 0, }; } - return req; + return enriched; + }); + + // Get counts by status + const passengerObjectId = toObjectId(userId); + const matchQuery = { passengerId: passengerObjectId }; + if (idapp) { + matchQuery.idapp = idapp; + } + + const counts = await RideRequest.aggregate([ + { $match: matchQuery }, + { + $group: { + _id: '$status', + count: { $sum: 1 }, + }, + }, + ]); + + const statusCounts = { + pending: 0, + accepted: 0, + rejected: 0, + cancelled: 0, + expired: 0, + completed: 0, + }; + + counts.forEach((c) => { + if (Object.prototype.hasOwnProperty.call(statusCounts, c._id)) { + statusCounts[c._id] = c.count; + } }); // Total count @@ -734,21 +919,21 @@ const getSentRequests = async (req, res) => { success: true, data: { requests: enrichedRequests, + counts: statusCounts, pagination: { page: parseInt(page), limit: parseInt(limit), total, - pages: Math.ceil(total / parseInt(limit)) - } - } + pages: Math.ceil(total / parseInt(limit)), + }, + }, }); - } catch (error) { console.error('Error fetching sent requests:', error); res.status(500).json({ success: false, message: 'Errore nel recupero delle richieste inviate', - error: error.message + error: error.message, }); } }; @@ -764,4 +949,4 @@ module.exports = { getRequestById, getReceivedRequests, getSentRequests, -}; +}; \ No newline at end of file diff --git a/src/routes/viaggiRoutes.js b/src/routes/viaggiRoutes.js index 7be50d9..3905408 100644 --- a/src/routes/viaggiRoutes.js +++ b/src/routes/viaggiRoutes.js @@ -142,6 +142,8 @@ router.get('/cities/recent', authenticate, rideController.getRecentCities); */ router.post('/requests', authenticate, rideRequestController.createRequest); +// ⚠️ IMPORTANTE: Route specifiche PRIMA di quelle con :id + /** * @route GET /api/viaggi/requests/received * @desc Richieste ricevute (sono conducente) @@ -156,6 +158,20 @@ router.get('/requests/received', authenticate, rideRequestController.getReceived */ router.get('/requests/sent', authenticate, rideRequestController.getSentRequests); +/** + * @route GET /api/viaggi/requests/pending + * @desc Richieste pendenti (per il conducente) + * @access Private + */ +router.get('/requests/pending', authenticate, rideRequestController.getPendingRequests); + +/** + * @route GET /api/viaggi/requests/my + * @desc Le mie richieste (come passeggero) + * @access Private + */ +router.get('/requests/my', authenticate, rideRequestController.getMyRequests); + /** * @route GET /api/viaggi/requests/ride/:rideId * @desc Richieste per un viaggio specifico @@ -163,6 +179,8 @@ router.get('/requests/sent', authenticate, rideRequestController.getSentRequests */ router.get('/requests/ride/:rideId', authenticate, rideRequestController.getRequestsForRide); +// ⚠️ Route con :id DOPO tutte le route specifiche + /** * @route GET /api/viaggi/requests/:id * @desc Dettaglio singola richiesta @@ -190,7 +208,6 @@ router.post('/requests/:id/reject', authenticate, rideRequestController.rejectRe * @access Private (driver o passenger) */ router.post('/requests/:id/cancel', authenticate, rideRequestController.cancelRequest); - // ============================================================ // 💬 CHAT - Messaggistica // ============================================================ @@ -414,7 +431,7 @@ router.post('/feedback/:id/helpful', authenticate, feedbackController.markAsHelp router.get('/driver/user/:userId', async (req, res) => { try { const { userId } = req.params; - const idapp = req.user.idapp; + const idapp = req.query.idapp; const { User } = require('../models/user'); const Ride = require('../models/Ride');