- Implementazione TRASPORTI ! Passo 1

This commit is contained in:
Surya Paolo
2025-12-22 01:19:28 +01:00
parent afeedf27a5
commit 2e7801b4ba
17 changed files with 7078 additions and 2546 deletions

View File

@@ -0,0 +1,611 @@
const Chat = require('../models/Chat');
const Message = require('../models/Message');
const { User } = require('../models/User');
/**
* @desc Ottieni tutte le chat dell'utente
* @route GET /api/trasporti/chats
* @access Private
*/
const getMyChats = async (req, res) => {
try {
const userId = req.user._id;
const { idapp, page = 1, limit = 20 } = req.query;
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è obbligatorio'
});
}
const skip = (parseInt(page) - 1) * parseInt(limit);
const chats = await Chat.find({
idapp,
participants: userId,
isActive: true,
blockedBy: { $ne: userId }
})
.populate('participants', 'username name surname profile.img')
.populate('rideId', 'departure destination dateTime status')
.sort({ updatedAt: -1 })
.skip(skip)
.limit(parseInt(limit));
// Aggiungi conteggio non letti per ogni chat
const chatsWithUnread = chats.map(chat => {
const chatObj = chat.toObject();
chatObj.unreadCount = chat.getUnreadForUser(userId);
// Trova l'altro partecipante (per chat dirette)
if (chat.type === 'direct') {
chatObj.otherParticipant = chat.participants.find(
p => p._id.toString() !== userId.toString()
);
}
return chatObj;
});
const total = await Chat.countDocuments({
idapp,
participants: userId,
isActive: true
});
res.json({
success: true,
data: chatsWithUnread,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / parseInt(limit))
}
});
} catch (error) {
console.error('Errore recupero chat:', error);
res.status(500).json({
success: false,
message: 'Errore nel recupero delle chat',
error: error.message
});
}
};
/**
* @desc Ottieni o crea una chat diretta con un utente
* @route POST /api/trasporti/chats/direct
* @access Private
*/
const getOrCreateDirectChat = async (req, res) => {
try {
const userId = req.user._id;
const { idapp, otherUserId, rideId } = req.body;
if (!idapp || !otherUserId) {
return res.status(400).json({
success: false,
message: 'idapp e otherUserId sono obbligatori'
});
}
// Verifica che l'altro utente esista
const otherUser = await User.findById(otherUserId);
if (!otherUser) {
return res.status(404).json({
success: false,
message: 'Utente non trovato'
});
}
// Non puoi chattare con te stesso
if (userId.toString() === otherUserId) {
return res.status(400).json({
success: false,
message: 'Non puoi creare una chat con te stesso'
});
}
const chat = await Chat.findOrCreateDirect(idapp, userId, otherUserId, rideId);
await chat.populate('participants', 'username name surname profile.img');
await chat.populate('rideId', 'departure destination dateTime');
const chatObj = chat.toObject();
chatObj.otherParticipant = chat.participants.find(
p => p._id.toString() !== userId.toString()
);
chatObj.unreadCount = chat.getUnreadForUser(userId);
res.json({
success: true,
data: chatObj
});
} catch (error) {
console.error('Errore creazione chat:', error);
res.status(500).json({
success: false,
message: 'Errore nella creazione della chat',
error: error.message
});
}
};
/**
* @desc Ottieni una chat per ID
* @route GET /api/trasporti/chats/:id
* @access Private
*/
const getChatById = async (req, res) => {
try {
const { id } = req.params;
const userId = req.user._id;
const chat = await Chat.findById(id)
.populate('participants', 'username name surname profile.img profile.Cell')
.populate('rideId', 'departure destination dateTime status type');
if (!chat) {
return res.status(404).json({
success: false,
message: 'Chat non trovata'
});
}
// Verifica che l'utente sia partecipante
if (!chat.hasParticipant(userId)) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato ad accedere a questa chat'
});
}
// Marca come letta
await chat.markAsRead(userId);
const chatObj = chat.toObject();
chatObj.unreadCount = 0; // Appena marcata come letta
if (chat.type === 'direct') {
chatObj.otherParticipant = chat.participants.find(
p => p._id.toString() !== userId.toString()
);
}
res.json({
success: true,
data: chatObj
});
} catch (error) {
console.error('Errore recupero chat:', error);
res.status(500).json({
success: false,
message: 'Errore nel recupero della chat',
error: error.message
});
}
};
/**
* @desc Ottieni i messaggi di una chat
* @route GET /api/trasporti/chats/:id/messages
* @access Private
*/
const getChatMessages = async (req, res) => {
try {
const { id } = req.params;
const { idapp, before, after, limit = 50 } = req.query;
const userId = req.user._id;
// Verifica accesso alla chat
const chat = await Chat.findById(id);
if (!chat) {
return res.status(404).json({
success: false,
message: 'Chat non trovata'
});
}
if (!chat.hasParticipant(userId)) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato'
});
}
const messages = await Message.getByChat(idapp, id, {
limit: parseInt(limit),
before,
after
});
// Marca messaggi come letti
await Promise.all(
messages
.filter(m => m.senderId && m.senderId._id.toString() !== userId.toString())
.map(m => m.markAsReadBy(userId))
);
// Aggiorna unread count nella chat
await chat.markAsRead(userId);
res.json({
success: true,
data: messages.reverse(), // Ordine cronologico
hasMore: messages.length === parseInt(limit)
});
} catch (error) {
console.error('Errore recupero messaggi:', error);
res.status(500).json({
success: false,
message: 'Errore nel recupero dei messaggi',
error: error.message
});
}
};
/**
* @desc Invia un messaggio
* @route POST /api/trasporti/chats/:id/messages
* @access Private
*/
const sendMessage = async (req, res) => {
try {
const { id } = req.params;
const { idapp, text, type = 'text', metadata, replyTo } = req.body;
const userId = req.user._id;
if (!idapp || !text) {
return res.status(400).json({
success: false,
message: 'idapp e text sono obbligatori'
});
}
// Verifica accesso alla chat
const chat = await Chat.findById(id);
if (!chat) {
return res.status(404).json({
success: false,
message: 'Chat non trovata'
});
}
if (!chat.hasParticipant(userId)) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato'
});
}
// Verifica che la chat non sia bloccata
if (chat.isBlockedFor(userId)) {
return res.status(403).json({
success: false,
message: 'Non puoi inviare messaggi in questa chat'
});
}
// Crea il messaggio
const message = new Message({
idapp,
chatId: id,
senderId: userId,
text,
type,
metadata: metadata || {},
replyTo: replyTo || null,
readBy: [{ userId, readAt: new Date() }] // Il mittente l'ha già letto
});
await message.save();
// Popola per la risposta
await message.populate('senderId', 'username name surname profile.img');
if (replyTo) {
await message.populate('replyTo', 'text senderId');
}
// TODO: Inviare notifica push agli altri partecipanti
res.status(201).json({
success: true,
data: message
});
} catch (error) {
console.error('Errore invio messaggio:', error);
res.status(500).json({
success: false,
message: 'Errore nell\'invio del messaggio',
error: error.message
});
}
};
/**
* @desc Marca tutti i messaggi di una chat come letti
* @route PUT /api/trasporti/chats/:id/read
* @access Private
*/
const markChatAsRead = async (req, res) => {
try {
const { id } = req.params;
const userId = req.user._id;
const chat = await Chat.findById(id);
if (!chat) {
return res.status(404).json({
success: false,
message: 'Chat non trovata'
});
}
if (!chat.hasParticipant(userId)) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato'
});
}
await chat.markAsRead(userId);
// Marca tutti i messaggi come letti
await Message.updateMany(
{
chatId: id,
senderId: { $ne: userId },
'readBy.userId': { $ne: userId }
},
{
$push: { readBy: { userId, readAt: new Date() } }
}
);
res.json({
success: true,
message: 'Chat marcata come letta'
});
} catch (error) {
console.error('Errore marca come letto:', error);
res.status(500).json({
success: false,
message: 'Errore',
error: error.message
});
}
};
/**
* @desc Blocca/sblocca una chat
* @route PUT /api/trasporti/chats/:id/block
* @access Private
*/
const toggleBlockChat = async (req, res) => {
try {
const { id } = req.params;
const { block } = req.body;
const userId = req.user._id;
const chat = await Chat.findById(id);
if (!chat) {
return res.status(404).json({
success: false,
message: 'Chat non trovata'
});
}
if (!chat.hasParticipant(userId)) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato'
});
}
if (block) {
if (!chat.blockedBy.includes(userId)) {
chat.blockedBy.push(userId);
}
} else {
chat.blockedBy = chat.blockedBy.filter(
id => id.toString() !== userId.toString()
);
}
await chat.save();
res.json({
success: true,
message: block ? 'Chat bloccata' : 'Chat sbloccata',
data: { blocked: block }
});
} catch (error) {
console.error('Errore blocco chat:', error);
res.status(500).json({
success: false,
message: 'Errore',
error: error.message
});
}
};
/**
* @desc Muta/smuta notifiche di una chat
* @route PUT /api/trasporti/chats/:id/mute
* @access Private
*/
const toggleMuteChat = async (req, res) => {
try {
const { id } = req.params;
const { mute } = req.body;
const userId = req.user._id;
const chat = await Chat.findById(id);
if (!chat) {
return res.status(404).json({
success: false,
message: 'Chat non trovata'
});
}
if (!chat.hasParticipant(userId)) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato'
});
}
if (mute) {
if (!chat.mutedBy.includes(userId)) {
chat.mutedBy.push(userId);
}
} else {
chat.mutedBy = chat.mutedBy.filter(
id => id.toString() !== userId.toString()
);
}
await chat.save();
res.json({
success: true,
message: mute ? 'Notifiche disattivate' : 'Notifiche attivate',
data: { muted: mute }
});
} catch (error) {
console.error('Errore mute chat:', error);
res.status(500).json({
success: false,
message: 'Errore',
error: error.message
});
}
};
/**
* @desc Conta messaggi non letti totali
* @route GET /api/trasporti/chats/unread/count
* @access Private
*/
const getUnreadCount = async (req, res) => {
try {
const userId = req.user._id;
const { idapp } = req.query;
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è obbligatorio'
});
}
const chats = await Chat.find({
idapp,
participants: userId,
isActive: true
});
let totalUnread = 0;
const chatUnreads = [];
for (const chat of chats) {
const unread = chat.getUnreadForUser(userId);
totalUnread += unread;
if (unread > 0) {
chatUnreads.push({
chatId: chat._id,
unread
});
}
}
res.json({
success: true,
data: {
total: totalUnread,
chats: chatUnreads
}
});
} catch (error) {
console.error('Errore conteggio non letti:', error);
res.status(500).json({
success: false,
message: 'Errore',
error: error.message
});
}
};
/**
* @desc Elimina un messaggio (soft delete)
* @route DELETE /api/trasporti/chats/:chatId/messages/:messageId
* @access Private
*/
const deleteMessage = async (req, res) => {
try {
const { chatId, messageId } = req.params;
const userId = req.user._id;
const message = await Message.findById(messageId);
if (!message) {
return res.status(404).json({
success: false,
message: 'Messaggio non trovato'
});
}
if (message.chatId.toString() !== chatId) {
return res.status(400).json({
success: false,
message: 'Messaggio non appartiene a questa chat'
});
}
// Solo il mittente può eliminare
if (message.senderId.toString() !== userId.toString()) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato a eliminare questo messaggio'
});
}
await message.softDelete();
res.json({
success: true,
message: 'Messaggio eliminato'
});
} catch (error) {
console.error('Errore eliminazione messaggio:', error);
res.status(500).json({
success: false,
message: 'Errore',
error: error.message
});
}
};
module.exports = {
getMyChats,
getOrCreateDirectChat,
getChatById,
getChatMessages,
sendMessage,
markChatAsRead,
toggleBlockChat,
toggleMuteChat,
getUnreadCount,
deleteMessage
};

