- Trasporti- Passo 2

This commit is contained in:
Surya Paolo
2025-12-22 23:39:47 +01:00
parent 2e7801b4ba
commit b78e3ce544
16 changed files with 1096 additions and 505 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -4,7 +4,7 @@ const sharp = require('sharp');
const path = require('path'); const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'; const UPLOAD_DIR = process.env.UPLOAD_DIR || './upload';
const assetController = { const assetController = {
// POST /assets/upload // POST /assets/upload
@@ -50,9 +50,9 @@ const assetController = {
sourceType: 'upload', sourceType: 'upload',
file: { file: {
path: file.path, path: file.path,
url: `/uploads/${file.filename}`, url: `/upload/${file.filename}`,
thumbnailPath: thumbPath, thumbnailPath: thumbPath,
thumbnailUrl: `/uploads/thumbs/${thumbName}`, thumbnailUrl: `/upload/thumbs/${thumbName}`,
originalName: file.originalname, originalName: file.originalname,
mimeType: file.mimetype, mimeType: file.mimetype,
size: file.size, size: file.size,
@@ -106,7 +106,7 @@ const assetController = {
sourceType: 'upload', sourceType: 'upload',
file: { file: {
path: file.path, path: file.path,
url: `/uploads/${file.filename}`, url: `/upload/${file.filename}`,
originalName: file.originalname, originalName: file.originalname,
mimeType: file.mimetype, mimeType: file.mimetype,
size: file.size, size: file.size,
@@ -199,7 +199,7 @@ const assetController = {
sourceType: 'ai', sourceType: 'ai',
file: { file: {
path: filePath, path: filePath,
url: `/uploads/ai-generated/${fileName}`, url: `/upload/ai-generated/${fileName}`,
mimeType: 'image/jpeg', mimeType: 'image/jpeg',
size: fileSize, size: fileSize,
dimensions dimensions

View File

@@ -1,6 +1,6 @@
const Chat = require('../models/Chat'); const Chat = require('../models/Chat');
const Message = require('../models/Message'); const Message = require('../models/Message');
const { User } = require('../models/User'); const { User } = require('../models/user');
/** /**
* @desc Ottieni tutte le chat dell'utente * @desc Ottieni tutte le chat dell'utente
@@ -10,7 +10,8 @@ const { User } = require('../models/User');
const getMyChats = async (req, res) => { const getMyChats = async (req, res) => {
try { try {
const userId = req.user._id; const userId = req.user._id;
const { idapp, page = 1, limit = 20 } = req.query; const idapp = req.user.idapp;
const { page = 1, limit = 20 } = req.query;
if (!idapp) { if (!idapp) {
return res.status(400).json({ return res.status(400).json({
@@ -83,7 +84,8 @@ const getMyChats = async (req, res) => {
const getOrCreateDirectChat = async (req, res) => { const getOrCreateDirectChat = async (req, res) => {
try { try {
const userId = req.user._id; const userId = req.user._id;
const { idapp, otherUserId, rideId } = req.body; const idapp = req.user.idapp;
const { otherUserId, rideId } = req.body;
if (!idapp || !otherUserId) { if (!idapp || !otherUserId) {
return res.status(400).json({ return res.status(400).json({
@@ -199,7 +201,8 @@ const getChatById = async (req, res) => {
const getChatMessages = async (req, res) => { const getChatMessages = async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
const { idapp, before, after, limit = 50 } = req.query; const idapp = req.user.idapp;
const { before, after, limit = 50 } = req.query;
const userId = req.user._id; const userId = req.user._id;
// Verifica accesso alla chat // Verifica accesso alla chat
@@ -209,7 +212,7 @@ const getChatMessages = async (req, res) => {
success: false, success: false,
message: 'Chat non trovata' message: 'Chat non trovata'
}); });
} }
if (!chat.hasParticipant(userId)) { if (!chat.hasParticipant(userId)) {
return res.status(403).json({ return res.status(403).json({
@@ -258,7 +261,8 @@ const getChatMessages = async (req, res) => {
const sendMessage = async (req, res) => { const sendMessage = async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
const { idapp, text, type = 'text', metadata, replyTo } = req.body; const idapp = req.user.idapp;
const { text, type = 'text', metadata, replyTo } = req.body;
const userId = req.user._id; const userId = req.user._id;
if (!idapp || !text) { if (!idapp || !text) {
@@ -499,7 +503,7 @@ const toggleMuteChat = async (req, res) => {
const getUnreadCount = async (req, res) => { const getUnreadCount = async (req, res) => {
try { try {
const userId = req.user._id; const userId = req.user._id;
const { idapp } = req.query; const idapp = req.user.idapp;
if (!idapp) { if (!idapp) {
return res.status(400).json({ return res.status(400).json({
@@ -597,6 +601,67 @@ const deleteMessage = async (req, res) => {
} }
}; };
/**
* @desc Elimina una chat (soft delete)
* @route DELETE /api/trasporti/chats/:id
* @access Private
*/
const deleteChat = 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'
});
}
// Verifica che l'utente sia partecipante
if (!chat.hasParticipant(userId)) {
return res.status(403).json({
success: false,
message: 'Non sei autorizzato a eliminare questa chat'
});
}
// Soft delete: segna come non attiva per questo utente
// Se vuoi hard delete, usa: await chat.deleteOne();
// Opzione 1: Soft delete (chat rimane ma nascosta per questo utente)
if (!chat.deletedBy) {
chat.deletedBy = [];
}
if (!chat.deletedBy.includes(userId)) {
chat.deletedBy.push(userId);
}
// Se tutti i partecipanti hanno eliminato, marca come non attiva
if (chat.deletedBy.length === chat.participants.length) {
chat.isActive = false;
}
await chat.save();
res.json({
success: true,
message: 'Chat eliminata'
});
} catch (error) {
console.error('Errore eliminazione chat:', error);
res.status(500).json({
success: false,
message: 'Errore nell\'eliminazione della chat',
error: error.message
});
}
};
module.exports = { module.exports = {
getMyChats, getMyChats,
getOrCreateDirectChat, getOrCreateDirectChat,
@@ -607,5 +672,6 @@ module.exports = {
toggleBlockChat, toggleBlockChat,
toggleMuteChat, toggleMuteChat,
getUnreadCount, getUnreadCount,
deleteMessage deleteMessage,
deleteChat,
}; };

View File

@@ -1,7 +1,7 @@
const Feedback = require('../models/Feedback'); const Feedback = require('../models/Feedback');
const Ride = require('../models/Ride'); const Ride = require('../models/Ride');
const RideRequest = require('../models/RideRequest'); const RideRequest = require('../models/RideRequest');
const { User } = require('../models/User'); const { User } = require('../models/user');
/** /**
* @desc Crea un feedback per un viaggio * @desc Crea un feedback per un viaggio

View File

@@ -6,7 +6,7 @@ const imageGenerator = require('../services/imageGenerator');
const path = require('path'); const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'; const UPLOAD_DIR = process.env.UPLOAD_DIR || './upload';
const posterController = { const posterController = {
// POST /posters // POST /posters
@@ -396,7 +396,7 @@ const posterController = {
// Aggiorna asset nel poster // Aggiorna asset nel poster
const assetData = { const assetData = {
sourceType: 'ai', sourceType: 'ai',
url: `/uploads/ai-generated/${fileName}`, url: `/upload/ai-generated/${fileName}`,
mimeType: 'image/jpeg', mimeType: 'image/jpeg',
aiParams: { aiParams: {
prompt, prompt,
@@ -572,7 +572,7 @@ const posterController = {
assets: { assets: {
backgroundImage: { backgroundImage: {
sourceType: 'ai', sourceType: 'ai',
url: `/uploads/ai-generated/${fileName}`, url: `/upload/ai-generated/${fileName}`,
mimeType: 'image/jpeg', mimeType: 'image/jpeg',
aiParams: { aiParams: {
prompt: aiPrompt, prompt: aiPrompt,
@@ -629,12 +629,12 @@ const posterController = {
poster.setRenderOutput({ poster.setRenderOutput({
png: { png: {
path: result.pngPath, path: result.pngPath,
url: `/uploads/posters/final/${path.basename(result.pngPath)}`, url: `/upload/posters/final/${path.basename(result.pngPath)}`,
size: result.pngSize size: result.pngSize
}, },
jpg: { jpg: {
path: result.jpgPath, path: result.jpgPath,
url: `/uploads/posters/final/${path.basename(result.jpgPath)}`, url: `/upload/posters/final/${path.basename(result.jpgPath)}`,
size: result.jpgSize, size: result.jpgSize,
quality: 95 quality: 95
}, },

View File

@@ -1,5 +1,5 @@
const Ride = require('../models/Ride'); const Ride = require('../models/Ride');
const User = require('../models/User'); const User = require('../models/user');
const RideRequest = require('../models/RideRequest'); const RideRequest = require('../models/RideRequest');
/** /**

View File

@@ -602,6 +602,157 @@ const getRequestById = async (req, res) => {
} }
}; };
/**
* @desc Ottieni richieste ricevute (io come conducente)
* @route GET /api/trasporti/requests/received
* @access Private
*/
const getReceivedRequests = async (req, res) => {
try {
const driverId = req.user._id;
const { status, page = 1, limit = 20 } = req.query;
// Build query
const query = { driverId };
if (status) {
query.status = status;
}
const skip = (parseInt(page) - 1) * parseInt(limit);
// Fetch requests
const requests = await RideRequest.find(query)
.populate('passengerId', 'name surname profile.img') // <-- FIX: passengerId invece di userId
.populate('rideId', 'departure destination departureDate availableSeats')
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit))
.lean();
// Get counts by status
const counts = await RideRequest.aggregate([
{ $match: { driverId } },
{
$group: {
_id: '$status',
count: { $sum: 1 }
}
}
]);
// Format counts
const statusCounts = {
pending: 0,
accepted: 0,
rejected: 0,
cancelled: 0,
expired: 0,
completed: 0
};
counts.forEach(c => {
if (statusCounts.hasOwnProperty(c._id)) {
statusCounts[c._id] = c.count;
}
});
// Total count
const total = await RideRequest.countDocuments(query);
res.json({
success: true,
data: {
requests,
counts: statusCounts,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / parseInt(limit))
}
}
});
} catch (error) {
console.error('Error fetching received requests:', error);
res.status(500).json({
success: false,
message: 'Errore nel recupero delle richieste ricevute',
error: error.message
});
}
};
/**
* @desc Ottieni richieste inviate (io come passeggero)
* @route GET /api/trasporti/requests/sent
* @access Private
*/
const getSentRequests = async (req, res) => {
try {
const userId = req.user._id;
const { status, page = 1, limit = 20 } = req.query;
// Build query - FIX: usa passengerId invece di userId
const query = { passengerId: userId };
if (status) {
query.status = status;
}
const skip = (parseInt(page) - 1) * parseInt(limit);
// Fetch requests
const requests = await RideRequest.find(query)
.populate('driverId', 'name surname profile.img')
.populate('rideId', 'departure destination departureDate availableSeats currentPassengers')
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit))
.lean();
// Enrich with ride info
const enrichedRequests = requests.map(request => {
const req = { ...request };
// Add ride info if rideId is populated
if (req.rideId) {
req.rideInfo = {
departure: req.rideId.departure?.city || req.rideId.departure,
destination: req.rideId.destination?.city || req.rideId.destination,
departureDate: req.rideId.departureDate,
availableSeats: req.rideId.availableSeats,
currentPassengers: req.rideId.currentPassengers || 0
};
}
return req;
});
// Total count
const total = await RideRequest.countDocuments(query);
res.json({
success: true,
data: {
requests: enrichedRequests,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / parseInt(limit))
}
}
});
} catch (error) {
console.error('Error fetching sent requests:', error);
res.status(500).json({
success: false,
message: 'Errore nel recupero delle richieste inviate',
error: error.message
});
}
};
module.exports = { module.exports = {
createRequest, createRequest,
getRequestsForRide, getRequestsForRide,
@@ -611,6 +762,6 @@ module.exports = {
rejectRequest, rejectRequest,
cancelRequest, cancelRequest,
getRequestById, getRequestById,
getReceivedRequests: getPendingRequests, getReceivedRequests,
getSentRequests: getMyRequests, getSentRequests,
}; };

View File

@@ -5,9 +5,9 @@
"sourceType": "ai", "sourceType": "ai",
"file": { "file": {
"path": "/uploads/assets/backgrounds/forest_autumn_001.jpg", "path": "/upload/assets/backgrounds/forest_autumn_001.jpg",
"url": "/api/assets/asset_bg_001/file", "url": "/api/assets/asset_bg_001/file",
"thumbnailPath": "/uploads/assets/backgrounds/thumbs/forest_autumn_001_thumb.jpg", "thumbnailPath": "/upload/assets/backgrounds/thumbs/forest_autumn_001_thumb.jpg",
"thumbnailUrl": "/api/assets/asset_bg_001/thumbnail", "thumbnailUrl": "/api/assets/asset_bg_001/thumbnail",
"originalName": null, "originalName": null,
"mimeType": "image/jpeg", "mimeType": "image/jpeg",

View File

@@ -21,8 +21,8 @@
"backgroundImage": { "backgroundImage": {
"id": "asset_bg_001", "id": "asset_bg_001",
"sourceType": "ai", "sourceType": "ai",
"url": "/uploads/posters/poster_sagra_2025_bg.jpg", "url": "/upload/posters/poster_sagra_2025_bg.jpg",
"thumbnailUrl": "/uploads/posters/thumbs/poster_sagra_2025_bg_thumb.jpg", "thumbnailUrl": "/upload/posters/thumbs/poster_sagra_2025_bg_thumb.jpg",
"mimeType": "image/jpeg", "mimeType": "image/jpeg",
"size": 2458000, "size": 2458000,
"dimensions": { "width": 2480, "height": 3508 }, "dimensions": { "width": 2480, "height": 3508 },
@@ -41,8 +41,8 @@
"mainImage": { "mainImage": {
"id": "asset_main_001", "id": "asset_main_001",
"sourceType": "upload", "sourceType": "upload",
"url": "/uploads/assets/porcini_basket_hero.jpg", "url": "/upload/assets/porcini_basket_hero.jpg",
"thumbnailUrl": "/uploads/assets/thumbs/porcini_basket_hero_thumb.jpg", "thumbnailUrl": "/upload/assets/thumbs/porcini_basket_hero_thumb.jpg",
"originalName": "IMG_20241015_porcini.jpg", "originalName": "IMG_20241015_porcini.jpg",
"mimeType": "image/jpeg", "mimeType": "image/jpeg",
"size": 1845000, "size": 1845000,
@@ -54,7 +54,7 @@
"id": "asset_logo_001", "id": "asset_logo_001",
"slotId": "logo_slot_1", "slotId": "logo_slot_1",
"sourceType": "upload", "sourceType": "upload",
"url": "/uploads/logos/comune_borgomontano.png", "url": "/upload/logos/comune_borgomontano.png",
"originalName": "logo_comune.png", "originalName": "logo_comune.png",
"mimeType": "image/png", "mimeType": "image/png",
"size": 45000 "size": 45000
@@ -63,7 +63,7 @@
"id": "asset_logo_002", "id": "asset_logo_002",
"slotId": "logo_slot_2", "slotId": "logo_slot_2",
"sourceType": "upload", "sourceType": "upload",
"url": "/uploads/logos/proloco_borgomontano.png", "url": "/upload/logos/proloco_borgomontano.png",
"originalName": "logo_proloco.png", "originalName": "logo_proloco.png",
"mimeType": "image/png", "mimeType": "image/png",
"size": 38000 "size": 38000
@@ -72,7 +72,7 @@
"id": "asset_logo_003", "id": "asset_logo_003",
"slotId": "logo_slot_3", "slotId": "logo_slot_3",
"sourceType": "ai", "sourceType": "ai",
"url": "/uploads/logos/ai_generated_mushroom_logo.png", "url": "/upload/logos/ai_generated_mushroom_logo.png",
"mimeType": "image/png", "mimeType": "image/png",
"size": 52000, "size": 52000,
"aiParams": { "aiParams": {
@@ -100,12 +100,12 @@
"renderOutput": { "renderOutput": {
"png": { "png": {
"path": "/uploads/posters/final/poster_sagra_2025_final.png", "path": "/upload/posters/final/poster_sagra_2025_final.png",
"size": 8945000, "size": 8945000,
"url": "/api/posters/poster_sagra_funghi_2025_001/download/png" "url": "/api/posters/poster_sagra_funghi_2025_001/download/png"
}, },
"jpg": { "jpg": {
"path": "/uploads/posters/final/poster_sagra_2025_final.jpg", "path": "/upload/posters/final/poster_sagra_2025_final.jpg",
"quality": 95, "quality": 95,
"size": 2145000, "size": 2145000,
"url": "/api/posters/poster_sagra_funghi_2025_001/download/jpg" "url": "/api/posters/poster_sagra_funghi_2025_001/download/jpg"

View File

@@ -2,7 +2,7 @@ const multer = require('multer');
const path = require('path'); const path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'; const UPLOAD_DIR = process.env.UPLOAD_DIR || './upload';
const storage = multer.diskStorage({ const storage = multer.diskStorage({
destination: (req, file, cb) => { destination: (req, file, cb) => {

View File

@@ -1,86 +1,94 @@
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const Schema = mongoose.Schema; const Schema = mongoose.Schema;
const LastMessageSchema = new Schema({ const LastMessageSchema = new Schema(
text: { {
text: {
type: String,
trim: true,
},
senderId: {
type: Schema.Types.ObjectId,
ref: 'User',
},
timestamp: {
type: Date,
default: Date.now,
},
type: String, type: String,
trim: true
}, },
senderId: { { _id: false }
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({ const ChatSchema = new Schema(
idapp: { {
type: String, idapp: {
required: true, type: String,
index: true 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,
},
}, },
participants: [{ {
type: Schema.Types.ObjectId, timestamps: true,
ref: 'User', toJSON: { virtuals: true },
required: true toObject: { virtuals: 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 // Indici
ChatSchema.index({ participants: 1 }); ChatSchema.index({ participants: 1 });
@@ -88,71 +96,89 @@ ChatSchema.index({ idapp: 1, participants: 1 });
ChatSchema.index({ idapp: 1, updatedAt: -1 }); ChatSchema.index({ idapp: 1, updatedAt: -1 });
// Virtual per contare messaggi non letti totali // Virtual per contare messaggi non letti totali
ChatSchema.virtual('totalUnread').get(function() { ChatSchema.virtual('totalUnread').get(function () {
if (!this.unreadCount) return 0; if (!this.unreadCount) return 0;
let total = 0; let total = 0;
this.unreadCount.forEach(count => { this.unreadCount.forEach((count) => {
total += count; total += count;
}); });
return total; return total;
}); });
// Metodo per ottenere unread count per un utente specifico // Metodo per ottenere unread count per un utente specifico
ChatSchema.methods.getUnreadForUser = function(userId) { ChatSchema.methods.getUnreadForUser = function (userId) {
if (!this.unreadCount) return 0; if (!this.unreadCount) return 0;
return this.unreadCount.get(userId.toString()) || 0; return this.unreadCount.get(userId.toString()) || 0;
}; };
// Metodo per incrementare unread count // ✅ FIX: incrementUnread (assicura conversione corretta)
ChatSchema.methods.incrementUnread = function(excludeUserId) { ChatSchema.methods.incrementUnread = function (excludeUserId) {
this.participants.forEach(participantId => { const excludeIdStr = excludeUserId.toString();
const id = participantId.toString();
if (id !== excludeUserId.toString()) { this.participants.forEach((participantId) => {
// Gestisci sia ObjectId che oggetti popolati
const id = participantId._id
? participantId._id.toString()
: participantId.toString();
if (id !== excludeIdStr) {
const current = this.unreadCount.get(id) || 0; const current = this.unreadCount.get(id) || 0;
this.unreadCount.set(id, current + 1); this.unreadCount.set(id, current + 1);
} }
}); });
return this.save(); return this.save();
}; };
// Metodo per resettare unread count per un utente // Metodo per resettare unread count per un utente
ChatSchema.methods.markAsRead = function(userId) { ChatSchema.methods.markAsRead = function (userId) {
this.unreadCount.set(userId.toString(), 0); this.unreadCount.set(userId.toString(), 0);
return this.save(); return this.save();
}; };
// Metodo per aggiornare ultimo messaggio // Metodo per aggiornare ultimo messaggio
ChatSchema.methods.updateLastMessage = function(message) { ChatSchema.methods.updateLastMessage = function (message) {
this.lastMessage = { this.lastMessage = {
text: message.text, text: message.text,
senderId: message.senderId, senderId: message.senderId,
timestamp: message.createdAt || new Date(), timestamp: message.createdAt || new Date(),
type: message.type || 'text' type: message.type || 'text',
}; };
return this.save(); return this.save();
}; };
// Metodo per verificare se un utente è partecipante // Metodo per verificare se un utente è partecipante
ChatSchema.methods.hasParticipant = function(userId) { // ✅ FIX: Gestisce sia ObjectId che oggetti User popolati
return this.participants.some( ChatSchema.methods.hasParticipant = function (userId) {
p => p.toString() === userId.toString() const userIdStr = userId.toString();
);
return this.participants.some((p) => {
// Se p è un oggetto popolato (ha _id), usa p._id
// Altrimenti p è già un ObjectId
const participantId = p._id ? p._id.toString() : p.toString();
return participantId === userIdStr;
});
}; };
// Metodo per verificare se la chat è bloccata per un utente // Metodo per verificare se la chat è bloccata per un utente
ChatSchema.methods.isBlockedFor = function(userId) { // ✅ FIX: Metodo isBlockedFor (stesso problema)
return this.blockedBy.some( ChatSchema.methods.isBlockedFor = function (userId) {
id => id.toString() === userId.toString() const userIdStr = userId.toString();
);
return this.blockedBy.some((id) => {
const blockedId = id._id ? id._id.toString() : id.toString();
return blockedId === userIdStr;
});
}; };
// Metodo statico per trovare o creare una chat diretta // Metodo statico per trovare o creare una chat diretta
ChatSchema.statics.findOrCreateDirect = async function(idapp, userId1, userId2, rideId = null) { ChatSchema.statics.findOrCreateDirect = async function (idapp, userId1, userId2, rideId = null) {
// Cerca chat esistente tra i due utenti // Cerca chat esistente tra i due utenti
let chat = await this.findOne({ let chat = await this.findOne({
idapp, idapp,
type: 'direct', type: 'direct',
participants: { $all: [userId1, userId2], $size: 2 } participants: { $all: [userId1, userId2], $size: 2 },
}); });
if (!chat) { if (!chat) {
@@ -161,7 +187,7 @@ ChatSchema.statics.findOrCreateDirect = async function(idapp, userId1, userId2,
type: 'direct', type: 'direct',
participants: [userId1, userId2], participants: [userId1, userId2],
rideId, rideId,
unreadCount: new Map() unreadCount: new Map(),
}); });
await chat.save(); await chat.save();
} else if (rideId && !chat.rideId) { } else if (rideId && !chat.rideId) {
@@ -174,31 +200,31 @@ ChatSchema.statics.findOrCreateDirect = async function(idapp, userId1, userId2,
}; };
// Metodo statico per ottenere tutte le chat di un utente // Metodo statico per ottenere tutte le chat di un utente
ChatSchema.statics.getChatsForUser = function(idapp, userId) { ChatSchema.statics.getChatsForUser = function (idapp, userId) {
return this.find({ return this.find({
idapp, idapp,
participants: userId, participants: userId,
isActive: true, isActive: true,
blockedBy: { $ne: userId } blockedBy: { $ne: userId },
}) })
.populate('participants', 'username name surname profile.avatar') .populate('participants', 'username name surname profile.avatar')
.populate('rideId', 'departure destination dateTime') .populate('rideId', 'departure destination dateTime')
.sort({ updatedAt: -1 }); .sort({ updatedAt: -1 });
}; };
// Metodo statico per creare chat di gruppo per un viaggio // Metodo statico per creare chat di gruppo per un viaggio
ChatSchema.statics.createRideGroupChat = async function(idapp, rideId, title, participantIds) { ChatSchema.statics.createRideGroupChat = async function (idapp, rideId, title, participantIds) {
const chat = new this({ const chat = new this({
idapp, idapp,
type: 'group', type: 'group',
rideId, rideId,
title, title,
participants: participantIds, participants: participantIds,
unreadCount: new Map() unreadCount: new Map(),
}); });
return chat.save(); return chat.save();
}; };
const Chat = mongoose.model('Chat', ChatSchema); const Chat = mongoose.model('Chat', ChatSchema);
module.exports = Chat; module.exports = Chat;

View File

@@ -168,28 +168,31 @@ MessageSchema.methods.editText = function(newText) {
}; };
// Metodo statico per ottenere messaggi di una chat con paginazione // Metodo statico per ottenere messaggi di una chat con paginazione
MessageSchema.statics.getByChat = function(idapp, chatId, options = {}) { // Message.js (model)
const { limit = 50, before = null, after = null } = options;
MessageSchema.statics.getByChat = async function(idapp, chatId, options = {}) {
const { limit = 50, before, after } = options;
const query = { const query = {
idapp, idapp,
chatId, chatId,
isDeleted: false isDeleted: false
}; };
// Filtra per timestamp
if (before) { if (before) {
query.createdAt = { $lt: new Date(before) }; query.createdAt = { $lt: new Date(before) };
} }
if (after) { if (after) {
query.createdAt = { $gt: new Date(after) }; query.createdAt = { $gt: new Date(after) };
} }
// ✅ Sempre in ordine decrescente (dal più recente al più vecchio)
return this.find(query) return this.find(query)
.populate('senderId', 'username name surname profile.avatar') .populate('senderId', 'username name surname profile.img')
.populate('replyTo', 'text senderId') .populate('replyTo', 'text senderId')
.populate('metadata.rideId', 'departure destination dateTime') .sort({ createdAt: -1 }) // -1 = più recente prima
.sort({ createdAt: -1 }) .limit(limit)
.limit(limit);
}; };
// Metodo statico per creare messaggio di sistema // Metodo statico per creare messaggio di sistema

View File

@@ -2,377 +2,417 @@ const mongoose = require('mongoose');
const Schema = mongoose.Schema; const Schema = mongoose.Schema;
// Schema per le coordinate geografiche // Schema per le coordinate geografiche
const CoordinatesSchema = new Schema({ const CoordinatesSchema = new Schema(
lat: { {
type: Number, lat: {
required: true type: Number,
required: true,
},
lng: {
type: Number,
required: true,
},
}, },
lng: { { _id: false }
type: Number, );
required: true
}
}, { _id: false });
// Schema per una località (partenza, destinazione, waypoint) // Schema per una località (partenza, destinazione, waypoint)
const LocationSchema = new Schema({ const LocationSchema = new Schema(
city: { {
type: String, city: {
required: true, type: String,
trim: true 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,
},
}, },
address: { { _id: false }
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) // Schema per i waypoint (tappe intermedie)
const WaypointSchema = new Schema({ const WaypointSchema = new Schema({
location: { location: {
type: LocationSchema, type: LocationSchema,
required: true required: true,
}, },
order: { order: {
type: Number, type: Number,
required: true required: true,
}, },
estimatedArrival: { estimatedArrival: {
type: Date type: Date,
}, },
stopDuration: { stopDuration: {
type: Number, // minuti di sosta type: Number, // minuti di sosta
default: 0 default: 0,
} },
}); });
// Schema per la ricorrenza del viaggio // Schema per la ricorrenza del viaggio
const RecurrenceSchema = new Schema({ const RecurrenceSchema = new Schema(
type: { {
type: String, type: {
enum: ['once', 'weekly', 'custom_days', 'custom_dates'], type: String,
default: 'once' 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,
},
],
}, },
daysOfWeek: [{ { _id: false }
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 // Schema per i passeggeri
const PassengersSchema = new Schema({ const PassengersSchema = new Schema(
available: { {
type: Number, available: {
required: true, type: Number,
min: 0 required: true,
min: 0,
},
max: {
type: Number,
required: true,
min: 1,
},
}, },
max: { { _id: false }
type: Number, );
required: true,
min: 1
}
}, { _id: false });
// Schema per il veicolo // Schema per il veicolo
const VehicleSchema = new Schema({ const VehicleSchema = new Schema(
type: { {
type: String, type: {
enum: ['auto', 'moto', 'furgone', 'minibus', 'altro'], type: String,
default: 'auto' 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,
},
photos: [
{
type: String,
trim: true,
},
],
features: [
{
type: String,
enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi'],
},
],
}, },
brand: { { _id: false }
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 // Schema per le preferenze di viaggio
const RidePreferencesSchema = new Schema({ const RidePreferencesSchema = new Schema(
smoking: { {
type: String, smoking: {
enum: ['yes', 'no', 'outside_only'], type: String,
default: 'no' 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,
},
}, },
pets: { { _id: false }
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 // Schema per il contributo/pagamento
const ContributionItemSchema = new Schema({ const ContributionItemSchema = new Schema({
contribTypeId: { contribTypeId: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Contribtype', ref: 'Contribtype',
required: true required: true,
}, },
price: { price: {
type: Number, type: Number,
min: 0 min: 0,
}, },
pricePerKm: { pricePerKm: {
type: Number, 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, 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: { notes: {
type: String, type: String,
trim: true, trim: true,
maxlength: 1000
}, },
cancellationReason: {
type: String,
trim: true
},
cancelledAt: {
type: Date
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: 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 // Indici per ricerche ottimizzate
RideSchema.index({ 'departure.city': 1, 'destination.city': 1 }); RideSchema.index({ 'departure.city': 1, 'destination.city': 1 });
RideSchema.index({ 'departure.coordinates': '2dsphere' }); RideSchema.index({ 'departure.coordinates': '2dsphere' });
@@ -382,41 +422,41 @@ RideSchema.index({ dateTime: 1, status: 1 });
RideSchema.index({ idapp: 1, status: 1, dateTime: 1 }); RideSchema.index({ idapp: 1, status: 1, dateTime: 1 });
// Virtual per verificare se il viaggio è pieno // Virtual per verificare se il viaggio è pieno
RideSchema.virtual('isFull').get(function() { RideSchema.virtual('isFull').get(function () {
if (this.type === 'request') return false; if (this.type === 'request') return false;
// ⚠️ CONTROLLO: verifica che passengers esista
if (!this.passengers || typeof this.passengers.available === 'undefined') return false;
return this.passengers.available <= 0; return this.passengers.available <= 0;
}); });
// Virtual per calcolare posti occupati // Virtual per calcolare posti occupati
RideSchema.virtual('bookedSeats').get(function() { RideSchema.virtual('bookedSeats').get(function () {
if (!this.confirmedPassengers) return 0; if (!this.confirmedPassengers) return 0;
return this.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0); return this.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0);
}); });
// Virtual per ottenere tutte le città del percorso // Virtual per ottenere tutte le città del percorso
RideSchema.virtual('allCities').get(function() { RideSchema.virtual('allCities').get(function () {
const cities = [this.departure.city]; const cities = [this.departure.city];
if (this.waypoints && this.waypoints.length > 0) { if (this.waypoints && this.waypoints.length > 0) {
this.waypoints this.waypoints.sort((a, b) => a.order - b.order).forEach((wp) => cities.push(wp.location.city));
.sort((a, b) => a.order - b.order)
.forEach(wp => cities.push(wp.location.city));
} }
cities.push(this.destination.city); cities.push(this.destination.city);
return cities; return cities;
}); });
// Metodo per verificare se passa per una città // Metodo per verificare se passa per una città
RideSchema.methods.passesThrough = function(cityName) { RideSchema.methods.passesThrough = function (cityName) {
const normalizedCity = cityName.toLowerCase().trim(); const normalizedCity = cityName.toLowerCase().trim();
return this.allCities.some(city => return this.allCities.some(
city.toLowerCase().trim().includes(normalizedCity) || (city) => city.toLowerCase().trim().includes(normalizedCity) || normalizedCity.includes(city.toLowerCase().trim())
normalizedCity.includes(city.toLowerCase().trim())
); );
}; };
// Metodo per aggiornare posti disponibili // Metodo per aggiornare posti disponibili
RideSchema.methods.updateAvailableSeats = function() { RideSchema.methods.updateAvailableSeats = function () {
if (this.type === 'offer') { // ⚠️ CONTROLLO: verifica che sia un'offerta e che passengers esista
if (this.type === 'offer' && this.passengers) {
const booked = this.bookedSeats; const booked = this.bookedSeats;
this.passengers.available = this.passengers.max - booked; this.passengers.available = this.passengers.max - booked;
if (this.passengers.available <= 0) { if (this.passengers.available <= 0) {
@@ -429,9 +469,9 @@ RideSchema.methods.updateAvailableSeats = function() {
}; };
// Pre-save hook // Pre-save hook
RideSchema.pre('save', function(next) { RideSchema.pre('save', function (next) {
// Aggiorna posti disponibili se necessario // ⚠️ CONTROLLO: Aggiorna posti disponibili solo se è un'offerta e passengers esiste
if (this.type === 'offer' && this.isModified('confirmedPassengers')) { if (this.type === 'offer' && this.passengers && this.isModified('confirmedPassengers')) {
const booked = this.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0); const booked = this.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0);
this.passengers.available = this.passengers.max - booked; this.passengers.available = this.passengers.max - booked;
if (this.passengers.available <= 0) { if (this.passengers.available <= 0) {
@@ -442,11 +482,11 @@ RideSchema.pre('save', function(next) {
}); });
// Metodi statici per ricerche comuni // Metodi statici per ricerche comuni
RideSchema.statics.findActiveByCity = function(idapp, departureCity, destinationCity, options = {}) { RideSchema.statics.findActiveByCity = function (idapp, departureCity, destinationCity, options = {}) {
const query = { const query = {
idapp, idapp,
status: { $in: ['active', 'full'] }, status: { $in: ['active', 'full'] },
dateTime: { $gte: new Date() } dateTime: { $gte: new Date() },
}; };
if (departureCity) { if (departureCity) {
@@ -472,17 +512,13 @@ RideSchema.statics.findActiveByCity = function(idapp, departureCity, destination
}; };
// Ricerca viaggi che passano per una città intermedia // Ricerca viaggi che passano per una città intermedia
RideSchema.statics.findPassingThrough = function(idapp, cityName, options = {}) { RideSchema.statics.findPassingThrough = function (idapp, cityName, options = {}) {
const cityRegex = new RegExp(cityName, 'i'); const cityRegex = new RegExp(cityName, 'i');
const query = { const query = {
idapp, idapp,
status: { $in: ['active'] }, status: { $in: ['active'] },
dateTime: { $gte: new Date() }, dateTime: { $gte: new Date() },
$or: [ $or: [{ 'departure.city': cityRegex }, { 'destination.city': cityRegex }, { 'waypoints.location.city': cityRegex }],
{ 'departure.city': cityRegex },
{ 'destination.city': cityRegex },
{ 'waypoints.location.city': cityRegex }
]
}; };
if (options.type) { if (options.type) {

View File

@@ -643,6 +643,12 @@ const UserSchema = new mongoose.Schema(
enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi'], enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi'],
}, },
], ],
photos: [
{
type: String,
trim: true,
},
],
isDefault: { isDefault: {
type: Boolean, type: Boolean,
default: false, default: false,
@@ -7336,7 +7342,6 @@ const FuncUsers = {
UserSchema.index({ 'tokens.token': 1, 'tokens.access': 1, idapp: 1, deleted: 1, updatedAt: 1 }); UserSchema.index({ 'tokens.token': 1, 'tokens.access': 1, idapp: 1, deleted: 1, updatedAt: 1 });
const User = mongoose.models.User || mongoose.model('User', UserSchema); const User = mongoose.models.User || mongoose.model('User', UserSchema);
module.exports = { module.exports = {

View File

@@ -1,7 +1,13 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const path = require('path');
const { Contribtype } = require('../models/Contribtype'); // Adatta al tuo path const multer = require('multer');
const fs = require('fs');
const tools = require('../tools/general');
const { Contribtype } = require('../models/contribtype'); // Adatta al tuo path
// Import Controllers // Import Controllers
const rideController = require('../controllers/rideController'); const rideController = require('../controllers/rideController');
@@ -113,7 +119,6 @@ router.get('/stats/summary', authenticate, rideController.getStatsSummary);
*/ */
router.get('/cities/suggestions', rideController.getCitySuggestions); router.get('/cities/suggestions', rideController.getCitySuggestions);
/** /**
* @route GET /api/trasporti/cities/recents * @route GET /api/trasporti/cities/recents
* @desc città recenti per autocomplete * @desc città recenti per autocomplete
@@ -258,6 +263,13 @@ router.put('/chats/:id/mute', authenticate, chatController.toggleMuteChat);
*/ */
router.delete('/chats/:chatId/messages/:messageId', authenticate, chatController.deleteMessage); router.delete('/chats/:chatId/messages/:messageId', authenticate, chatController.deleteMessage);
/**
* @route DELETE /api/trasporti/chats/:id
* @desc Elimina chat (soft delete)
* @access Private (solo partecipanti)
*/
router.delete('/chats/:id', authenticate, chatController.deleteChat);
// ============================================================ // ============================================================
// ⭐ FEEDBACK - Recensioni // ⭐ FEEDBACK - Recensioni
// ============================================================ // ============================================================
@@ -390,16 +402,16 @@ router.get('/geo/suggest-waypoints', geocodingController.suggestWaypoints);
// ============================================================ // ============================================================
/** /**
* @route GET /api/trasporti/driver/:userId * @route GET /api/trasporti/driver/user/:userId
* @desc Profilo pubblico del conducente * @desc Profilo pubblico del conducente
* @access Public * @access Public
*/ */
router.get('/driver/:userId', async (req, res) => { router.get('/driver/user/:userId', async (req, res) => {
try { try {
const { userId } = req.params; const { userId } = req.params;
const { idapp } = req.query; const { idapp } = req.query;
const { User } = require('../models/User'); const { User } = require('../models/user');
const Ride = require('../models/Ride'); const Ride = require('../models/Ride');
const Feedback = require('../models/Feedback'); const Feedback = require('../models/Feedback');
@@ -489,6 +501,43 @@ router.get('/driver/:userId', async (req, res) => {
} }
}); });
/**
* @route GET /api/trasporti/driver/vehicles
* @desc Ottieni veicoli dell'utente corrente
* @access Private
*/
router.get('/driver/vehicles', authenticate, async (req, res) => {
try {
const { idapp } = req.query;
const userId = req.user._id; // Assumo che ci sia un middleware di autenticazione
if (!userId) {
return res.status(401).json({
success: false,
message: 'Autenticazione richiesta',
});
}
const { User } = require('../models/user');
const user = await User.findById(userId).select('profile.driverProfile.vehicles');
const vehicles = user?.profile?.driverProfile?.vehicles || [];
res.status(200).json({
success: true,
data: vehicles,
});
} catch (error) {
console.error('Errore recupero veicoli:', error);
res.status(500).json({
success: false,
message: 'Errore durante il recupero dei veicoli',
error: error.message,
});
}
});
/** /**
* @route PUT /api/trasporti/driver/profile * @route PUT /api/trasporti/driver/profile
* @desc Aggiorna profilo conducente * @desc Aggiorna profilo conducente
@@ -499,7 +548,7 @@ router.put('/driver/profile', authenticate, async (req, res) => {
const userId = req.user._id; const userId = req.user._id;
const { idapp, driverProfile, preferences } = req.body; const { idapp, driverProfile, preferences } = req.body;
const { User } = require('../models/User'); const { User } = require('../models/user');
const updateData = {}; const updateData = {};
@@ -543,9 +592,9 @@ router.put('/driver/profile', authenticate, async (req, res) => {
router.post('/driver/vehicles', authenticate, async (req, res) => { router.post('/driver/vehicles', authenticate, async (req, res) => {
try { try {
const userId = req.user._id; const userId = req.user._id;
const { vehicle } = req.body; const vehicle = req.body;
const { User } = require('../models/User'); const { User } = require('../models/user');
const user = await User.findByIdAndUpdate( const user = await User.findByIdAndUpdate(
userId, userId,
@@ -580,9 +629,9 @@ router.put('/driver/vehicles/:vehicleId', authenticate, async (req, res) => {
try { try {
const userId = req.user._id; const userId = req.user._id;
const { vehicleId } = req.params; const { vehicleId } = req.params;
const { vehicle } = req.body; const vehicle = req.body;
const { User } = require('../models/User'); const { User } = require('../models/user');
const user = await User.findOneAndUpdate( const user = await User.findOneAndUpdate(
{ {
@@ -617,6 +666,54 @@ router.put('/driver/vehicles/:vehicleId', authenticate, async (req, res) => {
} }
}); });
/**
* @route GET /api/trasporti/driver/vehicles/:vehicleId
* @desc Ottieni dettagli di un veicolo specifico
* @access Private
*/
router.get('/driver/vehicles/:vehicleId', authenticate, async (req, res) => {
try {
const userId = req.user._id;
const { vehicleId } = req.params;
const { User } = require('../models/user');
const user = await User.findOne({
_id: userId,
'profile.driverProfile.vehicles._id': vehicleId,
}).select('profile.driverProfile.vehicles');
if (!user) {
return res.status(404).json({
success: false,
message: 'Veicolo non trovato',
});
}
// Trova il veicolo specifico nell'array
const vehicle = user.profile.driverProfile.vehicles.find((v) => v._id.toString() === vehicleId);
if (!vehicle) {
return res.status(404).json({
success: false,
message: 'Veicolo non trovato',
});
}
res.status(200).json({
success: true,
data: vehicle,
});
} catch (error) {
console.error('Errore recupero veicolo:', error);
res.status(500).json({
success: false,
message: 'Errore durante il recupero del veicolo',
error: error.message,
});
}
});
/** /**
* @route DELETE /api/trasporti/driver/vehicles/:vehicleId * @route DELETE /api/trasporti/driver/vehicles/:vehicleId
* @desc Rimuovi veicolo * @desc Rimuovi veicolo
@@ -627,7 +724,7 @@ router.delete('/driver/vehicles/:vehicleId', authenticate, async (req, res) => {
const userId = req.user._id; const userId = req.user._id;
const { vehicleId } = req.params; const { vehicleId } = req.params;
const { User } = require('../models/User'); const { User } = require('../models/user');
await User.findByIdAndUpdate(userId, { await User.findByIdAndUpdate(userId, {
$pull: { 'profile.driverProfile.vehicles': { _id: vehicleId } }, $pull: { 'profile.driverProfile.vehicles': { _id: vehicleId } },
@@ -657,7 +754,7 @@ router.post('/driver/vehicles/:vehicleId/default', authenticate, async (req, res
const userId = req.user._id; const userId = req.user._id;
const { vehicleId } = req.params; const { vehicleId } = req.params;
const { User } = require('../models/User'); const { User } = require('../models/user');
// Prima rimuovi isDefault da tutti // Prima rimuovi isDefault da tutti
await User.updateOne({ _id: userId }, { $set: { 'profile.driverProfile.vehicles.$[].isDefault': false } }); await User.updateOne({ _id: userId }, { $set: { 'profile.driverProfile.vehicles.$[].isDefault': false } });
@@ -711,4 +808,211 @@ router.get('/contrib-types', async (req, res) => {
} }
}); });
const vehiclePhotoStorage = multer.diskStorage({
destination: (req, file, cb) => {
const webServerDir = tools.getdirByIdApp(req.user.idapp) + '/upload/vehicles';
if (!fs.existsSync(webServerDir)) {
fs.mkdirSync(webServerDir, { recursive: true });
}
console.log('📁 Destinazione foto veicolo:', webServerDir);
cb(null, webServerDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = path.extname(file.originalname);
const filename = 'vehicle-' + uniqueSuffix + ext;
console.log('📝 Nome file generato:', filename);
cb(null, filename);
},
});
const uploadVehiclePhoto = multer({
storage: vehiclePhotoStorage,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
},
fileFilter: (req, file, cb) => {
console.log('🔍 File ricevuto:', {
fieldname: file.fieldname,
originalname: file.originalname,
mimetype: file.mimetype,
});
const allowedMimes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
if (allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Solo immagini sono ammesse'), false);
}
},
});
/**
* @route POST /api/trasporti/upload/vehicle-photos
* @desc Upload multiple foto veicolo (max 5)
* @access Private
*/
router.post(
'/upload/vehicle-photos',
authenticate,
(req, res, next) => {
console.log('🚀 Request ricevuta per upload multiple foto veicolo');
next();
},
uploadVehiclePhoto.array('photos', 5),
(err, req, res, next) => {
if (err) {
console.error('❌ Errore multer:', err);
return res.status(400).json({
success: false,
message: err.message || 'Errore durante upload',
});
}
next();
},
async (req, res) => {
try {
console.log('✅ Files ricevuti:', req.files);
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
message: 'Nessuna foto caricata',
});
}
const photoUrls = req.files.map((file) => `/upload/vehicles/${file.filename}`);
res.status(200).json({
success: true,
message: `${req.files.length} foto caricate con successo`,
data: {
urls: photoUrls,
count: req.files.length,
},
});
} catch (error) {
console.error('❌ Errore upload foto veicolo:', error);
// Elimina tutti i file in caso di errore
if (req.files && req.files.length > 0) {
req.files.forEach((file) => {
if (file.path) {
fs.unlinkSync(file.path);
}
});
}
res.status(500).json({
success: false,
message: "Errore durante l'upload delle foto",
error: error.message,
});
}
}
);
/**
* @route POST /api/trasporti/upload/vehicle-photo
* @desc Upload foto veicolo
* @access Private
*/
router.post(
'/upload/vehicle-photo',
authenticate,
(req, res, next) => {
console.log('🚀 Request ricevuta per upload foto veicolo');
console.log('📋 Headers:', req.headers);
console.log('📦 Body type:', typeof req.body);
next();
},
uploadVehiclePhoto.single('photo'),
(err, req, res, next) => {
// Gestione errori multer
if (err) {
console.error('❌ Errore multer:', err);
return res.status(400).json({
success: false,
message: err.message || 'Errore durante upload',
});
}
next();
},
async (req, res) => {
try {
console.log('✅ File ricevuto:', req.file);
console.log('📄 Body:', req.body);
if (!req.file) {
return res.status(400).json({
success: false,
message: 'Nessuna foto caricata - req.file è undefined',
});
}
const photoUrl = `/upload/vehicles/${req.file.filename}`;
res.status(200).json({
success: true,
message: 'Foto caricata con successo',
data: {
url: photoUrl,
filename: req.file.filename,
},
});
} catch (error) {
console.error('❌ Errore upload foto veicolo:', error);
if (req.file && req.file.path) {
fs.unlinkSync(req.file.path);
}
res.status(500).json({
success: false,
message: "Errore durante l'upload della foto",
error: error.message,
});
}
}
);
/**
* @route DELETE /api/trasporti/upload/vehicle-photo
* @desc Elimina foto veicolo
* @access Private
*/
router.delete('/upload/vehicle-photo', authenticate, async (req, res) => {
try {
const { photoUrl } = req.body;
if (!photoUrl) {
return res.status(400).json({
success: false,
message: 'URL foto non fornito',
});
}
// Estrai il filename dall'URL
const filename = photoUrl.split('/').pop();
const filepath = path.join(vehiclesDir, filename);
// Verifica che il file esista
if (fs.existsSync(filepath)) {
fs.unlinkSync(filepath);
}
res.status(200).json({
success: true,
message: 'Foto eliminata con successo',
});
} catch (error) {
console.error('❌ Errore eliminazione foto:', error);
res.status(500).json({
success: false,
message: "Errore durante l'eliminazione della foto",
error: error.message,
});
}
});
module.exports = router; module.exports = router;

View File

@@ -709,7 +709,7 @@ class PosterRenderer {
} }
// Path locale // Path locale
if (url.startsWith('/uploads') || url.startsWith('./uploads')) { if (url.startsWith('/upload') || url.startsWith('./upload')) {
const localPath = url.startsWith('/') const localPath = url.startsWith('/')
? path.join(process.cwd(), url) ? path.join(process.cwd(), url)
: url; : url;