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

View File

@@ -1,6 +1,6 @@
const Chat = require('../models/Chat');
const Message = require('../models/Message');
const { User } = require('../models/User');
const { User } = require('../models/user');
/**
* @desc Ottieni tutte le chat dell'utente
@@ -10,7 +10,8 @@ const { User } = require('../models/User');
const getMyChats = async (req, res) => {
try {
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) {
return res.status(400).json({
@@ -83,7 +84,8 @@ const getMyChats = async (req, res) => {
const getOrCreateDirectChat = async (req, res) => {
try {
const userId = req.user._id;
const { idapp, otherUserId, rideId } = req.body;
const idapp = req.user.idapp;
const { otherUserId, rideId } = req.body;
if (!idapp || !otherUserId) {
return res.status(400).json({
@@ -199,7 +201,8 @@ const getChatById = async (req, res) => {
const getChatMessages = async (req, res) => {
try {
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;
// Verifica accesso alla chat
@@ -209,7 +212,7 @@ const getChatMessages = async (req, res) => {
success: false,
message: 'Chat non trovata'
});
}
}
if (!chat.hasParticipant(userId)) {
return res.status(403).json({
@@ -258,7 +261,8 @@ const getChatMessages = async (req, res) => {
const sendMessage = async (req, res) => {
try {
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;
if (!idapp || !text) {
@@ -499,7 +503,7 @@ const toggleMuteChat = async (req, res) => {
const getUnreadCount = async (req, res) => {
try {
const userId = req.user._id;
const { idapp } = req.query;
const idapp = req.user.idapp;
if (!idapp) {
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 = {
getMyChats,
getOrCreateDirectChat,
@@ -607,5 +672,6 @@ module.exports = {
toggleBlockChat,
toggleMuteChat,
getUnreadCount,
deleteMessage
deleteMessage,
deleteChat,
};

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
const Ride = require('../models/Ride');
const User = require('../models/User');
const User = require('../models/user');
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 = {
createRequest,
getRequestsForRide,
@@ -611,6 +762,6 @@ module.exports = {
rejectRequest,
cancelRequest,
getRequestById,
getReceivedRequests: getPendingRequests,
getSentRequests: getMyRequests,
getReceivedRequests,
getSentRequests,
};

View File

@@ -5,9 +5,9 @@
"sourceType": "ai",
"file": {
"path": "/uploads/assets/backgrounds/forest_autumn_001.jpg",
"path": "/upload/assets/backgrounds/forest_autumn_001.jpg",
"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",
"originalName": null,
"mimeType": "image/jpeg",

View File

@@ -21,8 +21,8 @@
"backgroundImage": {
"id": "asset_bg_001",
"sourceType": "ai",
"url": "/uploads/posters/poster_sagra_2025_bg.jpg",
"thumbnailUrl": "/uploads/posters/thumbs/poster_sagra_2025_bg_thumb.jpg",
"url": "/upload/posters/poster_sagra_2025_bg.jpg",
"thumbnailUrl": "/upload/posters/thumbs/poster_sagra_2025_bg_thumb.jpg",
"mimeType": "image/jpeg",
"size": 2458000,
"dimensions": { "width": 2480, "height": 3508 },
@@ -41,8 +41,8 @@
"mainImage": {
"id": "asset_main_001",
"sourceType": "upload",
"url": "/uploads/assets/porcini_basket_hero.jpg",
"thumbnailUrl": "/uploads/assets/thumbs/porcini_basket_hero_thumb.jpg",
"url": "/upload/assets/porcini_basket_hero.jpg",
"thumbnailUrl": "/upload/assets/thumbs/porcini_basket_hero_thumb.jpg",
"originalName": "IMG_20241015_porcini.jpg",
"mimeType": "image/jpeg",
"size": 1845000,
@@ -54,7 +54,7 @@
"id": "asset_logo_001",
"slotId": "logo_slot_1",
"sourceType": "upload",
"url": "/uploads/logos/comune_borgomontano.png",
"url": "/upload/logos/comune_borgomontano.png",
"originalName": "logo_comune.png",
"mimeType": "image/png",
"size": 45000
@@ -63,7 +63,7 @@
"id": "asset_logo_002",
"slotId": "logo_slot_2",
"sourceType": "upload",
"url": "/uploads/logos/proloco_borgomontano.png",
"url": "/upload/logos/proloco_borgomontano.png",
"originalName": "logo_proloco.png",
"mimeType": "image/png",
"size": 38000
@@ -72,7 +72,7 @@
"id": "asset_logo_003",
"slotId": "logo_slot_3",
"sourceType": "ai",
"url": "/uploads/logos/ai_generated_mushroom_logo.png",
"url": "/upload/logos/ai_generated_mushroom_logo.png",
"mimeType": "image/png",
"size": 52000,
"aiParams": {
@@ -100,12 +100,12 @@
"renderOutput": {
"png": {
"path": "/uploads/posters/final/poster_sagra_2025_final.png",
"path": "/upload/posters/final/poster_sagra_2025_final.png",
"size": 8945000,
"url": "/api/posters/poster_sagra_funghi_2025_001/download/png"
},
"jpg": {
"path": "/uploads/posters/final/poster_sagra_2025_final.jpg",
"path": "/upload/posters/final/poster_sagra_2025_final.jpg",
"quality": 95,
"size": 2145000,
"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 crypto = require('crypto');
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
const UPLOAD_DIR = process.env.UPLOAD_DIR || './upload';
const storage = multer.diskStorage({
destination: (req, file, cb) => {

View File

@@ -1,86 +1,94 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const LastMessageSchema = new Schema({
text: {
const LastMessageSchema = new Schema(
{
text: {
type: String,
trim: true,
},
senderId: {
type: Schema.Types.ObjectId,
ref: 'User',
},
timestamp: {
type: Date,
default: Date.now,
},
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 });
{ _id: false }
);
const ChatSchema = new Schema({
idapp: {
type: String,
required: true,
index: true
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,
},
},
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 },
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
);
// Indici
ChatSchema.index({ participants: 1 });
@@ -88,71 +96,89 @@ ChatSchema.index({ idapp: 1, participants: 1 });
ChatSchema.index({ idapp: 1, updatedAt: -1 });
// Virtual per contare messaggi non letti totali
ChatSchema.virtual('totalUnread').get(function() {
ChatSchema.virtual('totalUnread').get(function () {
if (!this.unreadCount) return 0;
let total = 0;
this.unreadCount.forEach(count => {
this.unreadCount.forEach((count) => {
total += count;
});
return total;
});
// Metodo per ottenere unread count per un utente specifico
ChatSchema.methods.getUnreadForUser = function(userId) {
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()) {
// ✅ FIX: incrementUnread (assicura conversione corretta)
ChatSchema.methods.incrementUnread = function (excludeUserId) {
const excludeIdStr = 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;
this.unreadCount.set(id, current + 1);
}
});
return this.save();
};
// Metodo per resettare unread count per un utente
ChatSchema.methods.markAsRead = function(userId) {
ChatSchema.methods.markAsRead = function (userId) {
this.unreadCount.set(userId.toString(), 0);
return this.save();
};
// Metodo per aggiornare ultimo messaggio
ChatSchema.methods.updateLastMessage = function(message) {
ChatSchema.methods.updateLastMessage = function (message) {
this.lastMessage = {
text: message.text,
senderId: message.senderId,
timestamp: message.createdAt || new Date(),
type: message.type || 'text'
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()
);
// ✅ FIX: Gestisce sia ObjectId che oggetti User popolati
ChatSchema.methods.hasParticipant = function (userId) {
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
ChatSchema.methods.isBlockedFor = function(userId) {
return this.blockedBy.some(
id => id.toString() === userId.toString()
);
// ✅ FIX: Metodo isBlockedFor (stesso problema)
ChatSchema.methods.isBlockedFor = function (userId) {
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
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
let chat = await this.findOne({
idapp,
type: 'direct',
participants: { $all: [userId1, userId2], $size: 2 }
participants: { $all: [userId1, userId2], $size: 2 },
});
if (!chat) {
@@ -161,7 +187,7 @@ ChatSchema.statics.findOrCreateDirect = async function(idapp, userId1, userId2,
type: 'direct',
participants: [userId1, userId2],
rideId,
unreadCount: new Map()
unreadCount: new Map(),
});
await chat.save();
} 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
ChatSchema.statics.getChatsForUser = function(idapp, userId) {
ChatSchema.statics.getChatsForUser = function (idapp, userId) {
return this.find({
idapp,
participants: userId,
isActive: true,
blockedBy: { $ne: userId }
blockedBy: { $ne: userId },
})
.populate('participants', 'username name surname profile.avatar')
.populate('rideId', 'departure destination dateTime')
.sort({ updatedAt: -1 });
.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) {
ChatSchema.statics.createRideGroupChat = async function (idapp, rideId, title, participantIds) {
const chat = new this({
idapp,
type: 'group',
rideId,
title,
participants: participantIds,
unreadCount: new Map()
unreadCount: new Map(),
});
return chat.save();
};
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
MessageSchema.statics.getByChat = function(idapp, chatId, options = {}) {
const { limit = 50, before = null, after = null } = options;
// Message.js (model)
MessageSchema.statics.getByChat = async function(idapp, chatId, options = {}) {
const { limit = 50, before, after } = options;
const query = {
idapp,
chatId,
isDeleted: false
};
// Filtra per timestamp
if (before) {
query.createdAt = { $lt: new Date(before) };
}
if (after) {
query.createdAt = { $gt: new Date(after) };
}
// ✅ Sempre in ordine decrescente (dal più recente al più vecchio)
return this.find(query)
.populate('senderId', 'username name surname profile.avatar')
.populate('senderId', 'username name surname profile.img')
.populate('replyTo', 'text senderId')
.populate('metadata.rideId', 'departure destination dateTime')
.sort({ createdAt: -1 })
.limit(limit);
.sort({ createdAt: -1 }) // -1 = più recente prima
.limit(limit)
};
// Metodo statico per creare messaggio di sistema

View File

@@ -2,377 +2,417 @@ const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Schema per le coordinate geografiche
const CoordinatesSchema = new Schema({
lat: {
type: Number,
required: true
const CoordinatesSchema = new Schema(
{
lat: {
type: Number,
required: true,
},
lng: {
type: Number,
required: true,
},
},
lng: {
type: Number,
required: true
}
}, { _id: false });
{ _id: false }
);
// Schema per una località (partenza, destinazione, waypoint)
const LocationSchema = new Schema({
city: {
type: String,
required: true,
trim: true
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,
},
},
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 });
{ _id: false }
);
// Schema per i waypoint (tappe intermedie)
const WaypointSchema = new Schema({
location: {
type: LocationSchema,
required: true
required: true,
},
order: {
type: Number,
required: true
required: true,
},
estimatedArrival: {
type: Date
type: Date,
},
stopDuration: {
type: Number, // minuti di sosta
default: 0
}
default: 0,
},
});
// Schema per la ricorrenza del viaggio
const RecurrenceSchema = new Schema({
type: {
type: String,
enum: ['once', 'weekly', 'custom_days', 'custom_dates'],
default: 'once'
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,
},
],
},
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 });
{ _id: false }
);
// Schema per i passeggeri
const PassengersSchema = new Schema({
available: {
type: Number,
required: true,
min: 0
const PassengersSchema = new Schema(
{
available: {
type: Number,
required: true,
min: 0,
},
max: {
type: Number,
required: true,
min: 1,
},
},
max: {
type: Number,
required: true,
min: 1
}
}, { _id: false });
{ _id: false }
);
// Schema per il veicolo
const VehicleSchema = new Schema({
type: {
type: String,
enum: ['auto', 'moto', 'furgone', 'minibus', 'altro'],
default: 'auto'
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,
},
photos: [
{
type: String,
trim: true,
},
],
features: [
{
type: String,
enum: ['aria_condizionata', 'wifi', 'presa_usb', 'bluetooth', 'bagagliaio_grande', 'seggiolino_bimbi'],
},
],
},
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 });
{ _id: false }
);
// Schema per le preferenze di viaggio
const RidePreferencesSchema = new Schema({
smoking: {
type: String,
enum: ['yes', 'no', 'outside_only'],
default: 'no'
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,
},
},
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 });
{ _id: false }
);
// Schema per il contributo/pagamento
const ContributionItemSchema = new Schema({
contribTypeId: {
type: Schema.Types.ObjectId,
ref: 'Contribtype',
required: true
required: true,
},
price: {
type: Number,
min: 0
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 }
});
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' });
@@ -382,41 +422,41 @@ 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() {
RideSchema.virtual('isFull').get(function () {
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;
});
// Virtual per calcolare posti occupati
RideSchema.virtual('bookedSeats').get(function() {
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() {
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));
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) {
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())
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') {
RideSchema.methods.updateAvailableSeats = function () {
// ⚠️ CONTROLLO: verifica che sia un'offerta e che passengers esista
if (this.type === 'offer' && this.passengers) {
const booked = this.bookedSeats;
this.passengers.available = this.passengers.max - booked;
if (this.passengers.available <= 0) {
@@ -429,9 +469,9 @@ RideSchema.methods.updateAvailableSeats = function() {
};
// Pre-save hook
RideSchema.pre('save', function(next) {
// Aggiorna posti disponibili se necessario
if (this.type === 'offer' && this.isModified('confirmedPassengers')) {
RideSchema.pre('save', function (next) {
// ⚠️ CONTROLLO: Aggiorna posti disponibili solo se è un'offerta e passengers esiste
if (this.type === 'offer' && this.passengers && 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) {
@@ -442,11 +482,11 @@ RideSchema.pre('save', function(next) {
});
// Metodi statici per ricerche comuni
RideSchema.statics.findActiveByCity = function(idapp, departureCity, destinationCity, options = {}) {
RideSchema.statics.findActiveByCity = function (idapp, departureCity, destinationCity, options = {}) {
const query = {
idapp,
status: { $in: ['active', 'full'] },
dateTime: { $gte: new Date() }
dateTime: { $gte: new Date() },
};
if (departureCity) {
@@ -472,17 +512,13 @@ RideSchema.statics.findActiveByCity = function(idapp, departureCity, destination
};
// 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 query = {
idapp,
status: { $in: ['active'] },
dateTime: { $gte: new Date() },
$or: [
{ 'departure.city': cityRegex },
{ 'destination.city': cityRegex },
{ 'waypoints.location.city': cityRegex }
]
$or: [{ 'departure.city': cityRegex }, { 'destination.city': cityRegex }, { 'waypoints.location.city': cityRegex }],
};
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'],
},
],
photos: [
{
type: String,
trim: true,
},
],
isDefault: {
type: Boolean,
default: false,
@@ -7336,7 +7342,6 @@ 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 = {

View File

@@ -1,7 +1,13 @@
const express = require('express');
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
const rideController = require('../controllers/rideController');
@@ -113,7 +119,6 @@ router.get('/stats/summary', authenticate, rideController.getStatsSummary);
*/
router.get('/cities/suggestions', rideController.getCitySuggestions);
/**
* @route GET /api/trasporti/cities/recents
* @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);
/**
* @route DELETE /api/trasporti/chats/:id
* @desc Elimina chat (soft delete)
* @access Private (solo partecipanti)
*/
router.delete('/chats/:id', authenticate, chatController.deleteChat);
// ============================================================
// ⭐ 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
* @access Public
*/
router.get('/driver/:userId', async (req, res) => {
router.get('/driver/user/:userId', async (req, res) => {
try {
const { userId } = req.params;
const { idapp } = req.query;
const { User } = require('../models/User');
const { User } = require('../models/user');
const Ride = require('../models/Ride');
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
* @desc Aggiorna profilo conducente
@@ -499,7 +548,7 @@ router.put('/driver/profile', authenticate, async (req, res) => {
const userId = req.user._id;
const { idapp, driverProfile, preferences } = req.body;
const { User } = require('../models/User');
const { User } = require('../models/user');
const updateData = {};
@@ -543,9 +592,9 @@ router.put('/driver/profile', authenticate, async (req, res) => {
router.post('/driver/vehicles', authenticate, async (req, res) => {
try {
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(
userId,
@@ -580,9 +629,9 @@ router.put('/driver/vehicles/:vehicleId', authenticate, async (req, res) => {
try {
const userId = req.user._id;
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(
{
@@ -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
* @desc Rimuovi veicolo
@@ -627,7 +724,7 @@ router.delete('/driver/vehicles/:vehicleId', authenticate, async (req, res) => {
const userId = req.user._id;
const { vehicleId } = req.params;
const { User } = require('../models/User');
const { User } = require('../models/user');
await User.findByIdAndUpdate(userId, {
$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 { vehicleId } = req.params;
const { User } = require('../models/User');
const { User } = require('../models/user');
// Prima rimuovi isDefault da tutti
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;

View File

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