View File

@@ -0,0 +1,926 @@
const Feedback = require('../models/Feedback');
const Ride = require('../models/Ride');
const RideRequest = require('../models/RideRequest');
const { User } = require('../models/User');
/**
* @desc Crea un feedback per un viaggio
* @route POST /api/trasporti/feedback
* @access Private
*/
const createFeedback = async (req, res) => {
try {
const {
idapp,
rideId,
rideRequestId,
toUserId,
role,
rating,
categories,
comment,
pros,
cons,
tags,
isPublic
} = req.body;
const fromUserId = req.user._id;
// Validazione base
if (!idapp || !rideId || !toUserId || !role || !rating) {
return res.status(400).json({
success: false,
message: 'Campi obbligatori: idapp, rideId, toUserId, role, rating'
});
}
// Verifica rating valido
if (rating < 1 || rating > 5) {
return res.status(400).json({
success: false,
message: 'Il rating deve essere tra 1 e 5'
});
}
// Verifica che il ride esista e sia completato
const ride = await Ride.findById(rideId);
if (!ride) {
return res.status(404).json({
success: false,
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()
);
if (!wasDriver && !wasPassenger) {
return res.status(403).json({
success: false,
message: 'Non hai partecipato a questo viaggio'
});
}
// Verifica che non stia valutando se stesso
if (fromUserId.toString() === toUserId.toString()) {
return res.status(400).json({
success: false,
message: 'Non puoi valutare te stesso'
});
}
// Verifica che non esista già un feedback
const existingFeedback = await Feedback.findOne({
rideId,
fromUserId,
toUserId
});
if (existingFeedback) {
return res.status(400).json({
success: false,
message: 'Hai già lasciato un feedback per questo utente in questo viaggio'
});
}
// Crea il feedback
const feedbackData = {
idapp,
rideId,
fromUserId,
toUserId,
role,
rating
};
if (rideRequestId) feedbackData.rideRequestId = rideRequestId;
if (categories) feedbackData.categories = categories;
if (comment) feedbackData.comment = comment;
if (pros) feedbackData.pros = pros;
if (cons) feedbackData.cons = cons;
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();
// Aggiorna la media rating dell'utente destinatario
await updateUserRating(idapp, toUserId);
// Aggiorna flag nella richiesta se presente
if (rideRequestId) {
await RideRequest.findByIdAndUpdate(rideRequestId, {
feedbackGiven: true
});
}
await feedback.populate('fromUserId', 'username name surname profile.img');
await feedback.populate('toUserId', 'username name surname');
res.status(201).json({
success: true,
message: 'Feedback inviato con successo!',
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
});
}
};
/**
* @desc Ottieni i feedback ricevuti da un utente
* @route GET /api/trasporti/feedback/user/:userId
* @access Public
*/
const getUserFeedback = async (req, res) => {
try {
const { userId } = req.params;
const { idapp, role, page = 1, limit = 10 } = req.query;
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è obbligatorio'
});
}
const query = {
idapp,
toUserId: userId,
isPublic: true
};
if (role) {
query.role = role;
}
const skip = (parseInt(page) - 1) * parseInt(limit);
const [feedbacks, total, stats] = await Promise.all([
Feedback.find(query)
.populate('fromUserId', 'username name surname profile.img')
.populate('rideId', 'departure destination dateTime')
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit)),
Feedback.countDocuments(query),
getStatsForUser(idapp, userId)
]);
res.json({
success: true,
data: feedbacks,
stats,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
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
});
}
};
/**
* @desc Ottieni statistiche feedback per un utente
* @route GET /api/trasporti/feedback/user/:userId/stats
* @access Public
*/
const getUserFeedbackStats = async (req, res) => {
try {
const { userId } = req.params;
const { idapp } = req.query;
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è obbligatorio'
});
}
const [stats, distribution] = await Promise.all([
getStatsForUser(idapp, userId),
getRatingDistribution(idapp, userId)
]);
res.json({
success: true,
data: {
...stats,
distribution
}
});
} catch (error) {
console.error('Errore recupero stats:', error);
res.status(500).json({
success: false,
message: 'Errore nel recupero delle statistiche',
error: error.message
});
}
};
/**
* @desc Ottieni i feedback per un viaggio
* @route GET /api/trasporti/feedback/ride/:rideId
* @access Public (con info limitate) / Private (info complete)
*/
const getRideFeedback = async (req, res) => {
try {
const { rideId } = req.params;
const { idapp } = req.query;
const userId = req.user?._id;
if (!idapp) {
return res.status(400).json({
success: false,
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'
});
}
// Query base per feedback pubblici
const query = {
idapp,
rideId,
isPublic: true
};
const feedbacks = await Feedback.find(query)
.populate('fromUserId', 'username name surname profile.img')
.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()
);
if (wasDriver || wasPassenger) {
myFeedbacks = feedbacks.filter(
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' }));
} else {
// Il passeggero deve dare feedback al conducente
const hasGivenToDriver = myFeedbacks.some(
f => f.toUserId._id.toString() === ride.userId.toString()
);
if (!hasGivenToDriver) {
pendingFeedbacks.push({ userId: ride.userId, role: 'driver' });
}
}
}
}
res.json({
success: true,
data: {
feedbacks,
pendingFeedbacks,
myFeedbacks
}
});
} catch (error) {
console.error('Errore recupero feedbacks viaggio:', error);
res.status(500).json({
success: false,
message: 'Errore',
error: error.message
});
}
};
/**
* @desc Verifica se l'utente può lasciare un feedback
* @route GET /api/trasporti/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.query;
const fromUserId = req.user._id;
if (!idapp) {
return res.status(400).json({
success: false,
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'
});
}
// 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()
);
if (!wasDriver && !wasPassenger) {
return res.json({
success: true,
data: {
canLeave: false,
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'
}
});
}
// 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()
);
if (!toUserWasDriver && !toUserWasPassenger) {
return res.json({
success: true,
data: {
canLeave: false,
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
}
});
}
// Verifica che non esista già un feedback
const existingFeedback = await Feedback.findOne({
rideId,
fromUserId,
toUserId
});
if (existingFeedback) {
return res.json({
success: true,
data: {
canLeave: false,
reason: 'Hai già lasciato un feedback per questo utente in questo viaggio',
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');
res.json({
success: true,
data: {
canLeave: true,
toUser: {
_id: toUser._id,
username: toUser.username,
name: toUser.name,
surname: toUser.surname,
img: toUser.profile?.img
},
toUserRole,
ride: {
_id: ride._id,
departure: ride.departure,
destination: ride.destination,
dateTime: ride.dateTime
}
}
});
} catch (error) {
console.error('Errore verifica canLeaveFeedback:', error);
res.status(500).json({
success: false,
message: 'Errore nella verifica',
error: error.message
});
}
};
/**
* @desc Rispondi a un feedback ricevuto
* @route POST /api/trasporti/feedback/:id/response
* @access Private
*/
const respondToFeedback = async (req, res) => {
try {
const { id } = req.params;
const { text } = req.body;
const userId = req.user._id;
if (!text) {
return res.status(400).json({
success: false,
message: 'Il testo della risposta è obbligatorio'
});
}
const feedback = await Feedback.findById(id);
if (!feedback) {
return res.status(404).json({
success: false,
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'
});
}
// Verifica che non abbia già risposto
if (feedback.response && feedback.response.text) {
return res.status(400).json({
success: false,
message: 'Hai già risposto a questo feedback'
});
}
// Aggiungi la risposta
feedback.response = {
text,
createdAt: new Date()
};
await feedback.save();
res.json({
success: true,
message: 'Risposta aggiunta',
data: feedback
});
} catch (error) {
console.error('Errore risposta feedback:', error);
res.status(500).json({
success: false,
message: 'Errore',
error: error.message
});
}
};
/**
* @desc Segna un feedback come utile
* @route POST /api/trasporti/feedback/:id/helpful
* @access Private
*/
const markAsHelpful = async (req, res) => {
try {
const { id } = req.params;
const userId = req.user._id;
const feedback = await Feedback.findById(id);
if (!feedback) {
return res.status(404).json({
success: false,
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
);
if (alreadyMarked) {
// Rimuovi il voto
feedback.helpful.users = feedback.helpful.users.filter(
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;
}
await feedback.save();
res.json({
success: true,
message: alreadyMarked ? 'Voto rimosso' : 'Feedback segnato come utile',
data: {
helpfulCount: feedback.helpful.count,
isHelpful: !alreadyMarked
}
});
} catch (error) {
console.error('Errore mark helpful:', error);
res.status(500).json({
success: false,
message: 'Errore',
error: error.message
});
}
};
/**
* @desc Segnala un feedback inappropriato
* @route POST /api/trasporti/feedback/:id/report
* @access Private
*/
const reportFeedback = async (req, res) => {
try {
const { id } = req.params;
const { reason } = req.body;
const userId = req.user._id;
if (!reason) {
return res.status(400).json({
success: false,
message: 'La motivazione è obbligatoria'
});
}
const feedback = await Feedback.findById(id);
if (!feedback) {
return res.status(404).json({
success: false,
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()
);
if (alreadyReported) {
return res.status(400).json({
success: false,
message: 'Hai già segnalato questo feedback'
});
}
// Aggiungi la segnalazione
feedback.reports.push({
userId,
reason,
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';
}
await feedback.save();
res.json({
success: true,
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
});
}
};
/**
* @desc Ottieni i miei feedback dati
* @route GET /api/trasporti/feedback/my/given
* @access Private
*/
const getMyGivenFeedback = async (req, res) => {
try {
const userId = req.user._id;
const { idapp, page = 1, limit = 20 } = req.query;
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è obbligatorio'
});
}
const skip = (parseInt(page) - 1) * parseInt(limit);
const [feedbacks, total] = await Promise.all([
Feedback.find({ idapp, fromUserId: userId })
.populate('toUserId', 'username name surname profile.img')
.populate('rideId', 'departure destination dateTime')
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit)),
Feedback.countDocuments({ idapp, fromUserId: userId })
]);
res.json({
success: true,
data: feedbacks,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
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
});
}
};
/**
* @desc Ottieni i miei feedback ricevuti
* @route GET /api/trasporti/feedback/my/received
* @access Private
*/
const getMyReceivedFeedback = async (req, res) => {
try {
const userId = req.user._id;
const { idapp, page = 1, limit = 20 } = req.query;
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è obbligatorio'
});
}
const skip = (parseInt(page) - 1) * parseInt(limit);
const [feedbacks, total, stats] = await Promise.all([
Feedback.find({ idapp, toUserId: userId })
.populate('fromUserId', 'username name surname profile.img')
.populate('rideId', 'departure destination dateTime')
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit)),
Feedback.countDocuments({ idapp, toUserId: userId }),
getStatsForUser(idapp, userId)
]);
res.json({
success: true,
data: feedbacks,
stats,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
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
});
}
};
// ============================================================
// 🔧 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)
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)
getFeedbacksForUser: getUserFeedback,
getFeedbacksForRide: getRideFeedback,
getMyGivenFeedbacks: getMyGivenFeedback,
getMyReceivedFeedbacks: getMyReceivedFeedback,
// Helper functions (esportate per uso in altri moduli)
getStatsForUser,
getRatingDistribution,
updateUserRating
};

