- Implementazione TRASPORTI ! Passo 1
This commit is contained in:
611
src/controllers/chatController.js
Normal file
611
src/controllers/chatController.js
Normal 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
|
||||
};
|
||||
926
src/controllers/feedbackController.js
Normal file
926
src/controllers/feedbackController.js
Normal 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
|
||||
};
|
||||
522
src/controllers/geocodingController.js
Normal file
522
src/controllers/geocodingController.js
Normal 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
|
||||
};
|
||||
1820
src/controllers/rideController.js
Normal file
1820
src/controllers/rideController.js
Normal file
File diff suppressed because it is too large
Load Diff
616
src/controllers/rideRequestController.js
Normal file
616
src/controllers/rideRequestController.js
Normal 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,
|
||||
};
|
||||
204
src/models/Chat.js
Normal file
204
src/models/Chat.js
Normal file
@@ -0,0 +1,204 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const LastMessageSchema = new Schema({
|
||||
text: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
senderId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
timestamp: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['text', 'ride_share', 'location', 'image', 'system'],
|
||||
default: 'text'
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
const ChatSchema = new Schema({
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
participants: [{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
}],
|
||||
rideId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Ride',
|
||||
index: true
|
||||
// Opzionale: chat collegata a un viaggio specifico
|
||||
},
|
||||
rideRequestId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'RideRequest'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['direct', 'ride', 'group'],
|
||||
default: 'direct'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
trim: true
|
||||
// Solo per chat di gruppo
|
||||
},
|
||||
lastMessage: {
|
||||
type: LastMessageSchema
|
||||
},
|
||||
unreadCount: {
|
||||
type: Map,
|
||||
of: Number,
|
||||
default: new Map()
|
||||
// { odIdUtente: numeroMessaggiNonLetti }
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
mutedBy: [{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
}],
|
||||
blockedBy: [{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
}],
|
||||
metadata: {
|
||||
type: Schema.Types.Mixed
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indici
|
||||
ChatSchema.index({ participants: 1 });
|
||||
ChatSchema.index({ idapp: 1, participants: 1 });
|
||||
ChatSchema.index({ idapp: 1, updatedAt: -1 });
|
||||
|
||||
// Virtual per contare messaggi non letti totali
|
||||
ChatSchema.virtual('totalUnread').get(function() {
|
||||
if (!this.unreadCount) return 0;
|
||||
let total = 0;
|
||||
this.unreadCount.forEach(count => {
|
||||
total += count;
|
||||
});
|
||||
return total;
|
||||
});
|
||||
|
||||
// Metodo per ottenere unread count per un utente specifico
|
||||
ChatSchema.methods.getUnreadForUser = function(userId) {
|
||||
if (!this.unreadCount) return 0;
|
||||
return this.unreadCount.get(userId.toString()) || 0;
|
||||
};
|
||||
|
||||
// Metodo per incrementare unread count
|
||||
ChatSchema.methods.incrementUnread = function(excludeUserId) {
|
||||
this.participants.forEach(participantId => {
|
||||
const id = participantId.toString();
|
||||
if (id !== excludeUserId.toString()) {
|
||||
const current = this.unreadCount.get(id) || 0;
|
||||
this.unreadCount.set(id, current + 1);
|
||||
}
|
||||
});
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per resettare unread count per un utente
|
||||
ChatSchema.methods.markAsRead = function(userId) {
|
||||
this.unreadCount.set(userId.toString(), 0);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per aggiornare ultimo messaggio
|
||||
ChatSchema.methods.updateLastMessage = function(message) {
|
||||
this.lastMessage = {
|
||||
text: message.text,
|
||||
senderId: message.senderId,
|
||||
timestamp: message.createdAt || new Date(),
|
||||
type: message.type || 'text'
|
||||
};
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per verificare se un utente è partecipante
|
||||
ChatSchema.methods.hasParticipant = function(userId) {
|
||||
return this.participants.some(
|
||||
p => p.toString() === userId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
// Metodo per verificare se la chat è bloccata per un utente
|
||||
ChatSchema.methods.isBlockedFor = function(userId) {
|
||||
return this.blockedBy.some(
|
||||
id => id.toString() === userId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
// Metodo statico per trovare o creare una chat diretta
|
||||
ChatSchema.statics.findOrCreateDirect = async function(idapp, userId1, userId2, rideId = null) {
|
||||
// Cerca chat esistente tra i due utenti
|
||||
let chat = await this.findOne({
|
||||
idapp,
|
||||
type: 'direct',
|
||||
participants: { $all: [userId1, userId2], $size: 2 }
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
chat = new this({
|
||||
idapp,
|
||||
type: 'direct',
|
||||
participants: [userId1, userId2],
|
||||
rideId,
|
||||
unreadCount: new Map()
|
||||
});
|
||||
await chat.save();
|
||||
} else if (rideId && !chat.rideId) {
|
||||
// Aggiorna con rideId se fornito
|
||||
chat.rideId = rideId;
|
||||
await chat.save();
|
||||
}
|
||||
|
||||
return chat;
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere tutte le chat di un utente
|
||||
ChatSchema.statics.getChatsForUser = function(idapp, userId) {
|
||||
return this.find({
|
||||
idapp,
|
||||
participants: userId,
|
||||
isActive: true,
|
||||
blockedBy: { $ne: userId }
|
||||
})
|
||||
.populate('participants', 'username name surname profile.avatar')
|
||||
.populate('rideId', 'departure destination dateTime')
|
||||
.sort({ updatedAt: -1 });
|
||||
};
|
||||
|
||||
// Metodo statico per creare chat di gruppo per un viaggio
|
||||
ChatSchema.statics.createRideGroupChat = async function(idapp, rideId, title, participantIds) {
|
||||
const chat = new this({
|
||||
idapp,
|
||||
type: 'group',
|
||||
rideId,
|
||||
title,
|
||||
participants: participantIds,
|
||||
unreadCount: new Map()
|
||||
});
|
||||
return chat.save();
|
||||
};
|
||||
|
||||
const Chat = mongoose.model('Chat', ChatSchema);
|
||||
|
||||
module.exports = Chat;
|
||||
357
src/models/Feedback.js
Normal file
357
src/models/Feedback.js
Normal file
@@ -0,0 +1,357 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const FeedbackCategoriesSchema = new Schema(
|
||||
{
|
||||
punctuality: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
cleanliness: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
communication: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
driving: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
// Solo per feedback a conducenti
|
||||
},
|
||||
respect: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
reliability: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const FeedbackSchema = new Schema(
|
||||
{
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
rideId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Ride',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
rideRequestId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'RideRequest',
|
||||
},
|
||||
fromUserId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
toUserId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: ['driver', 'passenger'],
|
||||
required: true,
|
||||
// Il ruolo dell'utente che RICEVE il feedback
|
||||
// 'driver' = sto valutando il conducente
|
||||
// 'passenger' = sto valutando il passeggero
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
categories: {
|
||||
type: FeedbackCategoriesSchema,
|
||||
},
|
||||
comment: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 1000,
|
||||
},
|
||||
pros: [
|
||||
{
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
],
|
||||
cons: [
|
||||
{
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
],
|
||||
tags: [
|
||||
{
|
||||
type: String,
|
||||
enum: [
|
||||
'puntuale',
|
||||
'gentile',
|
||||
'auto_pulita',
|
||||
'guida_sicura',
|
||||
'buona_conversazione',
|
||||
'silenzioso',
|
||||
'flessibile',
|
||||
'rispettoso',
|
||||
'affidabile',
|
||||
'consigliato',
|
||||
// Tag negativi
|
||||
'in_ritardo',
|
||||
'scortese',
|
||||
'guida_pericolosa',
|
||||
'auto_sporca',
|
||||
'non_rispettoso',
|
||||
],
|
||||
},
|
||||
],
|
||||
isPublic: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
// Feedback verificato (viaggio effettivamente completato)
|
||||
},
|
||||
response: {
|
||||
text: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500,
|
||||
},
|
||||
respondedAt: {
|
||||
type: Date,
|
||||
},
|
||||
},
|
||||
helpful: {
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
users: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
],
|
||||
},
|
||||
reported: {
|
||||
isReported: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
reason: String,
|
||||
reportedBy: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
reportedAt: Date,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true },
|
||||
}
|
||||
);
|
||||
|
||||
// Indici composti
|
||||
FeedbackSchema.index({ toUserId: 1, role: 1 });
|
||||
FeedbackSchema.index({ rideId: 1, fromUserId: 1 });
|
||||
FeedbackSchema.index({ idapp: 1, toUserId: 1 });
|
||||
|
||||
// Vincolo: un utente può lasciare un solo feedback per viaggio verso un altro utente
|
||||
FeedbackSchema.index({ rideId: 1, fromUserId: 1, toUserId: 1 }, { unique: true });
|
||||
|
||||
// Virtual per calcolare media categorie
|
||||
FeedbackSchema.virtual('categoryAverage').get(function () {
|
||||
if (!this.categories) return null;
|
||||
const cats = this.categories.toObject ? this.categories.toObject() : this.categories;
|
||||
const values = Object.values(cats).filter((v) => typeof v === 'number');
|
||||
if (values.length === 0) return null;
|
||||
return values.reduce((a, b) => a + b, 0) / values.length;
|
||||
});
|
||||
|
||||
// Metodo per aggiungere risposta
|
||||
FeedbackSchema.methods.addResponse = function (text) {
|
||||
this.response = {
|
||||
text,
|
||||
respondedAt: new Date(),
|
||||
};
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per segnare come utile
|
||||
FeedbackSchema.methods.markAsHelpful = function (userId) {
|
||||
if (!this.helpful.users.includes(userId)) {
|
||||
this.helpful.users.push(userId);
|
||||
this.helpful.count = this.helpful.users.length;
|
||||
return this.save();
|
||||
}
|
||||
return Promise.resolve(this);
|
||||
};
|
||||
|
||||
// Metodo per segnalare feedback
|
||||
FeedbackSchema.methods.report = function (userId, reason) {
|
||||
this.reported = {
|
||||
isReported: true,
|
||||
reason,
|
||||
reportedBy: userId,
|
||||
reportedAt: new Date(),
|
||||
};
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere feedback di un utente
|
||||
FeedbackSchema.statics.getForUser = function (idapp, userId, options = {}) {
|
||||
const query = {
|
||||
idapp,
|
||||
toUserId: userId,
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
if (options.role) {
|
||||
query.role = options.role;
|
||||
}
|
||||
|
||||
return this.find(query)
|
||||
.populate('fromUserId', 'username name surname profile.avatar')
|
||||
.populate('rideId', 'departure destination dateTime')
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(options.limit || 20);
|
||||
};
|
||||
|
||||
// Metodo statico per calcolare statistiche
|
||||
FeedbackSchema.statics.getStatsForUser = async function (idapp, userId) {
|
||||
const stats = await this.aggregate([
|
||||
{
|
||||
$match: {
|
||||
idapp,
|
||||
toUserId: new mongoose.Types.ObjectId(userId),
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$role',
|
||||
averageRating: { $avg: '$rating' },
|
||||
totalFeedbacks: { $sum: 1 },
|
||||
avgPunctuality: { $avg: '$categories.punctuality' },
|
||||
avgCleanliness: { $avg: '$categories.cleanliness' },
|
||||
avgCommunication: { $avg: '$categories.communication' },
|
||||
avgDriving: { $avg: '$categories.driving' },
|
||||
avgRespect: { $avg: '$categories.respect' },
|
||||
avgReliability: { $avg: '$categories.reliability' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Trasforma in oggetto più leggibile
|
||||
const result = {
|
||||
asDriver: null,
|
||||
asPassenger: null,
|
||||
overall: {
|
||||
averageRating: 0,
|
||||
totalFeedbacks: 0,
|
||||
},
|
||||
};
|
||||
|
||||
stats.forEach((stat) => {
|
||||
const data = {
|
||||
averageRating: Math.round(stat.averageRating * 10) / 10,
|
||||
totalFeedbacks: stat.totalFeedbacks,
|
||||
categories: {
|
||||
punctuality: stat.avgPunctuality ? Math.round(stat.avgPunctuality * 10) / 10 : null,
|
||||
cleanliness: stat.avgCleanliness ? Math.round(stat.avgCleanliness * 10) / 10 : null,
|
||||
communication: stat.avgCommunication ? Math.round(stat.avgCommunication * 10) / 10 : null,
|
||||
driving: stat.avgDriving ? Math.round(stat.avgDriving * 10) / 10 : null,
|
||||
respect: stat.avgRespect ? Math.round(stat.avgRespect * 10) / 10 : null,
|
||||
reliability: stat.avgReliability ? Math.round(stat.avgReliability * 10) / 10 : null,
|
||||
},
|
||||
};
|
||||
|
||||
if (stat._id === 'driver') {
|
||||
result.asDriver = data;
|
||||
} else if (stat._id === 'passenger') {
|
||||
result.asPassenger = data;
|
||||
}
|
||||
});
|
||||
|
||||
// Calcola overall
|
||||
const allStats = stats.reduce(
|
||||
(acc, s) => {
|
||||
acc.total += s.totalFeedbacks;
|
||||
acc.sum += s.averageRating * s.totalFeedbacks;
|
||||
return acc;
|
||||
},
|
||||
{ total: 0, sum: 0 }
|
||||
);
|
||||
|
||||
if (allStats.total > 0) {
|
||||
result.overall = {
|
||||
averageRating: Math.round((allStats.sum / allStats.total) * 10) / 10,
|
||||
totalFeedbacks: allStats.total,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Metodo statico per contare distribuzioni rating
|
||||
FeedbackSchema.statics.getRatingDistribution = async function (idapp, userId, role = null) {
|
||||
const match = {
|
||||
idapp,
|
||||
toUserId: new mongoose.Types.ObjectId(userId),
|
||||
};
|
||||
if (role) match.role = role;
|
||||
|
||||
return this.aggregate([
|
||||
{ $match: match },
|
||||
{
|
||||
$group: {
|
||||
_id: '$rating',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: -1 } },
|
||||
]);
|
||||
};
|
||||
|
||||
// Hook post-save per aggiornare rating utente
|
||||
FeedbackSchema.post('save', async function (doc) {
|
||||
try {
|
||||
const { User } = require('./User');
|
||||
|
||||
const stats = await mongoose.model('Feedback').getStatsForUser(doc.idapp, doc.toUserId);
|
||||
|
||||
await User.findByIdAndUpdate(doc.toUserId, {
|
||||
'profile.driverProfile.averageRating': stats.overall.averageRating,
|
||||
'profile.driverProfile.totalRatings': stats.overall.totalFeedbacks,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore aggiornamento rating utente:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const Feedback = mongoose.model('Feedback', FeedbackSchema);
|
||||
|
||||
module.exports = Feedback;
|
||||
238
src/models/Message.js
Normal file
238
src/models/Message.js
Normal file
@@ -0,0 +1,238 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const MessageSchema = new Schema({
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
chatId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Chat',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
senderId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 2000
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['text', 'ride_share', 'location', 'image', 'voice', 'system', 'ride_request', 'ride_accepted', 'ride_rejected'],
|
||||
default: 'text'
|
||||
},
|
||||
metadata: {
|
||||
// Per messaggi speciali (condivisione viaggio, posizione, ecc.)
|
||||
rideId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Ride'
|
||||
},
|
||||
rideRequestId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'RideRequest'
|
||||
},
|
||||
location: {
|
||||
lat: Number,
|
||||
lng: Number,
|
||||
address: String
|
||||
},
|
||||
imageUrl: String,
|
||||
voiceUrl: String,
|
||||
voiceDuration: Number,
|
||||
systemAction: String
|
||||
},
|
||||
readBy: [{
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
readAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}],
|
||||
replyTo: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Message'
|
||||
},
|
||||
isEdited: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
editedAt: {
|
||||
type: Date
|
||||
},
|
||||
isDeleted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
deletedAt: {
|
||||
type: Date
|
||||
},
|
||||
reactions: [{
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
emoji: String,
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indici per query efficienti
|
||||
MessageSchema.index({ chatId: 1, createdAt: -1 });
|
||||
MessageSchema.index({ senderId: 1, createdAt: -1 });
|
||||
MessageSchema.index({ idapp: 1, chatId: 1 });
|
||||
|
||||
// Virtual per verificare se il messaggio è stato letto da tutti
|
||||
MessageSchema.virtual('isReadByAll').get(function() {
|
||||
// Logica da implementare confrontando con partecipanti chat
|
||||
return false;
|
||||
});
|
||||
|
||||
// Metodo per marcare come letto da un utente
|
||||
MessageSchema.methods.markAsReadBy = function(userId) {
|
||||
const alreadyRead = this.readBy.some(
|
||||
r => r.userId.toString() === userId.toString()
|
||||
);
|
||||
|
||||
if (!alreadyRead) {
|
||||
this.readBy.push({
|
||||
userId,
|
||||
readAt: new Date()
|
||||
});
|
||||
return this.save();
|
||||
}
|
||||
return Promise.resolve(this);
|
||||
};
|
||||
|
||||
// Metodo per verificare se è stato letto da un utente
|
||||
MessageSchema.methods.isReadBy = function(userId) {
|
||||
return this.readBy.some(
|
||||
r => r.userId.toString() === userId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
// Metodo per aggiungere reazione
|
||||
MessageSchema.methods.addReaction = function(userId, emoji) {
|
||||
// Rimuovi eventuale reazione precedente dello stesso utente
|
||||
this.reactions = this.reactions.filter(
|
||||
r => r.userId.toString() !== userId.toString()
|
||||
);
|
||||
|
||||
this.reactions.push({
|
||||
userId,
|
||||
emoji,
|
||||
createdAt: new Date()
|
||||
});
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per rimuovere reazione
|
||||
MessageSchema.methods.removeReaction = function(userId) {
|
||||
this.reactions = this.reactions.filter(
|
||||
r => r.userId.toString() !== userId.toString()
|
||||
);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per soft delete
|
||||
MessageSchema.methods.softDelete = function() {
|
||||
this.isDeleted = true;
|
||||
this.deletedAt = new Date();
|
||||
this.text = '[Messaggio eliminato]';
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per modificare testo
|
||||
MessageSchema.methods.editText = function(newText) {
|
||||
this.text = newText;
|
||||
this.isEdited = true;
|
||||
this.editedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere messaggi di una chat con paginazione
|
||||
MessageSchema.statics.getByChat = function(idapp, chatId, options = {}) {
|
||||
const { limit = 50, before = null, after = null } = options;
|
||||
|
||||
const query = {
|
||||
idapp,
|
||||
chatId,
|
||||
isDeleted: false
|
||||
};
|
||||
|
||||
if (before) {
|
||||
query.createdAt = { $lt: new Date(before) };
|
||||
}
|
||||
if (after) {
|
||||
query.createdAt = { $gt: new Date(after) };
|
||||
}
|
||||
|
||||
return this.find(query)
|
||||
.populate('senderId', 'username name surname profile.avatar')
|
||||
.populate('replyTo', 'text senderId')
|
||||
.populate('metadata.rideId', 'departure destination dateTime')
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(limit);
|
||||
};
|
||||
|
||||
// Metodo statico per creare messaggio di sistema
|
||||
MessageSchema.statics.createSystemMessage = async function(idapp, chatId, text, action = null) {
|
||||
const message = new this({
|
||||
idapp,
|
||||
chatId,
|
||||
senderId: null, // Sistema
|
||||
text,
|
||||
type: 'system',
|
||||
metadata: {
|
||||
systemAction: action
|
||||
}
|
||||
});
|
||||
return message.save();
|
||||
};
|
||||
|
||||
// Metodo statico per contare messaggi non letti
|
||||
MessageSchema.statics.countUnreadForUser = async function(idapp, chatId, userId) {
|
||||
return this.countDocuments({
|
||||
idapp,
|
||||
chatId,
|
||||
isDeleted: false,
|
||||
senderId: { $ne: userId },
|
||||
'readBy.userId': { $ne: userId }
|
||||
});
|
||||
};
|
||||
|
||||
// Hook post-save per aggiornare la chat
|
||||
MessageSchema.post('save', async function(doc) {
|
||||
try {
|
||||
const Chat = mongoose.model('Chat');
|
||||
const chat = await Chat.findById(doc.chatId);
|
||||
|
||||
if (chat) {
|
||||
await chat.updateLastMessage(doc);
|
||||
await chat.incrementUnread(doc.senderId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Errore aggiornamento chat dopo messaggio:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const Message = mongoose.model('Message', MessageSchema);
|
||||
|
||||
module.exports = Message;
|
||||
499
src/models/Ride.js
Normal file
499
src/models/Ride.js
Normal file
@@ -0,0 +1,499 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
// Schema per le coordinate geografiche
|
||||
const CoordinatesSchema = new Schema({
|
||||
lat: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Schema per una località (partenza, destinazione, waypoint)
|
||||
const LocationSchema = new Schema({
|
||||
city: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
province: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
region: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
country: {
|
||||
type: String,
|
||||
default: 'Italia',
|
||||
trim: true
|
||||
},
|
||||
postalCode: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
coordinates: {
|
||||
type: CoordinatesSchema,
|
||||
required: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Schema per i waypoint (tappe intermedie)
|
||||
const WaypointSchema = new Schema({
|
||||
location: {
|
||||
type: LocationSchema,
|
||||
required: true
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
estimatedArrival: {
|
||||
type: Date
|
||||
},
|
||||
stopDuration: {
|
||||
type: Number, // minuti di sosta
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
// Schema per la ricorrenza del viaggio
|
||||
const RecurrenceSchema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['once', 'weekly', 'custom_days', 'custom_dates'],
|
||||
default: 'once'
|
||||
},
|
||||
daysOfWeek: [{
|
||||
type: Number,
|
||||
min: 0,
|
||||
max: 6
|
||||
// 0 = Domenica, 1 = Lunedì, ..., 6 = Sabato
|
||||
}],
|
||||
customDates: [{
|
||||
type: Date
|
||||
}],
|
||||
startDate: {
|
||||
type: Date
|
||||
},
|
||||
endDate: {
|
||||
type: Date
|
||||
},
|
||||
excludedDates: [{
|
||||
type: Date
|
||||
}]
|
||||
}, { _id: false });
|
||||
|
||||
// Schema per i passeggeri
|
||||
const PassengersSchema = new Schema({
|
||||
available: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 1
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Schema per il veicolo
|
||||
const VehicleSchema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['auto', 'moto', 'furgone', 'minibus', 'altro'],
|
||||
default: 'auto'
|
||||
},
|
||||
brand: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
colorHex: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
year: {
|
||||
type: Number
|
||||
},
|
||||
licensePlate: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
seats: {
|
||||
type: Number,
|
||||
min: 1
|
||||
},
|
||||
features: [{
|
||||
type: String,
|
||||
enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi']
|
||||
}]
|
||||
}, { _id: false });
|
||||
|
||||
// Schema per le preferenze di viaggio
|
||||
const RidePreferencesSchema = new Schema({
|
||||
smoking: {
|
||||
type: String,
|
||||
enum: ['yes', 'no', 'outside_only'],
|
||||
default: 'no'
|
||||
},
|
||||
pets: {
|
||||
type: String,
|
||||
enum: ['no', 'small', 'medium', 'large', 'all'],
|
||||
default: 'no'
|
||||
},
|
||||
luggage: {
|
||||
type: String,
|
||||
enum: ['none', 'small', 'medium', 'large'],
|
||||
default: 'medium'
|
||||
},
|
||||
packages: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxPackageSize: {
|
||||
type: String,
|
||||
enum: ['small', 'medium', 'large', 'xlarge'],
|
||||
default: 'medium'
|
||||
},
|
||||
music: {
|
||||
type: String,
|
||||
enum: ['no_music', 'quiet', 'moderate', 'loud', 'passenger_choice'],
|
||||
default: 'moderate'
|
||||
},
|
||||
conversation: {
|
||||
type: String,
|
||||
enum: ['quiet', 'moderate', 'chatty'],
|
||||
default: 'moderate'
|
||||
},
|
||||
foodAllowed: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
childrenFriendly: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
wheelchairAccessible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
otherPreferences: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Schema per il contributo/pagamento
|
||||
const ContributionItemSchema = new Schema({
|
||||
contribTypeId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Contribtype',
|
||||
required: true
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
min: 0
|
||||
},
|
||||
pricePerKm: {
|
||||
type: Number,
|
||||
min: 0
|
||||
},
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true
|
||||
}
|
||||
});
|
||||
|
||||
const ContributionSchema = new Schema({
|
||||
contribTypes: [ContributionItemSchema],
|
||||
negotiable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
freeForStudents: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
freeForElders: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Schema principale del Ride
|
||||
const RideSchema = new Schema({
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['offer', 'request'],
|
||||
required: true,
|
||||
index: true
|
||||
// offer = 🟢 Offerta passaggio (sono conducente)
|
||||
// request = 🔴 Richiesta passaggio (cerco passaggio)
|
||||
},
|
||||
departure: {
|
||||
type: LocationSchema,
|
||||
required: true
|
||||
},
|
||||
destination: {
|
||||
type: LocationSchema,
|
||||
required: true
|
||||
},
|
||||
waypoints: [WaypointSchema],
|
||||
dateTime: {
|
||||
type: Date,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
flexibleTime: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
flexibleMinutes: {
|
||||
type: Number,
|
||||
default: 30,
|
||||
min: 0,
|
||||
max: 180
|
||||
},
|
||||
recurrence: {
|
||||
type: RecurrenceSchema,
|
||||
default: () => ({ type: 'once' })
|
||||
},
|
||||
passengers: {
|
||||
type: PassengersSchema,
|
||||
required: function() {
|
||||
return this.type === 'offer';
|
||||
}
|
||||
},
|
||||
seatsNeeded: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
default: 1,
|
||||
// Solo per type = 'request'
|
||||
},
|
||||
vehicle: {
|
||||
type: VehicleSchema,
|
||||
required: function() {
|
||||
return this.type === 'offer';
|
||||
}
|
||||
},
|
||||
preferences: {
|
||||
type: RidePreferencesSchema,
|
||||
default: () => ({})
|
||||
},
|
||||
contribution: {
|
||||
type: ContributionSchema,
|
||||
default: () => ({ contribTypes: [] })
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['draft', 'active', 'full', 'in_progress', 'completed', 'cancelled', 'expired'],
|
||||
default: 'active',
|
||||
index: true
|
||||
},
|
||||
estimatedDistance: {
|
||||
type: Number, // in km
|
||||
min: 0
|
||||
},
|
||||
estimatedDuration: {
|
||||
type: Number, // in minuti
|
||||
min: 0
|
||||
},
|
||||
routePolyline: {
|
||||
type: String // Polyline encoded per visualizzare il percorso
|
||||
},
|
||||
confirmedPassengers: [{
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
seats: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
pickupPoint: LocationSchema,
|
||||
dropoffPoint: LocationSchema,
|
||||
confirmedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}],
|
||||
views: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
isFeatured: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 1000
|
||||
},
|
||||
cancellationReason: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
cancelledAt: {
|
||||
type: Date
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indici per ricerche ottimizzate
|
||||
RideSchema.index({ 'departure.city': 1, 'destination.city': 1 });
|
||||
RideSchema.index({ 'departure.coordinates': '2dsphere' });
|
||||
RideSchema.index({ 'destination.coordinates': '2dsphere' });
|
||||
RideSchema.index({ 'waypoints.location.city': 1 });
|
||||
RideSchema.index({ dateTime: 1, status: 1 });
|
||||
RideSchema.index({ idapp: 1, status: 1, dateTime: 1 });
|
||||
|
||||
// Virtual per verificare se il viaggio è pieno
|
||||
RideSchema.virtual('isFull').get(function() {
|
||||
if (this.type === 'request') return false;
|
||||
return this.passengers.available <= 0;
|
||||
});
|
||||
|
||||
// Virtual per calcolare posti occupati
|
||||
RideSchema.virtual('bookedSeats').get(function() {
|
||||
if (!this.confirmedPassengers) return 0;
|
||||
return this.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0);
|
||||
});
|
||||
|
||||
// Virtual per ottenere tutte le città del percorso
|
||||
RideSchema.virtual('allCities').get(function() {
|
||||
const cities = [this.departure.city];
|
||||
if (this.waypoints && this.waypoints.length > 0) {
|
||||
this.waypoints
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.forEach(wp => cities.push(wp.location.city));
|
||||
}
|
||||
cities.push(this.destination.city);
|
||||
return cities;
|
||||
});
|
||||
|
||||
// Metodo per verificare se passa per una città
|
||||
RideSchema.methods.passesThrough = function(cityName) {
|
||||
const normalizedCity = cityName.toLowerCase().trim();
|
||||
return this.allCities.some(city =>
|
||||
city.toLowerCase().trim().includes(normalizedCity) ||
|
||||
normalizedCity.includes(city.toLowerCase().trim())
|
||||
);
|
||||
};
|
||||
|
||||
// Metodo per aggiornare posti disponibili
|
||||
RideSchema.methods.updateAvailableSeats = function() {
|
||||
if (this.type === 'offer') {
|
||||
const booked = this.bookedSeats;
|
||||
this.passengers.available = this.passengers.max - booked;
|
||||
if (this.passengers.available <= 0) {
|
||||
this.status = 'full';
|
||||
} else if (this.status === 'full') {
|
||||
this.status = 'active';
|
||||
}
|
||||
}
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Pre-save hook
|
||||
RideSchema.pre('save', function(next) {
|
||||
// Aggiorna posti disponibili se necessario
|
||||
if (this.type === 'offer' && this.isModified('confirmedPassengers')) {
|
||||
const booked = this.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0);
|
||||
this.passengers.available = this.passengers.max - booked;
|
||||
if (this.passengers.available <= 0) {
|
||||
this.status = 'full';
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Metodi statici per ricerche comuni
|
||||
RideSchema.statics.findActiveByCity = function(idapp, departureCity, destinationCity, options = {}) {
|
||||
const query = {
|
||||
idapp,
|
||||
status: { $in: ['active', 'full'] },
|
||||
dateTime: { $gte: new Date() }
|
||||
};
|
||||
|
||||
if (departureCity) {
|
||||
query['departure.city'] = new RegExp(departureCity, 'i');
|
||||
}
|
||||
if (destinationCity) {
|
||||
query['destination.city'] = new RegExp(destinationCity, 'i');
|
||||
}
|
||||
if (options.type) {
|
||||
query.type = options.type;
|
||||
}
|
||||
if (options.date) {
|
||||
const startOfDay = new Date(options.date);
|
||||
startOfDay.setHours(0, 0, 0, 0);
|
||||
const endOfDay = new Date(options.date);
|
||||
endOfDay.setHours(23, 59, 59, 999);
|
||||
query.dateTime = { $gte: startOfDay, $lte: endOfDay };
|
||||
}
|
||||
|
||||
return this.find(query)
|
||||
.populate('userId', 'username name surname profile.driverProfile.averageRating')
|
||||
.sort({ dateTime: 1 });
|
||||
};
|
||||
|
||||
// Ricerca viaggi che passano per una città intermedia
|
||||
RideSchema.statics.findPassingThrough = function(idapp, cityName, options = {}) {
|
||||
const cityRegex = new RegExp(cityName, 'i');
|
||||
const query = {
|
||||
idapp,
|
||||
status: { $in: ['active'] },
|
||||
dateTime: { $gte: new Date() },
|
||||
$or: [
|
||||
{ 'departure.city': cityRegex },
|
||||
{ 'destination.city': cityRegex },
|
||||
{ 'waypoints.location.city': cityRegex }
|
||||
]
|
||||
};
|
||||
|
||||
if (options.type) {
|
||||
query.type = options.type;
|
||||
}
|
||||
|
||||
return this.find(query)
|
||||
.populate('userId', 'username name surname profile.driverProfile.averageRating')
|
||||
.sort({ dateTime: 1 });
|
||||
};
|
||||
|
||||
const Ride = mongoose.model('Ride', RideSchema);
|
||||
|
||||
module.exports = Ride;
|
||||
296
src/models/RideRequest.js
Normal file
296
src/models/RideRequest.js
Normal file
@@ -0,0 +1,296 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
// Schema per le coordinate
|
||||
const CoordinatesSchema = new Schema({
|
||||
lat: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Schema per località
|
||||
const LocationSchema = new Schema({
|
||||
city: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
province: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
coordinates: {
|
||||
type: CoordinatesSchema,
|
||||
required: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
const RideRequestSchema = new Schema({
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
rideId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Ride',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
passengerId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
driverId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500
|
||||
},
|
||||
pickupPoint: {
|
||||
type: LocationSchema
|
||||
},
|
||||
dropoffPoint: {
|
||||
type: LocationSchema
|
||||
},
|
||||
useOriginalRoute: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
// true = usa partenza/destinazione originali del ride
|
||||
},
|
||||
seatsRequested: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 1,
|
||||
default: 1
|
||||
},
|
||||
hasLuggage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
luggageSize: {
|
||||
type: String,
|
||||
enum: ['small', 'medium', 'large'],
|
||||
default: 'small'
|
||||
},
|
||||
hasPackages: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
packageDescription: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 200
|
||||
},
|
||||
hasPets: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
petType: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
petSize: {
|
||||
type: String,
|
||||
enum: ['small', 'medium', 'large']
|
||||
},
|
||||
specialNeeds: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 300
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'accepted', 'rejected', 'cancelled', 'expired', 'completed'],
|
||||
default: 'pending',
|
||||
index: true
|
||||
},
|
||||
responseMessage: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500
|
||||
},
|
||||
respondedAt: {
|
||||
type: Date
|
||||
},
|
||||
contribution: {
|
||||
agreed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
contribTypeId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Contribtype'
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
min: 0
|
||||
},
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true
|
||||
}
|
||||
},
|
||||
cancelledBy: {
|
||||
type: String,
|
||||
enum: ['passenger', 'driver']
|
||||
},
|
||||
cancellationReason: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
cancelledAt: {
|
||||
type: Date
|
||||
},
|
||||
completedAt: {
|
||||
type: Date
|
||||
},
|
||||
feedbackGiven: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indici composti per ricerche ottimizzate
|
||||
RideRequestSchema.index({ rideId: 1, status: 1 });
|
||||
RideRequestSchema.index({ passengerId: 1, status: 1 });
|
||||
RideRequestSchema.index({ driverId: 1, status: 1 });
|
||||
RideRequestSchema.index({ idapp: 1, createdAt: -1 });
|
||||
|
||||
// Virtual per verificare se la richiesta può essere cancellata
|
||||
RideRequestSchema.virtual('canCancel').get(function() {
|
||||
return ['pending', 'accepted'].includes(this.status);
|
||||
});
|
||||
|
||||
// Virtual per verificare se è in attesa
|
||||
RideRequestSchema.virtual('isPending').get(function() {
|
||||
return this.status === 'pending';
|
||||
});
|
||||
|
||||
// Metodo per accettare la richiesta
|
||||
RideRequestSchema.methods.accept = async function(responseMessage = '') {
|
||||
this.status = 'accepted';
|
||||
this.responseMessage = responseMessage;
|
||||
this.respondedAt = new Date();
|
||||
|
||||
// Aggiorna il ride con il passeggero confermato
|
||||
const Ride = mongoose.model('Ride');
|
||||
const ride = await Ride.findById(this.rideId);
|
||||
|
||||
if (ride) {
|
||||
ride.confirmedPassengers.push({
|
||||
userId: this.passengerId,
|
||||
seats: this.seatsRequested,
|
||||
pickupPoint: this.pickupPoint || ride.departure,
|
||||
dropoffPoint: this.dropoffPoint || ride.destination,
|
||||
confirmedAt: new Date()
|
||||
});
|
||||
await ride.updateAvailableSeats();
|
||||
}
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per rifiutare la richiesta
|
||||
RideRequestSchema.methods.reject = function(responseMessage = '') {
|
||||
this.status = 'rejected';
|
||||
this.responseMessage = responseMessage;
|
||||
this.respondedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per cancellare la richiesta
|
||||
RideRequestSchema.methods.cancel = async function(cancelledBy, reason = '') {
|
||||
this.status = 'cancelled';
|
||||
this.cancelledBy = cancelledBy;
|
||||
this.cancellationReason = reason;
|
||||
this.cancelledAt = new Date();
|
||||
|
||||
// Se era accettata, rimuovi il passeggero dal ride
|
||||
if (this.status === 'accepted') {
|
||||
const Ride = mongoose.model('Ride');
|
||||
const ride = await Ride.findById(this.rideId);
|
||||
|
||||
if (ride) {
|
||||
ride.confirmedPassengers = ride.confirmedPassengers.filter(
|
||||
p => p.userId.toString() !== this.passengerId.toString()
|
||||
);
|
||||
await ride.updateAvailableSeats();
|
||||
}
|
||||
}
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere richieste pendenti di un conducente
|
||||
RideRequestSchema.statics.getPendingForDriver = function(idapp, driverId) {
|
||||
return this.find({
|
||||
idapp,
|
||||
driverId,
|
||||
status: 'pending'
|
||||
})
|
||||
.populate('passengerId', 'username name surname email')
|
||||
.populate('rideId', 'departure destination dateTime')
|
||||
.sort({ createdAt: -1 });
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere richieste di un passeggero
|
||||
RideRequestSchema.statics.getByPassenger = function(idapp, passengerId, status = null) {
|
||||
const query = { idapp, passengerId };
|
||||
if (status) {
|
||||
query.status = status;
|
||||
}
|
||||
return this.find(query)
|
||||
.populate('rideId')
|
||||
.populate('driverId', 'username name surname')
|
||||
.sort({ createdAt: -1 });
|
||||
};
|
||||
|
||||
// Pre-save hook per validazioni
|
||||
RideRequestSchema.pre('save', async function(next) {
|
||||
if (this.isNew) {
|
||||
// Verifica che il ride esista e abbia posti disponibili
|
||||
const Ride = mongoose.model('Ride');
|
||||
const ride = await Ride.findById(this.rideId);
|
||||
|
||||
if (!ride) {
|
||||
throw new Error('Viaggio non trovato');
|
||||
}
|
||||
|
||||
if (ride.type === 'offer' && ride.passengers.available < this.seatsRequested) {
|
||||
throw new Error('Posti non sufficienti per questo viaggio');
|
||||
}
|
||||
|
||||
if (ride.userId.toString() === this.passengerId.toString()) {
|
||||
throw new Error('Non puoi richiedere un passaggio per il tuo stesso viaggio');
|
||||
}
|
||||
|
||||
// Imposta il driverId dal ride
|
||||
this.driverId = ride.userId;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
const RideRequest = mongoose.model('RideRequest', RideRequestSchema);
|
||||
|
||||
module.exports = RideRequest;
|
||||
@@ -56,11 +56,11 @@ ContribtypeSchema.statics.findAllIdApp = async function (idapp) {
|
||||
return await Contribtype.find(myfind).lean();
|
||||
};
|
||||
|
||||
const Contribtype = mongoose.model('Contribtype', ContribtypeSchema);
|
||||
const Contribtype = mongoose.models.Contribtype || mongoose.model('Contribtype', ContribtypeSchema);
|
||||
|
||||
Contribtype.createIndexes()
|
||||
.then(() => { })
|
||||
.catch((err) => { throw err; });
|
||||
|
||||
|
||||
module.exports = { Contribtype };
|
||||
module.exports = { Contribtype };
|
||||
@@ -175,6 +175,7 @@ const SiteSchema = new Schema({
|
||||
enableEcommerce: { type: Boolean, default: false },
|
||||
enableAI: { type: Boolean, default: false },
|
||||
enablePoster: { type: Boolean, default: false },
|
||||
enableTrasporti: { type: Boolean, default: false },
|
||||
enableGroups: { type: Boolean, default: false },
|
||||
enableCircuits: { type: Boolean, default: false },
|
||||
enableGoods: { type: Boolean, default: false },
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const mongoose = require('mongoose').set('debug', false);
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const validator = require('validator');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const _ = require('lodash');
|
||||
@@ -285,6 +287,10 @@ const UserSchema = new mongoose.Schema(
|
||||
cell: {
|
||||
type: String,
|
||||
},
|
||||
cellVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
country_pay: {
|
||||
type: String,
|
||||
},
|
||||
@@ -584,6 +590,264 @@ const UserSchema = new mongoose.Schema(
|
||||
],
|
||||
version: { type: Number },
|
||||
insert_circuito_ita: { type: Boolean },
|
||||
|
||||
// ============ DRIVER PROFILE ============
|
||||
driverProfile: {
|
||||
isDriver: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bio: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500,
|
||||
},
|
||||
vehicles: [
|
||||
{
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['auto', 'moto', 'furgone', 'minibus', 'altro'],
|
||||
default: 'auto',
|
||||
},
|
||||
brand: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
colorHex: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
year: {
|
||||
type: Number,
|
||||
},
|
||||
seats: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 50,
|
||||
},
|
||||
licensePlate: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
features: [
|
||||
{
|
||||
type: String,
|
||||
enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi'],
|
||||
},
|
||||
],
|
||||
isDefault: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
ridesCompletedAsDriver: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
ridesCompletedAsPassenger: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
averageRating: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 5,
|
||||
},
|
||||
totalRatings: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
verifiedDriver: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
licenseVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
licenseNumber: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
licenseExpiry: {
|
||||
type: Date,
|
||||
},
|
||||
memberSince: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
responseRate: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
responseTime: {
|
||||
type: String,
|
||||
enum: ['within_hour', 'within_day', 'within_days'],
|
||||
default: 'within_day',
|
||||
},
|
||||
totalKmShared: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
co2Saved: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
// kg di CO2 risparmiati
|
||||
},
|
||||
badges: [
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
earnedAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
},
|
||||
],
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
min: 1,
|
||||
},
|
||||
points: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
// ============ PREFERENCES ============
|
||||
preferences: {
|
||||
// Preferenze di viaggio
|
||||
smoking: {
|
||||
type: String,
|
||||
enum: ['yes', 'no', 'outside_only'],
|
||||
default: 'no',
|
||||
},
|
||||
pets: {
|
||||
type: String,
|
||||
enum: ['no', 'small', 'medium', 'large', 'all'],
|
||||
default: 'small',
|
||||
},
|
||||
music: {
|
||||
type: String,
|
||||
enum: ['no_music', 'quiet', 'moderate', 'loud', 'passenger_choice'],
|
||||
default: 'moderate',
|
||||
},
|
||||
conversation: {
|
||||
type: String,
|
||||
enum: ['quiet', 'moderate', 'chatty'],
|
||||
default: 'moderate',
|
||||
},
|
||||
|
||||
// Notifiche
|
||||
notifications: {
|
||||
rideRequests: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rideAccepted: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rideReminders: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
messages: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
marketing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
pushEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
emailEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Privacy
|
||||
privacy: {
|
||||
showEmail: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showPhone: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showLastName: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showRides: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Località preferite
|
||||
favoriteLocations: [
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
city: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
coordinates: {
|
||||
lat: Number,
|
||||
lng: Number,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['home', 'work', 'other'],
|
||||
default: 'other',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// Lingue parlate
|
||||
languages: [
|
||||
{
|
||||
type: String,
|
||||
},
|
||||
],
|
||||
|
||||
// Metodo di pagamento preferito
|
||||
preferredContribType: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Contribtype',
|
||||
},
|
||||
},
|
||||
},
|
||||
updatedAt: { type: Date, default: Date.now },
|
||||
},
|
||||
@@ -7016,8 +7280,6 @@ UserSchema.statics.getUsersList = function (idapp) {
|
||||
}).lean();
|
||||
};
|
||||
|
||||
const User = mongoose.model('User', UserSchema);
|
||||
|
||||
class Hero {
|
||||
constructor(name, level) {
|
||||
this.name = name;
|
||||
@@ -7074,6 +7336,9 @@ const FuncUsers = {
|
||||
|
||||
UserSchema.index({ 'tokens.token': 1, 'tokens.access': 1, idapp: 1, deleted: 1, updatedAt: 1 });
|
||||
|
||||
|
||||
const User = mongoose.models.User || mongoose.model('User', UserSchema);
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
Hero,
|
||||
|
||||
@@ -33,6 +33,10 @@ const { MyElem } = require('../models/myelem');
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
const trasportiRoutes = require('../routes/trasportiRoutes');
|
||||
router.use('/trasporti', trasportiRoutes);
|
||||
|
||||
|
||||
// Importa le routes video
|
||||
const videoRoutes = require('../routes/videoRoutes');
|
||||
|
||||
|
||||
714
src/routes/trasportiRoutes.js
Normal file
714
src/routes/trasportiRoutes.js
Normal file
@@ -0,0 +1,714 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const { Contribtype } = require('../models/Contribtype'); // Adatta al tuo path
|
||||
|
||||
// Import Controllers
|
||||
const rideController = require('../controllers/rideController');
|
||||
const rideRequestController = require('../controllers/rideRequestController');
|
||||
const chatController = require('../controllers/chatController');
|
||||
const feedbackController = require('../controllers/feedbackController');
|
||||
const geocodingController = require('../controllers/geocodingController');
|
||||
|
||||
// Middleware di autenticazione (usa il tuo esistente)
|
||||
const { authenticate } = require('../middleware/authenticate');
|
||||
|
||||
// ============================================================
|
||||
// 🚗 RIDES - Gestione Viaggi
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/rides
|
||||
* @desc Crea nuovo viaggio (offerta o richiesta)
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/rides', authenticate, rideController.createRide);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/rides
|
||||
* @desc Ottieni lista viaggi con filtri
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/rides', rideController.getRides);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/rides/search
|
||||
* @desc Ricerca viaggi avanzata
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/rides/search', rideController.searchRides);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/rides/stats
|
||||
* @desc Statistiche per widget homepage
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/rides/stats', authenticate, rideController.getRidesStats);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/rides/my
|
||||
* @desc I miei viaggi (come driver e passenger)
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/rides/my', authenticate, rideController.getMyRides);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/rides/match
|
||||
* @desc Match automatico offerta/richiesta
|
||||
* @access Private
|
||||
* @note ⚠️ IMPORTANTE: Questa route DEVE stare PRIMA di /rides/:id
|
||||
*/
|
||||
//router.get('/rides/match', authenticate, rideController.findMatches);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/rides/:id
|
||||
* @desc Dettaglio singolo viaggio
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/rides/:id', rideController.getRideById);
|
||||
|
||||
/**
|
||||
* @route PUT /api/trasporti/rides/:id
|
||||
* @desc Aggiorna viaggio
|
||||
* @access Private (solo owner)
|
||||
*/
|
||||
router.put('/rides/:id', authenticate, rideController.updateRide);
|
||||
|
||||
/**
|
||||
* @route DELETE /api/trasporti/rides/:id
|
||||
* @desc Cancella viaggio
|
||||
* @access Private (solo owner)
|
||||
*/
|
||||
router.delete('/rides/:id', authenticate, rideController.deleteRide);
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/rides/:id/complete
|
||||
* @desc Completa un viaggio
|
||||
* @access Private (solo driver)
|
||||
*/
|
||||
router.post('/rides/:id/complete', authenticate, rideController.completeRide);
|
||||
|
||||
// ============================================================
|
||||
// 📊 WIDGET & STATS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/widget/data
|
||||
* @desc Dati completi per widget homepage
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/widget/data', authenticate, rideController.getWidgetData);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/stats/summary
|
||||
* @desc Stats rapide per header widget (offerte, richieste, match)
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/stats/summary', authenticate, rideController.getStatsSummary);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/cities/suggestions
|
||||
* @desc Suggerimenti città per autocomplete (basato su viaggi esistenti)
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/cities/suggestions', rideController.getCitySuggestions);
|
||||
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/cities/recents
|
||||
* @desc città recenti per autocomplete
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/cities/recent', authenticate, rideController.getRecentCities);
|
||||
|
||||
// ============================================================
|
||||
// 📩 REQUESTS - Richieste Passaggio
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/requests
|
||||
* @desc Crea richiesta passaggio per un viaggio
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/requests', authenticate, rideRequestController.createRequest);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/requests/received
|
||||
* @desc Richieste ricevute (sono conducente)
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/requests/received', authenticate, rideRequestController.getReceivedRequests);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/requests/sent
|
||||
* @desc Richieste inviate (sono passeggero)
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/requests/sent', authenticate, rideRequestController.getSentRequests);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/requests/ride/:rideId
|
||||
* @desc Richieste per un viaggio specifico
|
||||
* @access Private (solo owner del viaggio)
|
||||
*/
|
||||
router.get('/requests/ride/:rideId', authenticate, rideRequestController.getRequestsForRide);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/requests/:id
|
||||
* @desc Dettaglio singola richiesta
|
||||
* @access Private (driver o passenger)
|
||||
*/
|
||||
router.get('/requests/:id', authenticate, rideRequestController.getRequestById);
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/requests/:id/accept
|
||||
* @desc Accetta richiesta passaggio
|
||||
* @access Private (solo driver)
|
||||
*/
|
||||
router.post('/requests/:id/accept', authenticate, rideRequestController.acceptRequest);
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/requests/:id/reject
|
||||
* @desc Rifiuta richiesta passaggio
|
||||
* @access Private (solo driver)
|
||||
*/
|
||||
router.post('/requests/:id/reject', authenticate, rideRequestController.rejectRequest);
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/requests/:id/cancel
|
||||
* @desc Cancella richiesta/prenotazione
|
||||
* @access Private (driver o passenger)
|
||||
*/
|
||||
router.post('/requests/:id/cancel', authenticate, rideRequestController.cancelRequest);
|
||||
|
||||
// ============================================================
|
||||
// 💬 CHAT - Messaggistica
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/chats
|
||||
* @desc Lista tutte le mie chat
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/chats', authenticate, chatController.getMyChats);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/chats/unread/count
|
||||
* @desc Conta messaggi non letti totali
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/chats/unread/count', authenticate, chatController.getUnreadCount);
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/chats/direct
|
||||
* @desc Ottieni o crea chat diretta con utente
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/chats/direct', authenticate, chatController.getOrCreateDirectChat);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/chats/:id
|
||||
* @desc Dettaglio chat
|
||||
* @access Private (solo partecipanti)
|
||||
*/
|
||||
router.get('/chats/:id', authenticate, chatController.getChatById);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/chats/:id/messages
|
||||
* @desc Messaggi di una chat
|
||||
* @access Private (solo partecipanti)
|
||||
*/
|
||||
router.get('/chats/:id/messages', authenticate, chatController.getChatMessages);
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/chats/:id/messages
|
||||
* @desc Invia messaggio
|
||||
* @access Private (solo partecipanti)
|
||||
*/
|
||||
router.post('/chats/:id/messages', authenticate, chatController.sendMessage);
|
||||
|
||||
/**
|
||||
* @route PUT /api/trasporti/chats/:id/read
|
||||
* @desc Segna chat come letta
|
||||
* @access Private (solo partecipanti)
|
||||
* @fix Corretto: markAsRead → markChatAsRead
|
||||
*/
|
||||
router.put('/chats/:id/read', authenticate, chatController.markChatAsRead);
|
||||
|
||||
/**
|
||||
* @route PUT /api/trasporti/chats/:id/block
|
||||
* @desc Blocca/sblocca chat
|
||||
* @access Private (solo partecipanti)
|
||||
*/
|
||||
router.put('/chats/:id/block', authenticate, chatController.toggleBlockChat);
|
||||
|
||||
/**
|
||||
* @route PUT /api/trasporti/chats/:id/mute
|
||||
* @desc Muta/smuta notifiche di una chat
|
||||
* @access Private (solo partecipanti)
|
||||
* @fix Aggiunta route mancante
|
||||
*/
|
||||
router.put('/chats/:id/mute', authenticate, chatController.toggleMuteChat);
|
||||
|
||||
/**
|
||||
* @route DELETE /api/trasporti/chats/:chatId/messages/:messageId
|
||||
* @desc Elimina messaggio
|
||||
* @access Private (solo mittente)
|
||||
* @fix Corretto: /messages/:id → /chats/:chatId/messages/:messageId
|
||||
*/
|
||||
router.delete('/chats/:chatId/messages/:messageId', authenticate, chatController.deleteMessage);
|
||||
|
||||
// ============================================================
|
||||
// ⭐ FEEDBACK - Recensioni
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/feedback
|
||||
* @desc Crea feedback per un viaggio
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/feedback', authenticate, feedbackController.createFeedback);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/feedback/my/received
|
||||
* @desc I miei feedback ricevuti
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/feedback/my/received', authenticate, feedbackController.getMyReceivedFeedbacks);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/feedback/my/given
|
||||
* @desc I miei feedback lasciati
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/feedback/my/given', authenticate, feedbackController.getMyGivenFeedbacks);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/feedback/user/:userId
|
||||
* @desc Feedback di un utente
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/feedback/user/:userId', feedbackController.getFeedbacksForUser);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/feedback/user/:userId/stats
|
||||
* @desc Statistiche feedback utente
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/feedback/user/:userId/stats', feedbackController.getUserFeedbackStats);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/feedback/ride/:rideId
|
||||
* @desc Feedback per un viaggio
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/feedback/ride/:rideId', feedbackController.getRideFeedback);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/feedback/can-leave/:rideId/:toUserId
|
||||
* @desc Verifica se posso lasciare feedback
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/feedback/can-leave/:rideId/:toUserId', authenticate, feedbackController.canLeaveFeedback);
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/feedback/:id/response
|
||||
* @desc Rispondi a un feedback
|
||||
* @access Private (solo destinatario)
|
||||
*/
|
||||
router.post('/feedback/:id/response', authenticate, feedbackController.respondToFeedback);
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/feedback/:id/report
|
||||
* @desc Segnala feedback
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/feedback/:id/report', authenticate, feedbackController.reportFeedback);
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/feedback/:id/helpful
|
||||
* @desc Segna feedback come utile
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/feedback/:id/helpful', authenticate, feedbackController.markAsHelpful);
|
||||
|
||||
// ============================================================
|
||||
// 🗺️ GEO - Geocoding & Mappe (Open Source)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/geo/autocomplete
|
||||
* @desc Autocomplete città (Photon)
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/geo/autocomplete', geocodingController.autocomplete);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/geo/cities/it
|
||||
* @desc Cerca città italiane
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/geo/cities/it', geocodingController.searchItalianCities);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/geo/geocode
|
||||
* @desc Indirizzo → Coordinate
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/geo/geocode', geocodingController.geocode);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/geo/reverse
|
||||
* @desc Coordinate → Indirizzo
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/geo/reverse', geocodingController.reverseGeocode);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/geo/route
|
||||
* @desc Calcola percorso tra punti
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/geo/route', geocodingController.getRoute);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/geo/distance
|
||||
* @desc Calcola distanza e durata
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/geo/distance', geocodingController.getDistance);
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/geo/suggest-waypoints
|
||||
* @desc Suggerisci città intermedie sul percorso
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/geo/suggest-waypoints', geocodingController.suggestWaypoints);
|
||||
|
||||
// ============================================================
|
||||
// 🔧 UTILITY & DRIVER PROFILE
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/driver/:userId
|
||||
* @desc Profilo pubblico del conducente
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/driver/:userId', async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { idapp } = req.query;
|
||||
|
||||
const { User } = require('../models/User');
|
||||
const Ride = require('../models/Ride');
|
||||
const Feedback = require('../models/Feedback');
|
||||
|
||||
// Dati utente
|
||||
const user = await User.findById(userId).select(
|
||||
'username name surname profile.img profile.Biografia profile.driverProfile profile.preferences.languages'
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Utente non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
// Statistiche viaggi
|
||||
const [ridesAsDriver, ridesAsPassenger, completedRides] = await Promise.all([
|
||||
Ride.countDocuments({ idapp, userId, type: 'offer' }),
|
||||
Ride.countDocuments({ idapp, 'confirmedPassengers.userId': userId }),
|
||||
Ride.countDocuments({ idapp, userId, status: 'completed' }),
|
||||
]);
|
||||
|
||||
// Ultimi viaggi come driver
|
||||
const recentRides = await Ride.find({
|
||||
idapp,
|
||||
userId,
|
||||
type: 'offer',
|
||||
status: { $in: ['active', 'completed'] },
|
||||
})
|
||||
.select('departure destination dateTime status')
|
||||
.sort({ dateTime: -1 })
|
||||
.limit(5);
|
||||
|
||||
// Statistiche feedback
|
||||
let feedbackStats = { averageRating: 0, totalFeedback: 0 };
|
||||
try {
|
||||
feedbackStats = await Feedback.getStatsForUser(idapp, userId);
|
||||
} catch (e) {
|
||||
console.log('Feedback stats non disponibili');
|
||||
}
|
||||
|
||||
// Ultimi feedback ricevuti
|
||||
let recentFeedback = [];
|
||||
try {
|
||||
recentFeedback = await Feedback.find({
|
||||
idapp,
|
||||
toUserId: userId,
|
||||
isPublic: true,
|
||||
})
|
||||
.populate('fromUserId', 'username name profile.img')
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(3);
|
||||
} catch (e) {
|
||||
console.log('Recent feedback non disponibili');
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
user: {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
surname: user.surname,
|
||||
img: user.profile?.img,
|
||||
bio: user.profile?.Biografia,
|
||||
driverProfile: user.profile?.driverProfile,
|
||||
languages: user.profile?.preferences?.languages,
|
||||
},
|
||||
stats: {
|
||||
ridesAsDriver,
|
||||
ridesAsPassenger,
|
||||
completedRides,
|
||||
...feedbackStats,
|
||||
},
|
||||
recentRides,
|
||||
recentFeedback,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore recupero profilo driver:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il recupero del profilo',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route PUT /api/trasporti/driver/profile
|
||||
* @desc Aggiorna profilo conducente
|
||||
* @access Private
|
||||
*/
|
||||
router.put('/driver/profile', authenticate, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { idapp, driverProfile, preferences } = req.body;
|
||||
|
||||
const { User } = require('../models/User');
|
||||
|
||||
const updateData = {};
|
||||
|
||||
if (driverProfile) {
|
||||
// Merge con dati esistenti
|
||||
Object.keys(driverProfile).forEach((key) => {
|
||||
updateData[`profile.driverProfile.${key}`] = driverProfile[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (preferences) {
|
||||
Object.keys(preferences).forEach((key) => {
|
||||
updateData[`profile.preferences.${key}`] = preferences[key];
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.findByIdAndUpdate(userId, { $set: updateData }, { new: true }).select(
|
||||
'profile.driverProfile profile.preferences'
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Profilo aggiornato',
|
||||
data: user.profile,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore aggiornamento profilo:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Errore durante l'aggiornamento",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/driver/vehicles
|
||||
* @desc Aggiungi veicolo
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/driver/vehicles', authenticate, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { vehicle } = req.body;
|
||||
|
||||
const { User } = require('../models/User');
|
||||
|
||||
const user = await User.findByIdAndUpdate(
|
||||
userId,
|
||||
{
|
||||
$push: { 'profile.driverProfile.vehicles': vehicle },
|
||||
$set: { 'profile.driverProfile.isDriver': true },
|
||||
},
|
||||
{ new: true }
|
||||
).select('profile.driverProfile.vehicles');
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Veicolo aggiunto',
|
||||
data: user.profile.driverProfile.vehicles,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore aggiunta veicolo:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Errore durante l'aggiunta del veicolo",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route PUT /api/trasporti/driver/vehicles/:vehicleId
|
||||
* @desc Aggiorna veicolo
|
||||
* @access Private
|
||||
*/
|
||||
router.put('/driver/vehicles/:vehicleId', authenticate, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { vehicleId } = req.params;
|
||||
const { vehicle } = req.body;
|
||||
|
||||
const { User } = require('../models/User');
|
||||
|
||||
const user = await User.findOneAndUpdate(
|
||||
{
|
||||
_id: userId,
|
||||
'profile.driverProfile.vehicles._id': vehicleId,
|
||||
},
|
||||
{
|
||||
$set: { 'profile.driverProfile.vehicles.$': { ...vehicle, _id: vehicleId } },
|
||||
},
|
||||
{ new: true }
|
||||
).select('profile.driverProfile.vehicles');
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Veicolo non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Veicolo aggiornato',
|
||||
data: user.profile.driverProfile.vehicles,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore aggiornamento veicolo:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Errore durante l'aggiornamento",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route DELETE /api/trasporti/driver/vehicles/:vehicleId
|
||||
* @desc Rimuovi veicolo
|
||||
* @access Private
|
||||
*/
|
||||
router.delete('/driver/vehicles/:vehicleId', authenticate, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { vehicleId } = req.params;
|
||||
|
||||
const { User } = require('../models/User');
|
||||
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$pull: { 'profile.driverProfile.vehicles': { _id: vehicleId } },
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Veicolo rimosso',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore rimozione veicolo:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante la rimozione',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route POST /api/trasporti/driver/vehicles/:vehicleId/default
|
||||
* @desc Imposta veicolo come predefinito
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/driver/vehicles/:vehicleId/default', authenticate, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { vehicleId } = req.params;
|
||||
|
||||
const { User } = require('../models/User');
|
||||
|
||||
// Prima rimuovi isDefault da tutti
|
||||
await User.updateOne({ _id: userId }, { $set: { 'profile.driverProfile.vehicles.$[].isDefault': false } });
|
||||
|
||||
// Poi imposta quello selezionato
|
||||
await User.updateOne(
|
||||
{ _id: userId, 'profile.driverProfile.vehicles._id': vehicleId },
|
||||
{ $set: { 'profile.driverProfile.vehicles.$.isDefault': true } }
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Veicolo predefinito impostato',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore impostazione default:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Errore durante l'operazione",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 📊 CONTRIB TYPES - Tipi di Contributo
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @route GET /api/trasporti/contrib-types
|
||||
* @desc Lista tipi di contributo disponibili
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/contrib-types', async (req, res) => {
|
||||
try {
|
||||
const { idapp } = req.query;
|
||||
|
||||
const contribTypes = await Contribtype.find({ idapp });
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: contribTypes,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore recupero contrib types:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il recupero',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ const registerFonts = async () => {
|
||||
{ file: 'PlayfairDisplay-Regular.ttf', family: 'Playfair Display', weight: '400' }
|
||||
];
|
||||
|
||||
|
||||
for (const font of fontMappings) {
|
||||
const fontPath = path.join(FONTS_DIR, font.file);
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user