View File

@@ -0,0 +1,522 @@
/**
* Controller per Geocoding usando servizi Open Source
* - Nominatim (OpenStreetMap) per geocoding/reverse
* - OSRM per routing
* - Photon per autocomplete
*/
const https = require('https');
const http = require('http');
// Configurazione servizi
const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org';
const PHOTON_BASE = 'https://photon.komoot.io';
const OSRM_BASE = 'https://router.project-osrm.org';
// User-Agent richiesto da Nominatim
const USER_AGENT = 'FreePlanetApp/1.0';
/**
* Helper per fare richieste HTTP/HTTPS
*/
const makeRequest = (url) => {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
const req = client.get(url, {
headers: {
'User-Agent': USER_AGENT,
'Accept': 'application/json'
}
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error('Errore parsing risposta'));
}
});
});
req.on('error', reject);
req.setTimeout(10000, () => {
req.destroy();
reject(new Error('Timeout richiesta'));
});
});
};
/**
* @desc Autocomplete città (Photon API)
* @route GET /api/trasporti/geo/autocomplete
*/
const autocomplete = async (req, res) => {
try {
const { q, limit = 5, lang = 'it' } = req.query;
if (!q || q.length < 2) {
return res.status(400).json({
success: false,
message: 'Query deve essere almeno 2 caratteri'
});
}
// Photon API - gratuito e veloce
const url = `${PHOTON_BASE}/api/?q=${encodeURIComponent(q)}&limit=${limit}&lang=${lang}&osm_tag=place:city&osm_tag=place:town&osm_tag=place:village`;
const data = await makeRequest(url);
// Formatta risultati
const results = data.features.map(feature => ({
city: feature.properties.name,
province: feature.properties.county || feature.properties.state,
region: feature.properties.state,
country: feature.properties.country,
postalCode: feature.properties.postcode,
coordinates: {
lat: feature.geometry.coordinates[1],
lng: feature.geometry.coordinates[0]
},
displayName: [
feature.properties.name,
feature.properties.county,
feature.properties.state,
feature.properties.country
].filter(Boolean).join(', '),
type: feature.properties.osm_value || 'place'
}));
res.status(200).json({
success: true,
data: results
});
} catch (error) {
console.error('Errore autocomplete:', error);
res.status(500).json({
success: false,
message: 'Errore durante la ricerca',
error: error.message
});
}
};
/**
* @desc Geocoding - indirizzo a coordinate (Nominatim)
* @route GET /api/trasporti/geo/geocode
*/
const geocode = async (req, res) => {
try {
const { address, city, country = 'Italy' } = req.query;
const searchQuery = [address, city, country].filter(Boolean).join(', ');
if (!searchQuery) {
return res.status(400).json({
success: false,
message: 'Fornisci un indirizzo o città da cercare'
});
}
const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=5&addressdetails=1`;
const data = await makeRequest(url);
if (!data || data.length === 0) {
return res.status(404).json({
success: false,
message: 'Nessun risultato trovato'
});
}
const results = data.map(item => ({
displayName: item.display_name,
city: item.address.city || item.address.town || item.address.village || item.address.municipality,
address: item.address.road ? `${item.address.road}${item.address.house_number ? ' ' + item.address.house_number : ''}` : null,
province: item.address.county || item.address.province,
region: item.address.state,
country: item.address.country,
postalCode: item.address.postcode,
coordinates: {
lat: parseFloat(item.lat),
lng: parseFloat(item.lon)
},
type: item.type,
importance: item.importance
}));
res.status(200).json({
success: true,
data: results
});
} catch (error) {
console.error('Errore geocoding:', error);
res.status(500).json({
success: false,
message: 'Errore durante il geocoding',
error: error.message
});
}
};
/**
* @desc Reverse geocoding - coordinate a indirizzo (Nominatim)
* @route GET /api/trasporti/geo/reverse
*/
const reverseGeocode = async (req, res) => {
try {
const { lat, lng } = req.query;
if (!lat || !lng) {
return res.status(400).json({
success: false,
message: 'Coordinate lat e lng richieste'
});
}
const url = `${NOMINATIM_BASE}/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`;
const data = await makeRequest(url);
if (!data || data.error) {
return res.status(404).json({
success: false,
message: 'Nessun risultato trovato'
});
}
const result = {
displayName: data.display_name,
city: data.address.city || data.address.town || data.address.village || data.address.municipality,
address: data.address.road ? `${data.address.road}${data.address.house_number ? ' ' + data.address.house_number : ''}` : null,
province: data.address.county || data.address.province,
region: data.address.state,
country: data.address.country,
postalCode: data.address.postcode,
coordinates: {
lat: parseFloat(lat),
lng: parseFloat(lng)
}
};
res.status(200).json({
success: true,
data: result
});
} catch (error) {
console.error('Errore reverse geocoding:', error);
res.status(500).json({
success: false,
message: 'Errore durante il reverse geocoding',
error: error.message
});
}
};
/**
* @desc Calcola percorso tra due punti (OSRM)
* @route GET /api/trasporti/geo/route
*/
const getRoute = async (req, res) => {
try {
const {
startLat, startLng,
endLat, endLng,
waypoints // formato: "lat1,lng1;lat2,lng2;..."
} = req.query;
if (!startLat || !startLng || !endLat || !endLng) {
return res.status(400).json({
success: false,
message: 'Coordinate di partenza e arrivo richieste'
});
}
// Costruisci stringa coordinate
let coordinates = `${startLng},${startLat}`;
if (waypoints) {
const waypointsList = waypoints.split(';');
waypointsList.forEach(wp => {
const [lat, lng] = wp.split(',');
coordinates += `;${lng},${lat}`;
});
}
coordinates += `;${endLng},${endLat}`;
const url = `${OSRM_BASE}/route/v1/driving/${coordinates}?overview=full&geometries=polyline&steps=true`;
const data = await makeRequest(url);
if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) {
return res.status(404).json({
success: false,
message: 'Impossibile calcolare il percorso'
});
}
const route = data.routes[0];
// Estrai città attraversate (dalle istruzioni)
const citiesAlongRoute = [];
if (route.legs) {
route.legs.forEach(leg => {
if (leg.steps) {
leg.steps.forEach(step => {
if (step.name && step.name.length > 0) {
// Qui potresti fare reverse geocoding per ottenere città
// Per ora usiamo i nomi delle strade principali
}
});
}
});
}
const result = {
distance: Math.round(route.distance / 1000 * 10) / 10, // km
duration: Math.round(route.duration / 60), // minuti
polyline: route.geometry, // Polyline encoded
legs: route.legs.map(leg => ({
distance: Math.round(leg.distance / 1000 * 10) / 10,
duration: Math.round(leg.duration / 60),
summary: leg.summary,
steps: leg.steps ? leg.steps.slice(0, 10).map(s => ({ // Limita step
instruction: s.maneuver ? s.maneuver.instruction : '',
name: s.name,
distance: Math.round(s.distance),
duration: Math.round(s.duration / 60)
})) : []
}))
};
res.status(200).json({
success: true,
data: result
});
} catch (error) {
console.error('Errore calcolo percorso:', error);
res.status(500).json({
success: false,
message: 'Errore durante il calcolo del percorso',
error: error.message
});
}
};
/**
* @desc Suggerisci città intermedie su un percorso
* @route GET /api/trasporti/geo/suggest-waypoints
*/
const suggestWaypoints = async (req, res) => {
try {
const { startLat, startLng, endLat, endLng } = req.query;
if (!startLat || !startLng || !endLat || !endLng) {
return res.status(400).json({
success: false,
message: 'Coordinate di partenza e arrivo richieste'
});
}
// Prima ottieni il percorso
const routeUrl = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=full&geometries=geojson`;
const routeData = await makeRequest(routeUrl);
if (!routeData || routeData.code !== 'Ok') {
return res.status(404).json({
success: false,
message: 'Impossibile calcolare il percorso'
});
}
// Prendi punti lungo il percorso (ogni ~50km circa)
const coordinates = routeData.routes[0].geometry.coordinates;
const totalPoints = coordinates.length;
const step = Math.max(1, Math.floor(totalPoints / 6)); // ~5 punti intermedi
const sampledPoints = [];
for (let i = step; i < totalPoints - step; i += step) {
sampledPoints.push(coordinates[i]);
}
// Fai reverse geocoding per ogni punto
const cities = [];
const seenCities = new Set();
for (const point of sampledPoints.slice(0, 5)) { // Limita a 5 richieste
try {
const reverseUrl = `${NOMINATIM_BASE}/reverse?format=json&lat=${point[1]}&lon=${point[0]}&addressdetails=1&zoom=10`;
const data = await makeRequest(reverseUrl);
if (data && data.address) {
const cityName = data.address.city || data.address.town || data.address.village;
if (cityName && !seenCities.has(cityName.toLowerCase())) {
seenCities.add(cityName.toLowerCase());
cities.push({
city: cityName,
province: data.address.county || data.address.province,
region: data.address.state,
coordinates: {
lat: point[1],
lng: point[0]
}
});
}
}
// Rate limiting - aspetta 1 secondo tra le richieste (requisito Nominatim)
await new Promise(resolve => setTimeout(resolve, 1100));
} catch (e) {
console.log('Errore reverse per punto:', e.message);
}
}
res.status(200).json({
success: true,
data: cities
});
} catch (error) {
console.error('Errore suggerimento waypoints:', error);
res.status(500).json({
success: false,
message: 'Errore durante il suggerimento delle tappe',
error: error.message
});
}
};
/**
* @desc Cerca città italiane (ottimizzato per Italia)
* @route GET /api/trasporti/geo/cities/it
*/
const searchItalianCities = async (req, res) => {
try {
const { q, limit = 10 } = req.query;
if (!q || q.length < 2) {
return res.status(400).json({
success: false,
message: 'Query deve essere almeno 2 caratteri'
});
}
// Usa Nominatim con filtro Italia
const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(q)}&countrycodes=it&limit=${limit}&addressdetails=1&featuretype=city`;
const data = await makeRequest(url);
const results = data
.filter(item =>
item.address &&
(item.address.city || item.address.town || item.address.village)
)
.map(item => ({
city: item.address.city || item.address.town || item.address.village,
province: item.address.county || item.address.province,
region: item.address.state,
postalCode: item.address.postcode,
coordinates: {
lat: parseFloat(item.lat),
lng: parseFloat(item.lon)
},
displayName: `${item.address.city || item.address.town || item.address.village}, ${item.address.county || item.address.state}`
}));
// Rimuovi duplicati
const unique = results.filter((v, i, a) =>
a.findIndex(t => t.city.toLowerCase() === v.city.toLowerCase()) === i
);
res.status(200).json({
success: true,
data: unique
});
} catch (error) {
console.error('Errore ricerca città italiane:', error);
res.status(500).json({
success: false,
message: 'Errore durante la ricerca',
error: error.message
});
}
};
/**
* @desc Calcola distanza e durata tra due punti
* @route GET /api/trasporti/geo/distance
*/
const getDistance = async (req, res) => {
try {
const { startLat, startLng, endLat, endLng } = req.query;
if (!startLat || !startLng || !endLat || !endLng) {
return res.status(400).json({
success: false,
message: 'Tutte le coordinate sono richieste'
});
}
const url = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=false`;
const data = await makeRequest(url);
if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) {
return res.status(404).json({
success: false,
message: 'Impossibile calcolare la distanza'
});
}
const route = data.routes[0];
res.status(200).json({
success: true,
data: {
distance: Math.round(route.distance / 1000 * 10) / 10, // km
duration: Math.round(route.duration / 60), // minuti
durationFormatted: formatDuration(route.duration)
}
});
} catch (error) {
console.error('Errore calcolo distanza:', error);
res.status(500).json({
success: false,
message: 'Errore durante il calcolo della distanza',
error: error.message
});
}
};
// Helper per formattare durata
const formatDuration = (seconds) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.round((seconds % 3600) / 60);
if (hours === 0) {
return `${minutes} min`;
} else if (minutes === 0) {
return `${hours} h`;
} else {
return `${hours} h ${minutes} min`;
}
};
module.exports = {
autocomplete,
geocode,
reverseGeocode,
getRoute,
suggestWaypoints,
searchItalianCities,
getDistance
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,616 @@
const RideRequest = require('../models/RideRequest');
const Ride = require('../models/Ride');
const Chat = require('../models/Chat');
const Message = require('../models/Message');
/**
* @desc Crea una richiesta di passaggio
* @route POST /api/trasporti/requests
* @access Private
*/
const createRequest = async (req, res) => {
try {
const {
idapp,
rideId,
message,
pickupPoint,
dropoffPoint,
useOriginalRoute,
seatsRequested,
hasLuggage,
luggageSize,
hasPackages,
packageDescription,
hasPets,
petType,
petSize,
specialNeeds,
contribution,
} = req.body;
const passengerId = req.user._id;
// Validazione
if (!idapp || !rideId) {
return res.status(400).json({
success: false,
message: 'idapp e rideId sono obbligatori',
});
}
// Verifica che il ride esista
const ride = await Ride.findById(rideId);
if (!ride) {
return res.status(404).json({
success: false,
message: 'Viaggio non trovato',
});
}
// Verifica che non sia il proprio viaggio
if (ride.userId.toString() === passengerId.toString()) {
return res.status(400).json({
success: false,
message: 'Non puoi richiedere un passaggio per il tuo stesso viaggio',
});
}
// Verifica che non ci sia già una richiesta pendente/accettata
const existingRequest = await RideRequest.findOne({
rideId,
passengerId,
status: { $in: ['pending', 'accepted'] },
});
if (existingRequest) {
return res.status(400).json({
success: false,
message: 'Hai già una richiesta attiva per questo viaggio',
});
}
// Verifica disponibilità posti
const seats = seatsRequested || 1;
if (ride.type === 'offer' && ride.passengers.available < seats) {
return res.status(400).json({
success: false,
message: `Posti insufficienti. Disponibili: ${ride.passengers.available}`,
});
}
// Crea la richiesta
const requestData = {
idapp,
rideId,
passengerId,
driverId: ride.userId,
seatsRequested: seats,
useOriginalRoute: useOriginalRoute !== false,
};
if (message) requestData.message = message;
if (pickupPoint) requestData.pickupPoint = pickupPoint;
if (dropoffPoint) requestData.dropoffPoint = dropoffPoint;
if (hasLuggage !== undefined) {
requestData.hasLuggage = hasLuggage;
requestData.luggageSize = luggageSize || 'small';
}
if (hasPackages !== undefined) {
requestData.hasPackages = hasPackages;
requestData.packageDescription = packageDescription;
}
if (hasPets !== undefined) {
requestData.hasPets = hasPets;
requestData.petType = petType;
requestData.petSize = petSize;
}
if (specialNeeds) requestData.specialNeeds = specialNeeds;
if (contribution) requestData.contribution = contribution;
const rideRequest = new RideRequest(requestData);
await rideRequest.save();
// Popola i dati per la risposta
await rideRequest.populate('passengerId', 'username name surname profile.img');
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);
// 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();
}
// TODO: Inviare notifica push al conducente
res.status(201).json({
success: true,
message: 'Richiesta di passaggio inviata!',
data: rideRequest,
chatId: chat._id,
});
} catch (error) {
console.error('Errore creazione richiesta:', error);
res.status(500).json({
success: false,
message: 'Errore nella creazione della richiesta',
error: error.message,
});
}
};
/**
* @desc Ottieni le richieste per un viaggio (per il conducente)
* @route GET /api/trasporti/requests/ride/:rideId
* @access Private
*/
const getRequestsForRide = async (req, res) => {
try {
const { rideId } = req.params;
const { idapp, status } = req.query;
const userId = req.user._id;
// Verifica che l'utente sia il proprietario del ride
const ride = await Ride.findById(rideId);
if (!ride) {
return res.status(404).json({
success: false,
message: 'Viaggio non trovato',
});
}
if (ride.userId.toString() !== userId.toString()) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato a vedere le richieste di questo viaggio',
});
}
const query = { idapp, rideId };
if (status) {
query.status = status;
}
const requests = await RideRequest.find(query)
.populate(
'passengerId',
'username name surname profile.img profile.Cell profile.driverProfile.averageRating profile.driverProfile.ridesCompletedAsPassenger'
)
.sort({ createdAt: -1 });
res.json({
success: true,
data: requests,
});
} catch (error) {
console.error('Errore recupero richieste:', error);
res.status(500).json({
success: false,
message: 'Errore nel recupero delle richieste',
error: error.message,
});
}
};
/**
* @desc Ottieni le mie richieste (come passeggero)
* @route GET /api/trasporti/requests/my
* @access Private
*/
const getMyRequests = async (req, res) => {
try {
const userId = req.user._id;
const { idapp, status, page = 1, limit = 20 } = req.query;
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è obbligatorio',
});
}
const query = { idapp, passengerId: userId };
if (status) {
query.status = status;
}
const skip = (parseInt(page) - 1) * parseInt(limit);
const [requests, total] = await Promise.all([
RideRequest.find(query)
.populate({
path: 'rideId',
populate: {
path: 'userId',
select: 'username name surname profile.img profile.Cell profile.driverProfile.averageRating',
},
})
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit)),
RideRequest.countDocuments(query),
]);
res.json({
success: true,
data: requests,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / parseInt(limit)),
},
});
} catch (error) {
console.error('Errore recupero mie richieste:', error);
res.status(500).json({
success: false,
message: 'Errore nel recupero delle tue richieste',
error: error.message,
});
}
};
/**
* @desc Ottieni richieste pendenti (per il conducente)
* @route GET /api/trasporti/requests/pending
* @access Private
*/
const getPendingRequests = async (req, res) => {
try {
const userId = req.user._id;
const { idapp } = req.query;
if (!idapp) {
return res.status(400).json({
success: false,
message: 'idapp è obbligatorio',
});
}
const requests = await RideRequest.find({
idapp,
driverId: userId,
status: 'pending',
})
.populate('passengerId', 'username name surname profile.img profile.driverProfile.averageRating')
.populate('rideId', 'departure destination dateTime passengers')
.sort({ createdAt: -1 });
res.json({
success: true,
data: requests,
total: requests.length,
});
} catch (error) {
console.error('Errore recupero richieste pendenti:', error);
res.status(500).json({
success: false,
message: 'Errore nel recupero delle richieste pendenti',
error: error.message,
});
}
};
/**
* @desc Accetta una richiesta di passaggio
* @route PUT /api/trasporti/requests/:id/accept
* @access Private (solo conducente)
*/
const acceptRequest = async (req, res) => {
try {
const { id } = req.params;
const { responseMessage, idapp } = req.body;
const userId = req.user._id;
const request = await RideRequest.findById(id).populate('rideId');
if (!request) {
return res.status(404).json({
success: false,
message: 'Richiesta non trovata',
});
}
// Verifica che sia il conducente
if (request.driverId.toString() !== userId.toString()) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato ad accettare questa richiesta',
});
}
// Verifica che sia ancora pendente
if (request.status !== 'pending') {
return res.status(400).json({
success: false,
message: `La richiesta è già stata ${request.status}`,
});
}
// Verifica disponibilità posti
const ride = request.rideId;
if (ride.passengers.available < request.seatsRequested) {
return res.status(400).json({
success: false,
message: 'Posti non più disponibili',
});
}
// Accetta la richiesta
request.status = 'accepted';
request.responseMessage = responseMessage || '';
request.respondedAt = new Date();
await request.save();
// Aggiorna il ride con il passeggero
ride.confirmedPassengers.push({
userId: request.passengerId,
seats: request.seatsRequested,
pickupPoint: request.pickupPoint || ride.departure,
dropoffPoint: request.dropoffPoint || ride.destination,
confirmedAt: new Date(),
});
await ride.updateAvailableSeats();
// 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();
// TODO: Inviare notifica push al passeggero
await request.populate('passengerId', 'username name surname profile.img');
await request.populate('rideId', 'departure destination dateTime');
res.json({
success: true,
message: 'Richiesta accettata!',
data: request,
});
} catch (error) {
console.error('Errore accettazione richiesta:', error);
res.status(500).json({
success: false,
message: "Errore nell'accettazione della richiesta",
error: error.message,
});
}
};
/**
* @desc Rifiuta una richiesta di passaggio
* @route PUT /api/trasporti/requests/:id/reject
* @access Private (solo conducente)
*/
const rejectRequest = async (req, res) => {
try {
const { id } = req.params;
const { responseMessage, idapp } = req.body;
const userId = req.user._id;
const request = await RideRequest.findById(id);
if (!request) {
return res.status(404).json({
success: false,
message: 'Richiesta non trovata',
});
}
// Verifica che sia il conducente
if (request.driverId.toString() !== userId.toString()) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato',
});
}
// Verifica che sia ancora pendente
if (request.status !== 'pending') {
return res.status(400).json({
success: false,
message: `La richiesta è già stata ${request.status}`,
});
}
// Rifiuta la richiesta
request.status = 'rejected';
request.responseMessage = responseMessage || '';
request.respondedAt = new Date();
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();
// TODO: Inviare notifica push al passeggero
res.json({
success: true,
message: 'Richiesta rifiutata',
data: request,
});
} catch (error) {
console.error('Errore rifiuto richiesta:', error);
res.status(500).json({
success: false,
message: 'Errore nel rifiuto della richiesta',
error: error.message,
});
}
};
/**
* @desc Cancella una richiesta (dal passeggero)
* @route PUT /api/trasporti/requests/:id/cancel
* @access Private
*/
const cancelRequest = async (req, res) => {
try {
const { id } = req.params;
const { reason } = req.body;
const userId = req.user._id;
const request = await RideRequest.findById(id);
if (!request) {
return res.status(404).json({
success: false,
message: 'Richiesta non trovata',
});
}
// Verifica che sia il passeggero o il conducente
const isPassenger = request.passengerId.toString() === userId.toString();
const isDriver = request.driverId.toString() === userId.toString();
if (!isPassenger && !isDriver) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato',
});
}
// Verifica che possa essere cancellata
if (!['pending', 'accepted'].includes(request.status)) {
return res.status(400).json({
success: false,
message: 'Questa richiesta non può essere cancellata',
});
}
// Se era accettata, rimuovi il passeggero dal ride
if (request.status === 'accepted') {
const ride = await Ride.findById(request.rideId);
if (ride) {
ride.confirmedPassengers = ride.confirmedPassengers.filter(
(p) => p.userId.toString() !== request.passengerId.toString()
);
await ride.updateAvailableSeats();
}
}
request.status = 'cancelled';
request.cancelledBy = isPassenger ? 'passenger' : 'driver';
request.cancellationReason = reason || '';
request.cancelledAt = new Date();
await request.save();
// TODO: Inviare notifica all'altra parte
res.json({
success: true,
message: 'Richiesta cancellata',
data: request,
});
} catch (error) {
console.error('Errore cancellazione richiesta:', error);
res.status(500).json({
success: false,
message: 'Errore nella cancellazione',
error: error.message,
});
}
};
/**
* @desc Ottieni una singola richiesta
* @route GET /api/trasporti/requests/:id
* @access Private
*/
const getRequestById = async (req, res) => {
try {
const { id } = req.params;
const userId = req.user._id;
const request = await RideRequest.findById(id)
.populate('passengerId', 'username name surname profile.img profile.Cell profile.driverProfile')
.populate('driverId', 'username name surname profile.img profile.Cell profile.driverProfile')
.populate({
path: 'rideId',
populate: {
path: 'userId',
select: 'username name surname profile.img',
},
})
.populate('contribution.contribTypeId', 'label icon color');
if (!request) {
return res.status(404).json({
success: false,
message: 'Richiesta non trovata',
});
}
// Verifica che l'utente sia coinvolto
const isPassenger = request.passengerId._id.toString() === userId.toString();
const isDriver = request.driverId._id.toString() === userId.toString();
if (!isPassenger && !isDriver) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato a vedere questa richiesta',
});
}
res.json({
success: true,
data: request,
});
} catch (error) {
console.error('Errore recupero richiesta:', error);
res.status(500).json({
success: false,
message: 'Errore nel recupero della richiesta',
error: error.message,
});
}
};
module.exports = {
createRequest,
getRequestsForRide,
getMyRequests,
getPendingRequests,
acceptRequest,
rejectRequest,
cancelRequest,
getRequestById,
getReceivedRequests: getPendingRequests,
getSentRequests: getMyRequests,
};