Compare commits
5 Commits
master
...
feat/recur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb40743694 | ||
|
|
85141df8a4 | ||
|
|
cb965eaa27 | ||
|
|
b78e3ce544 | ||
|
|
2e7801b4ba |
@@ -44,3 +44,4 @@ MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
||||
DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7"
|
||||
SERVER_A_URL="http://51.77.156.69:3000"
|
||||
API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK"
|
||||
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||
@@ -39,3 +39,4 @@ AUTH_NEW_SITES=123123123
|
||||
SCRIPTS_DIR=admin_scripts
|
||||
CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpYgZCw-TViM2wX6vB3wlK6GD0"},{"label":"gruppomacro.com","value":"bqmzGShoX7WqOBzkXocoECyBkPq3GfqcM5t6VFd8"}]
|
||||
DS_API_KEY="sk-222e3addb3d8455d8b0516d93906eec7"
|
||||
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||
@@ -45,3 +45,4 @@ GROK_API="xai-PcNM5obgPaETtmnfDWPZk235D75ZgxENU2QmeqPfMQCHh9dwCDVeRRe0oVVA2YOpiU
|
||||
REPLICATE_API_TOKEN="r8_AVhM6igwvoOnUA65cHVZdhEDfTqBVk94WTB0u"
|
||||
FAL_KEY="7d251c88-21b5-4b55-8b3e-4bafd910f99f:b81c0a36a25b052f26eb8ac226c7efff"
|
||||
HF_TOKEN="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"
|
||||
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||
@@ -42,3 +42,4 @@ MIAB_ADMIN_EMAIL=admin@lamiaposta.org
|
||||
MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
||||
SERVER_A_URL="http://51.77.156.69:3000"
|
||||
API_KEY_MSSQL="m68yADSr123MIVIDA@154$DSAGVOK"
|
||||
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||
@@ -39,3 +39,4 @@ CLOUDFLARE_TOKENS=[{"label":"Paolo.arena77@gmail.com","value":"M9EM309v8WFquJKpY
|
||||
MIAB_HOST=box.lamiaposta.org
|
||||
MIAB_ADMIN_EMAIL=admin@lamiaposta.org
|
||||
MIAB_ADMIN_PASSWORD=passpao1pabox@1A
|
||||
ORS_API_KEY="eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjNjNTllZmY1ZTM1ZDQ5ODI5NThhOTIzYTQ5MDkxOWIwIiwiaCI6Im11cm11cjY0In0="
|
||||
@@ -55,7 +55,11 @@ class UserController {
|
||||
}
|
||||
|
||||
// Send response with tokens
|
||||
res.header('x-auth', result.token).header('x-refrtok', result.refreshToken).header('x-browser-random', result.browser_random).send(result.user);
|
||||
res
|
||||
.header('x-auth', result.token)
|
||||
.header('x-refrtok', result.refreshToken)
|
||||
.header('x-browser-random', result.browser_random)
|
||||
.send(result.user);
|
||||
} catch (error) {
|
||||
console.error('Error in registration:', error.message);
|
||||
res.status(400).send({
|
||||
@@ -103,7 +107,11 @@ class UserController {
|
||||
}
|
||||
|
||||
// Send response with tokens
|
||||
res.header('x-auth', result.token).header('x-refrtok', result.refreshToken).header('x-browser-random', result.browser_random).send({
|
||||
res
|
||||
.header('x-auth', result.token)
|
||||
.header('x-refrtok', result.refreshToken)
|
||||
.header('x-browser-random', result.browser_random)
|
||||
.send({
|
||||
usertosend: result.user,
|
||||
code: server_constants.RIS_CODE_OK,
|
||||
subsExistonDb: result.subsExistonDb,
|
||||
@@ -487,6 +495,7 @@ class UserController {
|
||||
const { User } = require('../models/user');
|
||||
return User.isCollaboratore(user.perm);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = UserController;
|
||||
|
||||
@@ -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
|
||||
|
||||
789
src/controllers/chatController.js
Normal file
789
src/controllers/chatController.js
Normal file
@@ -0,0 +1,789 @@
|
||||
const Chat = require('../models/viaggi/Chat');
|
||||
const Message = require('../models/Message');
|
||||
const { User } = require('../models/user');
|
||||
|
||||
// ===== GET USER CHATS =====
|
||||
exports.getUserChats = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const idapp = req.user.idapp;
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// ✅ Trova chat dove l'utente è partecipante E non l'ha cancellata
|
||||
const chats = await Chat.find({
|
||||
idapp,
|
||||
participants: userId,
|
||||
isActive: true,
|
||||
deletedBy: { $ne: userId }, // ✅ Escludi chat cancellate
|
||||
})
|
||||
.populate('participants', 'username name surname profile')
|
||||
.populate({
|
||||
path: 'rideId',
|
||||
select: 'departure destination departureDate departureTime status',
|
||||
})
|
||||
.sort({ updatedAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
const enrichedChats = chats.map((chat) => {
|
||||
let unreadCount = 0;
|
||||
|
||||
if (chat.unreadCount) {
|
||||
if (chat.unreadCount instanceof Map) {
|
||||
unreadCount = chat.unreadCount.get(userId.toString()) || 0;
|
||||
} else if (typeof chat.unreadCount === 'object') {
|
||||
// Dopo .lean(), la Map diventa un oggetto plain
|
||||
unreadCount = chat.unreadCount[userId.toString()] || 0;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...chat,
|
||||
unreadCount,
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: enrichedChats,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
hasMore: chats.length === limit,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching chats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel recupero delle chat',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== GET OR CREATE DIRECT CHAT =====
|
||||
exports.getOrCreateDirectChat = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { otherUserId, rideId } = req.body;
|
||||
const idapp = req.user.idapp;
|
||||
|
||||
if (!otherUserId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'otherUserId è richiesto',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che l'altro utente esista
|
||||
const otherUser = await User.findById(otherUserId);
|
||||
if (!otherUser) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Utente non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
// Cerca chat esistente
|
||||
let chat = await Chat.findOne({
|
||||
idapp,
|
||||
type: 'direct',
|
||||
participants: { $all: [userId, otherUserId], $size: 2 },
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
// Crea nuova chat
|
||||
chat = new Chat({
|
||||
idapp,
|
||||
type: 'direct',
|
||||
participants: [userId, otherUserId],
|
||||
rideId: rideId || null,
|
||||
unreadCount: new Map(),
|
||||
});
|
||||
await chat.save();
|
||||
} else if (rideId && !chat.rideId) {
|
||||
// Aggiungi rideId se non presente
|
||||
chat.rideId = rideId;
|
||||
await chat.save();
|
||||
}
|
||||
|
||||
// ✅ Se la chat era stata cancellata da uno dei due, rimuovilo da deletedBy
|
||||
if (chat.deletedBy && chat.deletedBy.length > 0) {
|
||||
const wasDeleted = chat.deletedBy.some((id) => id.toString() === userId.toString());
|
||||
|
||||
if (wasDeleted) {
|
||||
chat.deletedBy = chat.deletedBy.filter((id) => id.toString() !== userId.toString());
|
||||
await chat.save();
|
||||
}
|
||||
}
|
||||
|
||||
// Popola i partecipanti
|
||||
await chat.populate('participants', 'username name surname profile');
|
||||
if (chat.rideId) {
|
||||
await chat.populate('rideId', 'departure destination departureDate');
|
||||
}
|
||||
|
||||
// Aggiungi unread count
|
||||
const chatObj = chat.toObject();
|
||||
chatObj.unreadCount = chat.getUnreadForUser(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: chatObj,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting/creating direct chat:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nella creazione della chat',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== GET CHAT BY ID =====
|
||||
exports.getChatById = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { chatId } = req.params;
|
||||
|
||||
const chat = await Chat.findById(chatId)
|
||||
.populate('participants', 'username name surname profile')
|
||||
.populate({
|
||||
path: 'rideId',
|
||||
select: 'departure destination departureDate departureTime status',
|
||||
})
|
||||
.lean();
|
||||
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che l'utente sia partecipante
|
||||
const isParticipant = chat.participants.some((p) => {
|
||||
const pId = p._id ? p._id.toString() : p.toString();
|
||||
return pId === userId.toString();
|
||||
});
|
||||
|
||||
if (!isParticipant) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica se l'utente ha cancellato questa chat
|
||||
const wasDeleted = chat.deletedBy?.some((id) => id.toString() === userId.toString());
|
||||
|
||||
if (wasDeleted) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiungi unread count per l'utente corrente
|
||||
let unreadCount = 0;
|
||||
if (chat.unreadCount) {
|
||||
if (chat.unreadCount instanceof Map) {
|
||||
unreadCount = chat.unreadCount.get(userId.toString()) || 0;
|
||||
} else if (typeof chat.unreadCount === 'object') {
|
||||
unreadCount = chat.unreadCount[userId.toString()] || 0;
|
||||
}
|
||||
}
|
||||
|
||||
const chatObj = {
|
||||
...chat,
|
||||
unreadCount,
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: chatObj,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting chat by ID:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel recupero della chat',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== GET CHAT MESSAGES =====
|
||||
exports.getChatMessages = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { chatId } = req.params;
|
||||
const idapp = req.user.idapp;
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const { before, after, limit = 50 } = req.query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Verifica chat e partecipazione
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
const query = {
|
||||
chatId,
|
||||
idapp,
|
||||
isDeleted: { $ne: true },
|
||||
};
|
||||
|
||||
// clearedBefore
|
||||
let clearedDate = null;
|
||||
if (chat.clearedBefore) {
|
||||
if (chat.clearedBefore instanceof Map) {
|
||||
clearedDate = chat.clearedBefore.get(userId.toString());
|
||||
} else if (typeof chat.clearedBefore === 'object') {
|
||||
clearedDate = chat.clearedBefore[userId.toString()];
|
||||
}
|
||||
}
|
||||
|
||||
if (clearedDate) {
|
||||
query.createdAt = { $gt: new Date(clearedDate) };
|
||||
}
|
||||
|
||||
// ✅ Paginazione: before (messaggi più vecchi)
|
||||
if (before) {
|
||||
query.createdAt = {
|
||||
...query.createdAt,
|
||||
$lt: new Date(before),
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ Polling: after (messaggi più nuovi)
|
||||
if (after) {
|
||||
query.createdAt = {
|
||||
...query.createdAt,
|
||||
$gt: new Date(after), // Messaggi DOPO questo timestamp
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ Ordina in base alla direzione
|
||||
const sortOrder = after ? 1 : -1; // after: asc, before: desc
|
||||
|
||||
const messages = await Message.find(query)
|
||||
.sort({ createdAt: sortOrder })
|
||||
.limit(parseInt(limit))
|
||||
.populate('senderId', 'username name surname profile.img profile.avatar')
|
||||
.populate({
|
||||
path: 'replyTo',
|
||||
select: 'text senderId',
|
||||
populate: {
|
||||
path: 'senderId',
|
||||
select: 'username name',
|
||||
},
|
||||
})
|
||||
.lean();
|
||||
|
||||
// ✅ Se usato after, i messaggi sono già in ordine cronologico
|
||||
// Se usato before, invertili
|
||||
if (!after) {
|
||||
messages.reverse();
|
||||
}
|
||||
|
||||
// Marca i messaggi come letti
|
||||
await chat.markAsRead(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: messages.reverse(),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
hasMore: messages.length === limit,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching messages:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel recupero dei messaggi',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== SEND MESSAGE =====
|
||||
exports.sendMessage = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const idapp = req.user.idapp;
|
||||
const { chatId } = req.params;
|
||||
const { text, type = 'text', metadata } = req.body;
|
||||
|
||||
// Verifica chat
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
if (chat.isBlockedFor(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non puoi inviare messaggi in questa chat',
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ IMPORTANTE: Se qualcuno aveva cancellato la chat, rimuovilo da deletedBy
|
||||
// così la chat riappare nella sua lista
|
||||
if (chat.deletedBy && chat.deletedBy.length > 0) {
|
||||
const otherParticipants = chat.participants.filter((p) => p.toString() !== userId.toString());
|
||||
|
||||
let needsSave = false;
|
||||
otherParticipants.forEach((participantId) => {
|
||||
const wasDeleted = chat.deletedBy.some((id) => id.toString() === participantId.toString());
|
||||
if (wasDeleted) {
|
||||
chat.deletedBy = chat.deletedBy.filter((id) => id.toString() !== participantId.toString());
|
||||
needsSave = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (needsSave) {
|
||||
await chat.save();
|
||||
}
|
||||
}
|
||||
|
||||
// Crea messaggio
|
||||
const message = new Message({
|
||||
idapp,
|
||||
chatId: chat._id,
|
||||
senderId: userId,
|
||||
text,
|
||||
type,
|
||||
metadata,
|
||||
readBy: [userId],
|
||||
});
|
||||
|
||||
await message.save();
|
||||
await message.populate('senderId', 'username name surname profile');
|
||||
|
||||
// Aggiorna chat
|
||||
await chat.updateLastMessage(message);
|
||||
await chat.incrementUnread(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: message,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Errore nell'invio del messaggio",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== MARK MESSAGES AS READ =====
|
||||
exports.markAsRead = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { chatId } = req.params;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
// Marca come letti
|
||||
await chat.markAsRead(userId);
|
||||
|
||||
// Aggiorna anche i singoli messaggi
|
||||
await Message.updateMany(
|
||||
{
|
||||
chatId: chat._id,
|
||||
senderId: { $ne: userId },
|
||||
readBy: { $ne: userId },
|
||||
},
|
||||
{
|
||||
$addToSet: { readBy: userId },
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Messaggi marcati come letti',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error marking as read:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nella marcatura dei messaggi',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== DELETE CHAT (SOFT DELETE) =====
|
||||
exports.deleteChat = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { chatId } = req.params;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Soft delete: aggiungi userId a deletedBy
|
||||
if (!chat.deletedBy) {
|
||||
chat.deletedBy = [];
|
||||
}
|
||||
|
||||
const alreadyDeleted = chat.deletedBy.some((id) => id.toString() === userId.toString());
|
||||
|
||||
if (!alreadyDeleted) {
|
||||
chat.deletedBy.push(userId);
|
||||
}
|
||||
|
||||
// ✅ Salva il timestamp di quando l'utente ha cancellato
|
||||
// così quando riappare la chat, vedrà solo messaggi nuovi
|
||||
if (!chat.clearedBefore) {
|
||||
chat.clearedBefore = new Map();
|
||||
}
|
||||
chat.clearedBefore.set(userId.toString(), new Date());
|
||||
|
||||
await chat.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Chat eliminata',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting chat:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Errore nell'eliminazione della chat",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== TOGGLE MUTE CHAT =====
|
||||
exports.toggleMuteChat = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { chatId } = req.params;
|
||||
const { mute } = req.body;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.mutedBy) {
|
||||
chat.mutedBy = [];
|
||||
}
|
||||
|
||||
if (mute) {
|
||||
// Aggiungi a mutedBy se non presente
|
||||
const alreadyMuted = chat.mutedBy.some((id) => id.toString() === userId.toString());
|
||||
if (!alreadyMuted) {
|
||||
chat.mutedBy.push(userId);
|
||||
}
|
||||
} else {
|
||||
// Rimuovi da mutedBy
|
||||
chat.mutedBy = chat.mutedBy.filter((id) => id.toString() !== userId.toString());
|
||||
}
|
||||
|
||||
await chat.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: mute ? 'Chat silenziata' : 'Notifiche attivate',
|
||||
data: { muted: mute },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling mute:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Errore nell'aggiornamento",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ===== GET UNREAD COUNT =====
|
||||
exports.getUnreadCount = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const idapp = req.user.idapp;
|
||||
|
||||
const chats = await Chat.find({
|
||||
idapp,
|
||||
participants: userId,
|
||||
isActive: true,
|
||||
deletedBy: { $ne: userId },
|
||||
}).lean();
|
||||
|
||||
let totalUnread = 0;
|
||||
chats.forEach((chat) => {
|
||||
const unread = chat.unreadCount?.get(userId.toString()) || 0;
|
||||
totalUnread += unread;
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
totalUnread,
|
||||
chatCount: chats.length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting unread count:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel conteggio messaggi non letti',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Marca una chat come letta
|
||||
* @route PUT /api/viaggi/chats/:chatId/read
|
||||
* @access Private
|
||||
*/
|
||||
exports.markChatAsRead = async (req, res) => {
|
||||
try {
|
||||
const { chatId } = req.params;
|
||||
const userId = req.user._id;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Chat non trovata',
|
||||
});
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non sei autorizzato',
|
||||
});
|
||||
}
|
||||
|
||||
// Reset unread count
|
||||
if (!chat.unreadCount) {
|
||||
chat.unreadCount = new Map();
|
||||
}
|
||||
chat.unreadCount.set(userId.toString(), 0);
|
||||
chat.markModified('unreadCount');
|
||||
await chat.save();
|
||||
|
||||
// Marca messaggi come letti
|
||||
await Message.updateMany(
|
||||
{
|
||||
chatId,
|
||||
senderId: { $ne: userId },
|
||||
readBy: { $ne: userId },
|
||||
},
|
||||
{
|
||||
$addToSet: { readBy: userId },
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Chat marcata come letta',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore marca come letto:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Muta/smuta notifiche
|
||||
* @route PUT /api/viaggi/chats/:chatId/mute
|
||||
* @access Private
|
||||
*/
|
||||
exports.toggleMuteChat = async (req, res) => {
|
||||
try {
|
||||
const { chatId } = req.params;
|
||||
const { mute } = req.body;
|
||||
const userId = req.user._id;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({ success: false, message: 'Chat non trovata' });
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({ success: false, message: 'Non sei autorizzato' });
|
||||
}
|
||||
|
||||
if (!chat.mutedBy) {
|
||||
chat.mutedBy = [];
|
||||
}
|
||||
|
||||
const isMuted = chat.mutedBy.some((mid) => mid.toString() === userId.toString());
|
||||
|
||||
if (mute && !isMuted) {
|
||||
chat.mutedBy.push(userId);
|
||||
} else if (!mute && isMuted) {
|
||||
chat.mutedBy = chat.mutedBy.filter((mid) => mid.toString() !== userId.toString());
|
||||
}
|
||||
|
||||
await chat.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: mute ? 'Notifiche disattivate' : 'Notifiche attivate',
|
||||
data: { muted: mute },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore mute chat:', error);
|
||||
res.status(500).json({ success: false, message: 'Errore', error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Elimina un messaggio (soft delete)
|
||||
* @route DELETE /api/viaggi/chats/:chatId/messages/:messageId
|
||||
* @access Private
|
||||
*/
|
||||
exports.deleteMessage = async (req, res) => {
|
||||
try {
|
||||
const { chatId, messageId } = req.params;
|
||||
const userId = req.user._id;
|
||||
|
||||
const message = await Message.findById(messageId);
|
||||
|
||||
if (!message) {
|
||||
return res.status(404).json({ success: false, message: 'Messaggio non trovato' });
|
||||
}
|
||||
|
||||
if (message.chatId.toString() !== chatId) {
|
||||
return res.status(400).json({ success: false, message: 'Messaggio non appartiene a questa chat' });
|
||||
}
|
||||
|
||||
if (message.senderId.toString() !== userId.toString()) {
|
||||
return res.status(403).json({ success: false, message: 'Non sei autorizzato' });
|
||||
}
|
||||
|
||||
message.isDeleted = true;
|
||||
message.deletedAt = new Date();
|
||||
message.text = 'Messaggio eliminato';
|
||||
await message.save();
|
||||
|
||||
res.json({ success: true, message: 'Messaggio eliminato' });
|
||||
} catch (error) {
|
||||
console.error('Errore eliminazione messaggio:', error);
|
||||
res.status(500).json({ success: false, message: 'Errore', error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Blocca/sblocca una chat
|
||||
* @route PUT /api/viaggi/chats/:chatId/block
|
||||
* @access Private
|
||||
*/
|
||||
exports.toggleBlockChat = async (req, res) => {
|
||||
try {
|
||||
const { chatId } = req.params;
|
||||
const { block } = req.body;
|
||||
const userId = req.user._id;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
if (!chat) {
|
||||
return res.status(404).json({ success: false, message: 'Chat non trovata' });
|
||||
}
|
||||
|
||||
if (!chat.hasParticipant(userId)) {
|
||||
return res.status(403).json({ success: false, message: 'Non sei autorizzato' });
|
||||
}
|
||||
|
||||
if (!chat.blockedBy) {
|
||||
chat.blockedBy = [];
|
||||
}
|
||||
|
||||
const isBlocked = chat.blockedBy.some((bid) => bid.toString() === userId.toString());
|
||||
|
||||
if (block && !isBlocked) {
|
||||
chat.blockedBy.push(userId);
|
||||
} else if (!block && isBlocked) {
|
||||
chat.blockedBy = chat.blockedBy.filter((bid) => bid.toString() !== userId.toString());
|
||||
}
|
||||
|
||||
await chat.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: block ? 'Chat bloccata' : 'Chat sbloccata',
|
||||
data: { blocked: block },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore blocco chat:', error);
|
||||
res.status(500).json({ success: false, message: 'Errore', error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = exports;
|
||||
931
src/controllers/feedbackController.js
Normal file
931
src/controllers/feedbackController.js
Normal file
@@ -0,0 +1,931 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Feedback = require('../models/viaggi/Feedback');
|
||||
const Ride = require('../models/viaggi/Ride');
|
||||
const RideRequest = require('../models/viaggi/RideRequest');
|
||||
const { User } = require('../models/user');
|
||||
|
||||
// ============================================================
|
||||
// 🔧 HELPER FUNCTIONS (definite prima per essere disponibili)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Converti userId in ObjectId in modo sicuro
|
||||
*/
|
||||
const toObjectId = (id) => {
|
||||
if (!id) return null;
|
||||
|
||||
if (id instanceof mongoose.Types.ObjectId) {
|
||||
return id;
|
||||
}
|
||||
|
||||
if (typeof id === 'object' && id._id) {
|
||||
return new mongoose.Types.ObjectId(id._id.toString());
|
||||
}
|
||||
|
||||
return new mongoose.Types.ObjectId(id.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Ottieni statistiche feedback per un utente
|
||||
*/
|
||||
const getStatsForUser = async (idapp, userId) => {
|
||||
try {
|
||||
const userObjectId = toObjectId(userId);
|
||||
|
||||
if (!userObjectId) {
|
||||
return {
|
||||
averageRating: 0,
|
||||
totalFeedback: 0,
|
||||
asDriver: 0,
|
||||
asPassenger: 0,
|
||||
distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const result = await Feedback.aggregate([
|
||||
{
|
||||
$match: {
|
||||
idapp,
|
||||
toUserId: userObjectId,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
averageRating: { $avg: '$rating' },
|
||||
totalFeedback: { $sum: 1 },
|
||||
asDriver: {
|
||||
$sum: { $cond: [{ $eq: ['$role', 'driver'] }, 1, 0] },
|
||||
},
|
||||
asPassenger: {
|
||||
$sum: { $cond: [{ $eq: ['$role', 'passenger'] }, 1, 0] },
|
||||
},
|
||||
rating5: { $sum: { $cond: [{ $eq: ['$rating', 5] }, 1, 0] } },
|
||||
rating4: { $sum: { $cond: [{ $eq: ['$rating', 4] }, 1, 0] } },
|
||||
rating3: { $sum: { $cond: [{ $eq: ['$rating', 3] }, 1, 0] } },
|
||||
rating2: { $sum: { $cond: [{ $eq: ['$rating', 2] }, 1, 0] } },
|
||||
rating1: { $sum: { $cond: [{ $eq: ['$rating', 1] }, 1, 0] } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return {
|
||||
averageRating: 0,
|
||||
totalFeedback: 0,
|
||||
asDriver: 0,
|
||||
asPassenger: 0,
|
||||
distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const stats = result[0];
|
||||
|
||||
return {
|
||||
averageRating: stats.averageRating
|
||||
? Math.round(stats.averageRating * 10) / 10
|
||||
: 0,
|
||||
totalFeedback: stats.totalFeedback || 0,
|
||||
asDriver: stats.asDriver || 0,
|
||||
asPassenger: stats.asPassenger || 0,
|
||||
distribution: {
|
||||
1: stats.rating1 || 0,
|
||||
2: stats.rating2 || 0,
|
||||
3: stats.rating3 || 0,
|
||||
4: stats.rating4 || 0,
|
||||
5: stats.rating5 || 0,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo stats feedback:', error);
|
||||
return {
|
||||
averageRating: 0,
|
||||
totalFeedback: 0,
|
||||
asDriver: 0,
|
||||
asPassenger: 0,
|
||||
distribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calcola la distribuzione dei rating per un utente
|
||||
*/
|
||||
const getRatingDistribution = async (idapp, userId) => {
|
||||
try {
|
||||
const userObjectId = toObjectId(userId);
|
||||
|
||||
if (!userObjectId) {
|
||||
return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
||||
}
|
||||
|
||||
const result = await Feedback.aggregate([
|
||||
{
|
||||
$match: {
|
||||
idapp,
|
||||
toUserId: userObjectId,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$rating',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: -1 } },
|
||||
]);
|
||||
|
||||
const distribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
||||
|
||||
result.forEach((r) => {
|
||||
if (r._id >= 1 && r._id <= 5) {
|
||||
distribution[r._id] = r.count;
|
||||
}
|
||||
});
|
||||
|
||||
return distribution;
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo distribuzione:', error);
|
||||
return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Aggiorna la media rating nel profilo utente
|
||||
*/
|
||||
const updateUserRating = async (idapp, userId) => {
|
||||
try {
|
||||
const stats = await getStatsForUser(idapp, userId);
|
||||
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$set: {
|
||||
'profile.driverProfile.averageRating': stats.averageRating,
|
||||
'profile.driverProfile.totalFeedback': stats.totalFeedback,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore aggiornamento rating utente:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 📝 CONTROLLER FUNCTIONS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @desc Crea un feedback per un viaggio
|
||||
* @route POST /api/viaggi/feedback
|
||||
* @access Private
|
||||
*/
|
||||
const createFeedback = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
idapp,
|
||||
rideId,
|
||||
rideRequestId,
|
||||
toUserId,
|
||||
role,
|
||||
rating,
|
||||
categories,
|
||||
comment,
|
||||
pros,
|
||||
cons,
|
||||
tags,
|
||||
isPublic,
|
||||
} = req.body;
|
||||
|
||||
const fromUserId = req.user._id;
|
||||
|
||||
// Validazione base
|
||||
if (!idapp || !rideId || !toUserId || !role || !rating) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Campi obbligatori: idapp, rideId, toUserId, role, rating',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica rating valido
|
||||
if (rating < 1 || rating > 5) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Il rating deve essere tra 1 e 5',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che il ride esista
|
||||
const ride = await Ride.findById(rideId);
|
||||
if (!ride) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Viaggio non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che l'utente abbia partecipato al viaggio
|
||||
const wasDriver = ride.userId.toString() === fromUserId.toString();
|
||||
const wasPassenger = ride.confirmedPassengers?.some(
|
||||
(p) => p.userId.toString() === fromUserId.toString()
|
||||
);
|
||||
|
||||
if (!wasDriver && !wasPassenger) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non hai partecipato a questo viaggio',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che non stia valutando se stesso
|
||||
if (fromUserId.toString() === toUserId.toString()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Non puoi valutare te stesso',
|
||||
});
|
||||
}
|
||||
|
||||
// Verifica che non esista già un feedback
|
||||
const existingFeedback = await Feedback.findOne({
|
||||
rideId,
|
||||
fromUserId,
|
||||
toUserId,
|
||||
});
|
||||
|
||||
if (existingFeedback) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Hai già lasciato un feedback per questo utente in questo viaggio',
|
||||
});
|
||||
}
|
||||
|
||||
// Crea il feedback
|
||||
const feedbackData = {
|
||||
idapp,
|
||||
rideId,
|
||||
fromUserId,
|
||||
toUserId,
|
||||
role,
|
||||
rating,
|
||||
isVerified: ride.status === 'completed',
|
||||
};
|
||||
|
||||
if (rideRequestId) feedbackData.rideRequestId = rideRequestId;
|
||||
if (categories) feedbackData.categories = categories;
|
||||
if (comment) feedbackData.comment = comment;
|
||||
if (pros) feedbackData.pros = pros;
|
||||
if (cons) feedbackData.cons = cons;
|
||||
if (tags) feedbackData.tags = tags;
|
||||
if (isPublic !== undefined) feedbackData.isPublic = isPublic;
|
||||
|
||||
const feedback = new Feedback(feedbackData);
|
||||
await feedback.save();
|
||||
|
||||
// Aggiorna la media rating dell'utente destinatario
|
||||
await updateUserRating(idapp, toUserId);
|
||||
|
||||
// Aggiorna flag nella richiesta se presente
|
||||
if (rideRequestId) {
|
||||
await RideRequest.findByIdAndUpdate(rideRequestId, {
|
||||
$set: { feedbackGiven: true },
|
||||
});
|
||||
}
|
||||
|
||||
await feedback.populate('fromUserId', 'username name surname profile.img');
|
||||
await feedback.populate('toUserId', 'username name surname');
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Feedback inviato con successo!',
|
||||
data: feedback,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore creazione feedback:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nella creazione del feedback',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Ottieni i feedback ricevuti da un utente
|
||||
* @route GET /api/viaggi/feedback/user/:userId
|
||||
* @access Public
|
||||
*/
|
||||
const getUserFeedback = async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { idapp, role, page = 1, limit = 10 } = req.query;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const query = {
|
||||
idapp,
|
||||
toUserId: userId,
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
if (role) {
|
||||
query.role = role;
|
||||
}
|
||||
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const [feedbacks, total, stats] = await Promise.all([
|
||||
Feedback.find(query)
|
||||
.populate('fromUserId', 'username name surname profile.img')
|
||||
.populate('rideId', 'departure destination departureDate')
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
Feedback.countDocuments(query),
|
||||
getStatsForUser(idapp, userId),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: feedbacks,
|
||||
stats,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / parseInt(limit)),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore recupero feedbacks:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel recupero dei feedback',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Ottieni statistiche feedback per un utente
|
||||
* @route GET /api/viaggi/feedback/user/:userId/stats
|
||||
* @access Public
|
||||
*/
|
||||
const getUserFeedbackStats = async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { idapp } = req.query;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const stats = await getStatsForUser(idapp, userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore recupero stats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel recupero delle statistiche',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Ottieni i feedback per un viaggio
|
||||
* @route GET /api/viaggi/feedback/ride/:rideId
|
||||
* @access Public/Private
|
||||
*/
|
||||
const getRideFeedback = async (req, res) => {
|
||||
try {
|
||||
const { rideId } = req.params;
|
||||
const { idapp } = req.query;
|
||||
const userId = req.user?._id;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const ride = await Ride.findById(rideId);
|
||||
if (!ride) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Viaggio non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
const query = {
|
||||
idapp,
|
||||
rideId,
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
const feedbacks = await Feedback.find(query)
|
||||
.populate('fromUserId', 'username name surname profile.img')
|
||||
.populate('toUserId', 'username name surname profile.img')
|
||||
.sort({ createdAt: -1 });
|
||||
|
||||
let pendingFeedbacks = [];
|
||||
let myFeedbacks = [];
|
||||
|
||||
if (userId) {
|
||||
const wasDriver = ride.userId.toString() === userId.toString();
|
||||
const wasPassenger = ride.confirmedPassengers?.some(
|
||||
(p) => p.userId.toString() === userId.toString()
|
||||
);
|
||||
|
||||
if (wasDriver || wasPassenger) {
|
||||
myFeedbacks = feedbacks.filter(
|
||||
(f) => f.fromUserId._id.toString() === userId.toString()
|
||||
);
|
||||
|
||||
if (wasDriver) {
|
||||
const feedbackGivenTo = myFeedbacks.map((f) => f.toUserId._id.toString());
|
||||
pendingFeedbacks = (ride.confirmedPassengers || [])
|
||||
.filter((p) => !feedbackGivenTo.includes(p.userId.toString()))
|
||||
.map((p) => ({ userId: p.userId, role: 'passenger' }));
|
||||
} else {
|
||||
const hasGivenToDriver = myFeedbacks.some(
|
||||
(f) => f.toUserId._id.toString() === ride.userId.toString()
|
||||
);
|
||||
if (!hasGivenToDriver) {
|
||||
pendingFeedbacks.push({ userId: ride.userId, role: 'driver' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
feedbacks,
|
||||
pendingFeedbacks,
|
||||
myFeedbacks,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore recupero feedbacks viaggio:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Verifica se l'utente può lasciare un feedback
|
||||
* @route GET /api/viaggi/feedback/can-leave/:rideId/:toUserId
|
||||
* @access Private
|
||||
*/
|
||||
const canLeaveFeedback = async (req, res) => {
|
||||
try {
|
||||
const { rideId, toUserId } = req.params;
|
||||
const { idapp } = req.query;
|
||||
const fromUserId = req.user._id;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const ride = await Ride.findById(rideId);
|
||||
if (!ride) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Viaggio non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
const wasDriver = ride.userId.toString() === fromUserId.toString();
|
||||
const wasPassenger = ride.confirmedPassengers?.some(
|
||||
(p) => p.userId.toString() === fromUserId.toString()
|
||||
);
|
||||
|
||||
if (!wasDriver && !wasPassenger) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
canLeave: false,
|
||||
reason: 'Non hai partecipato a questo viaggio',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (fromUserId.toString() === toUserId.toString()) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
canLeave: false,
|
||||
reason: 'Non puoi valutare te stesso',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const toUserWasDriver = ride.userId.toString() === toUserId.toString();
|
||||
const toUserWasPassenger = ride.confirmedPassengers?.some(
|
||||
(p) => p.userId.toString() === toUserId.toString()
|
||||
);
|
||||
|
||||
if (!toUserWasDriver && !toUserWasPassenger) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
canLeave: false,
|
||||
reason: "L'utente destinatario non ha partecipato a questo viaggio",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (ride.status !== 'completed') {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
canLeave: false,
|
||||
reason: 'Il viaggio non è ancora stato completato',
|
||||
rideStatus: ride.status,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const existingFeedback = await Feedback.findOne({
|
||||
rideId,
|
||||
fromUserId,
|
||||
toUserId,
|
||||
});
|
||||
|
||||
if (existingFeedback) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
canLeave: false,
|
||||
reason: 'Hai già lasciato un feedback per questo utente in questo viaggio',
|
||||
existingFeedbackId: existingFeedback._id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const toUserRole = toUserWasDriver ? 'driver' : 'passenger';
|
||||
const toUser = await User.findById(toUserId).select('username name surname profile.img');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
canLeave: true,
|
||||
toUser: toUser ? {
|
||||
_id: toUser._id,
|
||||
username: toUser.username,
|
||||
name: toUser.name,
|
||||
surname: toUser.surname,
|
||||
img: toUser.profile?.img,
|
||||
} : null,
|
||||
toUserRole,
|
||||
ride: {
|
||||
_id: ride._id,
|
||||
departure: ride.departure,
|
||||
destination: ride.destination,
|
||||
departureDate: ride.departureDate,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore verifica canLeaveFeedback:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nella verifica',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Rispondi a un feedback ricevuto
|
||||
* @route POST /api/viaggi/feedback/:id/response
|
||||
* @access Private
|
||||
*/
|
||||
const respondToFeedback = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { text } = req.body;
|
||||
const userId = req.user._id;
|
||||
|
||||
if (!text || !text.trim()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Il testo della risposta è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const feedback = await Feedback.findById(id);
|
||||
|
||||
if (!feedback) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Feedback non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
if (feedback.toUserId.toString() !== userId.toString()) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Non sei autorizzato a rispondere a questo feedback',
|
||||
});
|
||||
}
|
||||
|
||||
if (feedback.response?.text) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Hai già risposto a questo feedback',
|
||||
});
|
||||
}
|
||||
|
||||
feedback.response = {
|
||||
text: text.trim(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
await feedback.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Risposta aggiunta',
|
||||
data: feedback,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore risposta feedback:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Segna un feedback come utile
|
||||
* @route POST /api/viaggi/feedback/:id/helpful
|
||||
* @access Private
|
||||
*/
|
||||
const markAsHelpful = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const userId = req.user._id;
|
||||
|
||||
const feedback = await Feedback.findById(id);
|
||||
|
||||
if (!feedback) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Feedback non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
if (!feedback.helpful) {
|
||||
feedback.helpful = { count: 0, users: [] };
|
||||
}
|
||||
|
||||
const userIdStr = userId.toString();
|
||||
const alreadyMarked = feedback.helpful.users.some(
|
||||
(u) => u.toString() === userIdStr
|
||||
);
|
||||
|
||||
if (alreadyMarked) {
|
||||
feedback.helpful.users = feedback.helpful.users.filter(
|
||||
(u) => u.toString() !== userIdStr
|
||||
);
|
||||
feedback.helpful.count = Math.max(0, feedback.helpful.count - 1);
|
||||
} else {
|
||||
feedback.helpful.users.push(userId);
|
||||
feedback.helpful.count += 1;
|
||||
}
|
||||
|
||||
await feedback.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: alreadyMarked ? 'Voto rimosso' : 'Feedback segnato come utile',
|
||||
data: {
|
||||
helpfulCount: feedback.helpful.count,
|
||||
isHelpful: !alreadyMarked,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore mark helpful:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Segnala un feedback inappropriato
|
||||
* @route POST /api/viaggi/feedback/:id/report
|
||||
* @access Private
|
||||
*/
|
||||
const reportFeedback = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { reason } = req.body;
|
||||
const userId = req.user._id;
|
||||
|
||||
if (!reason || !reason.trim()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'La motivazione è obbligatoria',
|
||||
});
|
||||
}
|
||||
|
||||
const feedback = await Feedback.findById(id);
|
||||
|
||||
if (!feedback) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Feedback non trovato',
|
||||
});
|
||||
}
|
||||
|
||||
if (!feedback.reports) {
|
||||
feedback.reports = [];
|
||||
}
|
||||
|
||||
const alreadyReported = feedback.reports.some(
|
||||
(r) => r.userId.toString() === userId.toString()
|
||||
);
|
||||
|
||||
if (alreadyReported) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Hai già segnalato questo feedback',
|
||||
});
|
||||
}
|
||||
|
||||
feedback.reports.push({
|
||||
userId,
|
||||
reason: reason.trim(),
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
if (feedback.reports.length >= 3) {
|
||||
feedback.isPublic = false;
|
||||
feedback.hiddenReason = 'Nascosto automaticamente per multiple segnalazioni';
|
||||
}
|
||||
|
||||
await feedback.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Feedback segnalato. Lo esamineremo al più presto.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore segnalazione:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Ottieni i miei feedback dati
|
||||
* @route GET /api/viaggi/feedback/my/given
|
||||
* @access Private
|
||||
*/
|
||||
const getMyGivenFeedback = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { idapp, page = 1, limit = 20 } = req.query;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const [feedbacks, total] = await Promise.all([
|
||||
Feedback.find({ idapp, fromUserId: userId })
|
||||
.populate('toUserId', 'username name surname profile.img')
|
||||
.populate('rideId', 'departure destination departureDate')
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
Feedback.countDocuments({ idapp, fromUserId: userId }),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: feedbacks,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / parseInt(limit)),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore recupero feedback dati:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Ottieni i miei feedback ricevuti
|
||||
* @route GET /api/viaggi/feedback/my/received
|
||||
* @access Private
|
||||
*/
|
||||
const getMyReceivedFeedback = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const { idapp, page = 1, limit = 20 } = req.query;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è obbligatorio',
|
||||
});
|
||||
}
|
||||
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const [feedbacks, total, stats] = await Promise.all([
|
||||
Feedback.find({ idapp, toUserId: userId })
|
||||
.populate('fromUserId', 'username name surname profile.img')
|
||||
.populate('rideId', 'departure destination departureDate')
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
Feedback.countDocuments({ idapp, toUserId: userId }),
|
||||
getStatsForUser(idapp, userId),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: feedbacks,
|
||||
stats,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / parseInt(limit)),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore recupero feedback ricevuti:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 📤 EXPORTS
|
||||
// ============================================================
|
||||
|
||||
module.exports = {
|
||||
// Controller functions
|
||||
createFeedback,
|
||||
getUserFeedback,
|
||||
getUserFeedbackStats,
|
||||
getRideFeedback,
|
||||
canLeaveFeedback,
|
||||
respondToFeedback,
|
||||
reportFeedback,
|
||||
markAsHelpful,
|
||||
getMyGivenFeedback,
|
||||
getMyReceivedFeedback,
|
||||
|
||||
// Alias per compatibilità
|
||||
getFeedbacksForUser: getUserFeedback,
|
||||
getFeedbacksForRide: getRideFeedback,
|
||||
getMyGivenFeedbacks: getMyGivenFeedback,
|
||||
getMyReceivedFeedbacks: getMyReceivedFeedback,
|
||||
|
||||
// Helper functions
|
||||
getStatsForUser,
|
||||
getRatingDistribution,
|
||||
updateUserRating,
|
||||
};
|
||||
735
src/controllers/geocodingController.js
Normal file
735
src/controllers/geocodingController.js
Normal file
@@ -0,0 +1,735 @@
|
||||
/**
|
||||
* Controller per Geocoding usando OpenRouteService
|
||||
* Documentazione: https://openrouteservice.org/dev/#/api-docs
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
// Configurazione OpenRouteService
|
||||
const ORS_BASE = 'https://api.openrouteservice.org';
|
||||
const ORS_API_KEY = process.env.ORS_API_KEY || 'YOUR_API_KEY_HERE';
|
||||
|
||||
/**
|
||||
* Helper per fare richieste HTTPS a OpenRouteService
|
||||
*/
|
||||
const makeRequest = (url, method = 'GET', body = null) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const urlObj = new URL(url);
|
||||
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method,
|
||||
headers: {
|
||||
Authorization: ORS_API_KEY,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => (data += chunk));
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (res.statusCode >= 400) {
|
||||
reject(new Error(parsed.error?.message || `HTTP ${res.statusCode}`));
|
||||
} else {
|
||||
resolve(parsed);
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('Errore parsing risposta'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.setTimeout(15000, () => {
|
||||
req.destroy();
|
||||
reject(new Error('Timeout richiesta'));
|
||||
});
|
||||
|
||||
if (body) {
|
||||
req.write(JSON.stringify(body));
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Autocomplete città (ORS Geocode Autocomplete)
|
||||
* @route GET /api/geo/autocomplete
|
||||
*/
|
||||
const autocomplete = async (req, res) => {
|
||||
try {
|
||||
const { q, limit = 5, lang = 'it', country = 'IT' } = req.query;
|
||||
|
||||
if (!q || q.length < 2) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Query deve essere almeno 2 caratteri',
|
||||
});
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
text: q,
|
||||
size: limit,
|
||||
lang,
|
||||
'boundary.country': country,
|
||||
layers: 'locality,county,region', // Solo città/comuni
|
||||
});
|
||||
|
||||
const url = `${ORS_BASE}/geocode/autocomplete?${params}`;
|
||||
const data = await makeRequest(url);
|
||||
|
||||
const results = data.features.map((feature) => ({
|
||||
id: feature.properties.id,
|
||||
city: feature.properties.name,
|
||||
locality: feature.properties.locality,
|
||||
county: feature.properties.county,
|
||||
region: feature.properties.region,
|
||||
country: feature.properties.country,
|
||||
postalCode: feature.properties.postalcode,
|
||||
coordinates: {
|
||||
lat: feature.geometry.coordinates[1],
|
||||
lng: feature.geometry.coordinates[0],
|
||||
},
|
||||
displayName: feature.properties.label,
|
||||
type: feature.properties.layer,
|
||||
confidence: feature.properties.confidence,
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: results.length,
|
||||
data: results,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore autocomplete:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante la ricerca',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Geocoding - indirizzo a coordinate (ORS Geocode Search)
|
||||
* @route GET /api/geo/geocode
|
||||
*/
|
||||
const geocode = async (req, res) => {
|
||||
try {
|
||||
const { address, city, country = 'IT', limit = 5, lang = 'it' } = req.query;
|
||||
|
||||
const searchQuery = [address, city].filter(Boolean).join(', ');
|
||||
|
||||
if (!searchQuery) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Fornisci un indirizzo o città da cercare',
|
||||
});
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
text: searchQuery,
|
||||
size: limit,
|
||||
lang,
|
||||
'boundary.country': country,
|
||||
});
|
||||
|
||||
const url = `${ORS_BASE}/geocode/search?${params}`;
|
||||
const data = await makeRequest(url);
|
||||
|
||||
if (!data.features || data.features.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Nessun risultato trovato',
|
||||
});
|
||||
}
|
||||
|
||||
const results = data.features.map((feature) => ({
|
||||
id: feature.properties.id,
|
||||
displayName: feature.properties.label,
|
||||
name: feature.properties.name,
|
||||
street: feature.properties.street,
|
||||
houseNumber: feature.properties.housenumber,
|
||||
city: feature.properties.locality || feature.properties.county,
|
||||
county: feature.properties.county,
|
||||
region: feature.properties.region,
|
||||
country: feature.properties.country,
|
||||
postalCode: feature.properties.postalcode,
|
||||
coordinates: {
|
||||
lat: feature.geometry.coordinates[1],
|
||||
lng: feature.geometry.coordinates[0],
|
||||
},
|
||||
type: feature.properties.layer,
|
||||
confidence: feature.properties.confidence,
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: results.length,
|
||||
data: results,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore geocoding:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il geocoding',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Reverse geocoding - coordinate a indirizzo (ORS Reverse)
|
||||
* @route GET /api/geo/reverse
|
||||
*/
|
||||
const reverseGeocode = async (req, res) => {
|
||||
try {
|
||||
const { lat, lng, lang = 'it' } = req.query;
|
||||
|
||||
if (!lat || !lng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate lat e lng richieste',
|
||||
});
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
'point.lat': lat,
|
||||
'point.lon': lng,
|
||||
lang,
|
||||
size: '1',
|
||||
layers: 'address,street,locality',
|
||||
});
|
||||
|
||||
const url = `${ORS_BASE}/geocode/reverse?${params}`;
|
||||
const data = await makeRequest(url);
|
||||
|
||||
if (!data.features || data.features.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Nessun risultato trovato',
|
||||
});
|
||||
}
|
||||
|
||||
const feature = data.features[0];
|
||||
|
||||
const result = {
|
||||
displayName: feature.properties.label,
|
||||
name: feature.properties.name,
|
||||
street: feature.properties.street,
|
||||
houseNumber: feature.properties.housenumber,
|
||||
city: feature.properties.locality || feature.properties.county,
|
||||
county: feature.properties.county,
|
||||
region: feature.properties.region,
|
||||
country: feature.properties.country,
|
||||
postalCode: feature.properties.postalcode,
|
||||
coordinates: {
|
||||
lat: parseFloat(lat),
|
||||
lng: parseFloat(lng),
|
||||
},
|
||||
distance: feature.properties.distance, // distanza dal punto esatto
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore reverse geocoding:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il reverse geocoding',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Calcola percorso tra due o più punti (ORS Directions)
|
||||
* @route POST /api/geo/route
|
||||
* @body { coordinates: [[lng,lat], [lng,lat], ...], profile: 'driving-car' }
|
||||
*/
|
||||
const getRoute = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
startLat,
|
||||
startLng,
|
||||
endLat,
|
||||
endLng,
|
||||
waypoints, // formato: "lat1,lng1;lat2,lng2;..."
|
||||
profile = 'driving-car', // driving-car, driving-hgv, cycling-regular, foot-walking
|
||||
language = 'it',
|
||||
units = 'km',
|
||||
} = req.query;
|
||||
|
||||
if (!startLat || !startLng || !endLat || !endLng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate di partenza e arrivo richieste',
|
||||
});
|
||||
}
|
||||
|
||||
// Costruisci array coordinate [lng, lat] (formato GeoJSON)
|
||||
const coordinates = [[parseFloat(startLng), parseFloat(startLat)]];
|
||||
|
||||
if (waypoints) {
|
||||
const waypointsList = waypoints.split(';');
|
||||
waypointsList.forEach((wp) => {
|
||||
const [lat, lng] = wp.split(',').map(parseFloat);
|
||||
coordinates.push([lng, lat]);
|
||||
});
|
||||
}
|
||||
|
||||
coordinates.push([parseFloat(endLng), parseFloat(endLat)]);
|
||||
|
||||
// Richiesta POST a ORS Directions
|
||||
const url = `${ORS_BASE}/v2/directions/${profile}`;
|
||||
|
||||
const body = {
|
||||
coordinates,
|
||||
language,
|
||||
units,
|
||||
geometry: true,
|
||||
instructions: true,
|
||||
maneuvers: true,
|
||||
};
|
||||
|
||||
const data = await makeRequest(url, 'POST', body);
|
||||
|
||||
if (!data.routes || data.routes.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impossibile calcolare il percorso',
|
||||
});
|
||||
}
|
||||
|
||||
const route = data.routes[0];
|
||||
const summary = route.summary;
|
||||
|
||||
const result = {
|
||||
distance: Math.round(summary.distance * 10) / 10, // km
|
||||
duration: Math.round(summary.duration / 60), // minuti
|
||||
durationFormatted: formatDuration(summary.duration),
|
||||
bbox: data.bbox, // Bounding box
|
||||
geometry: route.geometry, // Polyline encoded
|
||||
segments: route.segments.map((segment) => ({
|
||||
distance: Math.round(segment.distance * 10) / 10,
|
||||
duration: Math.round(segment.duration / 60),
|
||||
steps: segment.steps.map((step) => ({
|
||||
instruction: step.instruction,
|
||||
name: step.name,
|
||||
distance: Math.round(step.distance * 100) / 100,
|
||||
duration: Math.round(step.duration / 60),
|
||||
type: step.type,
|
||||
maneuver: step.maneuver,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo percorso:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il calcolo del percorso',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Calcola matrice distanze tra più punti (ORS Matrix)
|
||||
* @route POST /api/geo/matrix
|
||||
*/
|
||||
const getMatrix = async (req, res) => {
|
||||
try {
|
||||
const { locations, profile = 'driving-car' } = req.body;
|
||||
|
||||
if (!locations || locations.length < 2) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Almeno 2 location richieste',
|
||||
});
|
||||
}
|
||||
|
||||
// Formato locations: [[lng, lat], [lng, lat], ...]
|
||||
const url = `${ORS_BASE}/v2/matrix/${profile}`;
|
||||
|
||||
const body = {
|
||||
locations,
|
||||
metrics: ['distance', 'duration'],
|
||||
units: 'km',
|
||||
};
|
||||
|
||||
const data = await makeRequest(url, 'POST', body);
|
||||
|
||||
const result = {
|
||||
distances: data.distances, // Matrice distanze in km
|
||||
durations: data.durations, // Matrice durate in secondi
|
||||
sources: data.sources,
|
||||
destinations: data.destinations,
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo matrice:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il calcolo della matrice',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Suggerisci città intermedie su un percorso
|
||||
* @route GET /api/geo/suggest-waypoints
|
||||
*/
|
||||
const suggestWaypoints = async (req, res) => {
|
||||
try {
|
||||
const { startLat, startLng, endLat, endLng, count = 3 } = req.query;
|
||||
|
||||
if (!startLat || !startLng || !endLat || !endLng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate di partenza e arrivo richieste',
|
||||
});
|
||||
}
|
||||
|
||||
// Prima ottieni il percorso
|
||||
const routeUrl = `${ORS_BASE}/v2/directions/driving-car`;
|
||||
const routeBody = {
|
||||
coordinates: [
|
||||
[parseFloat(startLng), parseFloat(startLat)],
|
||||
[parseFloat(endLng), parseFloat(endLat)],
|
||||
],
|
||||
geometry: true,
|
||||
};
|
||||
|
||||
const routeData = await makeRequest(routeUrl, 'POST', routeBody);
|
||||
|
||||
if (!routeData.routes || routeData.routes.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impossibile calcolare il percorso',
|
||||
});
|
||||
}
|
||||
|
||||
// Decodifica polyline per ottenere punti
|
||||
const geometry = routeData.routes[0].geometry;
|
||||
const decodedPoints = decodePolyline(geometry);
|
||||
|
||||
// Seleziona punti equidistanti lungo il percorso
|
||||
const totalPoints = decodedPoints.length;
|
||||
const step = Math.floor(totalPoints / (parseInt(count) + 1));
|
||||
|
||||
const sampledPoints = [];
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const index = Math.min(step * i, totalPoints - 1);
|
||||
sampledPoints.push(decodedPoints[index]);
|
||||
}
|
||||
|
||||
// Fai reverse geocoding per ogni punto
|
||||
const cities = [];
|
||||
const seenCities = new Set();
|
||||
|
||||
for (const point of sampledPoints) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
'point.lat': point[1],
|
||||
'point.lon': point[0],
|
||||
lang: 'it',
|
||||
size: '1',
|
||||
layers: 'locality,county',
|
||||
});
|
||||
|
||||
const reverseUrl = `${ORS_BASE}/geocode/reverse?${params}`;
|
||||
const data = await makeRequest(reverseUrl);
|
||||
|
||||
if (data.features && data.features.length > 0) {
|
||||
const feature = data.features[0];
|
||||
const cityName = feature.properties.locality || feature.properties.county;
|
||||
|
||||
if (cityName && !seenCities.has(cityName.toLowerCase())) {
|
||||
seenCities.add(cityName.toLowerCase());
|
||||
cities.push({
|
||||
city: cityName,
|
||||
county: feature.properties.county,
|
||||
region: feature.properties.region,
|
||||
coordinates: {
|
||||
lat: point[1],
|
||||
lng: point[0],
|
||||
},
|
||||
displayName: feature.properties.label,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Errore reverse per punto:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: cities.length,
|
||||
data: cities,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore suggerimento waypoints:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il suggerimento delle tappe',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Cerca città italiane (ottimizzato)
|
||||
* @route GET /api/geo/cities/it
|
||||
*/
|
||||
const searchItalianCities = async (req, res) => {
|
||||
try {
|
||||
const { q, limit = 10, region } = req.query;
|
||||
|
||||
if (!q || q.length < 2) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Query deve essere almeno 2 caratteri',
|
||||
});
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
text: q,
|
||||
size: limit,
|
||||
lang: 'it',
|
||||
'boundary.country': 'IT',
|
||||
layers: 'locality,county',
|
||||
});
|
||||
|
||||
// Filtro opzionale per regione
|
||||
if (region) {
|
||||
params.append('region', region);
|
||||
}
|
||||
|
||||
const url = `${ORS_BASE}/geocode/search?${params}`;
|
||||
const data = await makeRequest(url);
|
||||
|
||||
const results = data.features
|
||||
.filter((f) => f.properties.locality || f.properties.county)
|
||||
.map((feature) => ({
|
||||
city: feature.properties.locality || feature.properties.name,
|
||||
county: feature.properties.county,
|
||||
region: feature.properties.region,
|
||||
postalCode: feature.properties.postalcode,
|
||||
coordinates: {
|
||||
lat: feature.geometry.coordinates[1],
|
||||
lng: feature.geometry.coordinates[0],
|
||||
},
|
||||
displayName: `${feature.properties.locality || feature.properties.name}, ${feature.properties.region}`,
|
||||
confidence: feature.properties.confidence,
|
||||
}));
|
||||
|
||||
// Rimuovi duplicati
|
||||
const unique = results.filter(
|
||||
(v, i, a) => a.findIndex((t) => t.city?.toLowerCase() === v.city?.toLowerCase()) === i
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
count: unique.length,
|
||||
data: unique,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore ricerca città italiane:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante la ricerca',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Calcola distanza e durata tra due punti (semplificato)
|
||||
* @route GET /api/geo/distance
|
||||
*/
|
||||
const getDistance = async (req, res) => {
|
||||
try {
|
||||
const { startLat, startLng, endLat, endLng, profile = 'driving-car' } = req.query;
|
||||
|
||||
if (!startLat || !startLng || !endLat || !endLng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Tutte le coordinate sono richieste',
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${ORS_BASE}/v2/directions/${profile}`;
|
||||
|
||||
const body = {
|
||||
coordinates: [
|
||||
[parseFloat(startLng), parseFloat(startLat)],
|
||||
[parseFloat(endLng), parseFloat(endLat)],
|
||||
],
|
||||
geometry: false,
|
||||
instructions: false,
|
||||
};
|
||||
|
||||
const data = await makeRequest(url, 'POST', body);
|
||||
|
||||
if (!data.routes || data.routes.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impossibile calcolare la distanza',
|
||||
});
|
||||
}
|
||||
|
||||
const summary = data.routes[0].summary;
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
distance: Math.round(summary.distance * 10) / 10, // km
|
||||
duration: Math.round(summary.duration / 60), // minuti
|
||||
durationFormatted: formatDuration(summary.duration),
|
||||
profile,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo distanza:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il calcolo della distanza',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Ottieni isocrone (aree raggiungibili in X minuti)
|
||||
* @route GET /api/geo/isochrone
|
||||
*/
|
||||
const getIsochrone = async (req, res) => {
|
||||
try {
|
||||
const { lat, lng, minutes = 30, profile = 'driving-car' } = req.query;
|
||||
|
||||
if (!lat || !lng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate richieste',
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${ORS_BASE}/v2/isochrones/${profile}`;
|
||||
|
||||
const body = {
|
||||
locations: [[parseFloat(lng), parseFloat(lat)]],
|
||||
range: [parseInt(minutes) * 60], // secondi
|
||||
range_type: 'time',
|
||||
};
|
||||
|
||||
const data = await makeRequest(url, 'POST', body);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
type: 'FeatureCollection',
|
||||
features: data.features,
|
||||
center: { lat: parseFloat(lat), lng: parseFloat(lng) },
|
||||
minutes: parseInt(minutes),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo isocrone:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il calcolo isocrone',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Formatta durata in formato leggibile
|
||||
*/
|
||||
const formatDuration = (seconds) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.round((seconds % 3600) / 60);
|
||||
|
||||
if (hours === 0) {
|
||||
return `${minutes} min`;
|
||||
} else if (minutes === 0) {
|
||||
return `${hours} h`;
|
||||
} else {
|
||||
return `${hours} h ${minutes} min`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Decodifica polyline encoded (formato Google/ORS)
|
||||
*/
|
||||
const decodePolyline = (encoded) => {
|
||||
const points = [];
|
||||
let index = 0;
|
||||
let lat = 0;
|
||||
let lng = 0;
|
||||
|
||||
while (index < encoded.length) {
|
||||
let b;
|
||||
let shift = 0;
|
||||
let result = 0;
|
||||
|
||||
do {
|
||||
b = encoded.charCodeAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
|
||||
const dlat = result & 1 ? ~(result >> 1) : result >> 1;
|
||||
lat += dlat;
|
||||
|
||||
shift = 0;
|
||||
result = 0;
|
||||
|
||||
do {
|
||||
b = encoded.charCodeAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
|
||||
const dlng = result & 1 ? ~(result >> 1) : result >> 1;
|
||||
lng += dlng;
|
||||
|
||||
points.push([lng / 1e5, lat / 1e5]); // [lng, lat] formato GeoJSON
|
||||
}
|
||||
|
||||
return points;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
autocomplete,
|
||||
geocode,
|
||||
reverseGeocode,
|
||||
getRoute,
|
||||
getMatrix,
|
||||
suggestWaypoints,
|
||||
searchItalianCities,
|
||||
getDistance,
|
||||
getIsochrone,
|
||||
};
|
||||
522
src/controllers/geocodingController_OLD.js
Normal file
522
src/controllers/geocodingController_OLD.js
Normal file
@@ -0,0 +1,522 @@
|
||||
/**
|
||||
* Controller per Geocoding usando servizi Open Source
|
||||
* - Nominatim (OpenStreetMap) per geocoding/reverse
|
||||
* - OSRM per routing
|
||||
* - Photon per autocomplete
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
// Configurazione servizi
|
||||
const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org';
|
||||
const PHOTON_BASE = 'https://photon.komoot.io';
|
||||
const OSRM_BASE = 'https://router.project-osrm.org';
|
||||
|
||||
// User-Agent richiesto da Nominatim
|
||||
const USER_AGENT = 'FreePlanetApp/1.0';
|
||||
|
||||
/**
|
||||
* Helper per fare richieste HTTP/HTTPS
|
||||
*/
|
||||
const makeRequest = (url) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = url.startsWith('https') ? https : http;
|
||||
|
||||
const req = client.get(url, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (e) {
|
||||
reject(new Error('Errore parsing risposta'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.setTimeout(10000, () => {
|
||||
req.destroy();
|
||||
reject(new Error('Timeout richiesta'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Autocomplete città (Photon API)
|
||||
* @route GET /api/viaggi/geo/autocomplete
|
||||
*/
|
||||
const autocomplete = async (req, res) => {
|
||||
try {
|
||||
const { q, limit = 5, lang = 'it' } = req.query;
|
||||
|
||||
if (!q || q.length < 2) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Query deve essere almeno 2 caratteri'
|
||||
});
|
||||
}
|
||||
|
||||
// Photon API - gratuito e veloce
|
||||
const url = `${PHOTON_BASE}/api/?q=${encodeURIComponent(q)}&limit=${limit}&lang=${lang}&osm_tag=place:city&osm_tag=place:town&osm_tag=place:village`;
|
||||
|
||||
const data = await makeRequest(url);
|
||||
|
||||
// Formatta risultati
|
||||
const results = data.features.map(feature => ({
|
||||
city: feature.properties.name,
|
||||
province: feature.properties.county || feature.properties.state,
|
||||
region: feature.properties.state,
|
||||
country: feature.properties.country,
|
||||
postalCode: feature.properties.postcode,
|
||||
coordinates: {
|
||||
lat: feature.geometry.coordinates[1],
|
||||
lng: feature.geometry.coordinates[0]
|
||||
},
|
||||
displayName: [
|
||||
feature.properties.name,
|
||||
feature.properties.county,
|
||||
feature.properties.state,
|
||||
feature.properties.country
|
||||
].filter(Boolean).join(', '),
|
||||
type: feature.properties.osm_value || 'place'
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore autocomplete:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante la ricerca',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Geocoding - indirizzo a coordinate (Nominatim)
|
||||
* @route GET /api/viaggi/geo/geocode
|
||||
*/
|
||||
const geocode = async (req, res) => {
|
||||
try {
|
||||
const { address, city, country = 'Italy' } = req.query;
|
||||
|
||||
const searchQuery = [address, city, country].filter(Boolean).join(', ');
|
||||
|
||||
if (!searchQuery) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Fornisci un indirizzo o città da cercare'
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=5&addressdetails=1`;
|
||||
|
||||
const data = await makeRequest(url);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Nessun risultato trovato'
|
||||
});
|
||||
}
|
||||
|
||||
const results = data.map(item => ({
|
||||
displayName: item.display_name,
|
||||
city: item.address.city || item.address.town || item.address.village || item.address.municipality,
|
||||
address: item.address.road ? `${item.address.road}${item.address.house_number ? ' ' + item.address.house_number : ''}` : null,
|
||||
province: item.address.county || item.address.province,
|
||||
region: item.address.state,
|
||||
country: item.address.country,
|
||||
postalCode: item.address.postcode,
|
||||
coordinates: {
|
||||
lat: parseFloat(item.lat),
|
||||
lng: parseFloat(item.lon)
|
||||
},
|
||||
type: item.type,
|
||||
importance: item.importance
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore geocoding:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il geocoding',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Reverse geocoding - coordinate a indirizzo (Nominatim)
|
||||
* @route GET /api/viaggi/geo/reverse
|
||||
*/
|
||||
const reverseGeocode = async (req, res) => {
|
||||
try {
|
||||
const { lat, lng } = req.query;
|
||||
|
||||
if (!lat || !lng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate lat e lng richieste'
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${NOMINATIM_BASE}/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`;
|
||||
|
||||
const data = await makeRequest(url);
|
||||
|
||||
if (!data || data.error) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Nessun risultato trovato'
|
||||
});
|
||||
}
|
||||
|
||||
const result = {
|
||||
displayName: data.display_name,
|
||||
city: data.address.city || data.address.town || data.address.village || data.address.municipality,
|
||||
address: data.address.road ? `${data.address.road}${data.address.house_number ? ' ' + data.address.house_number : ''}` : null,
|
||||
province: data.address.county || data.address.province,
|
||||
region: data.address.state,
|
||||
country: data.address.country,
|
||||
postalCode: data.address.postcode,
|
||||
coordinates: {
|
||||
lat: parseFloat(lat),
|
||||
lng: parseFloat(lng)
|
||||
}
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore reverse geocoding:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il reverse geocoding',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Calcola percorso tra due punti (OSRM)
|
||||
* @route GET /api/viaggi/geo/route
|
||||
*/
|
||||
const getRoute = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
startLat, startLng,
|
||||
endLat, endLng,
|
||||
waypoints // formato: "lat1,lng1;lat2,lng2;..."
|
||||
} = req.query;
|
||||
|
||||
if (!startLat || !startLng || !endLat || !endLng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate di partenza e arrivo richieste'
|
||||
});
|
||||
}
|
||||
|
||||
// Costruisci stringa coordinate
|
||||
let coordinates = `${startLng},${startLat}`;
|
||||
|
||||
if (waypoints) {
|
||||
const waypointsList = waypoints.split(';');
|
||||
waypointsList.forEach(wp => {
|
||||
const [lat, lng] = wp.split(',');
|
||||
coordinates += `;${lng},${lat}`;
|
||||
});
|
||||
}
|
||||
|
||||
coordinates += `;${endLng},${endLat}`;
|
||||
|
||||
const url = `${OSRM_BASE}/route/v1/driving/${coordinates}?overview=full&geometries=polyline&steps=true`;
|
||||
|
||||
const data = await makeRequest(url);
|
||||
|
||||
if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impossibile calcolare il percorso'
|
||||
});
|
||||
}
|
||||
|
||||
const route = data.routes[0];
|
||||
|
||||
// Estrai città attraversate (dalle istruzioni)
|
||||
const citiesAlongRoute = [];
|
||||
if (route.legs) {
|
||||
route.legs.forEach(leg => {
|
||||
if (leg.steps) {
|
||||
leg.steps.forEach(step => {
|
||||
if (step.name && step.name.length > 0) {
|
||||
// Qui potresti fare reverse geocoding per ottenere città
|
||||
// Per ora usiamo i nomi delle strade principali
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const result = {
|
||||
distance: Math.round(route.distance / 1000 * 10) / 10, // km
|
||||
duration: Math.round(route.duration / 60), // minuti
|
||||
polyline: route.geometry, // Polyline encoded
|
||||
legs: route.legs.map(leg => ({
|
||||
distance: Math.round(leg.distance / 1000 * 10) / 10,
|
||||
duration: Math.round(leg.duration / 60),
|
||||
summary: leg.summary,
|
||||
steps: leg.steps ? leg.steps.slice(0, 10).map(s => ({ // Limita step
|
||||
instruction: s.maneuver ? s.maneuver.instruction : '',
|
||||
name: s.name,
|
||||
distance: Math.round(s.distance),
|
||||
duration: Math.round(s.duration / 60)
|
||||
})) : []
|
||||
}))
|
||||
};
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo percorso:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il calcolo del percorso',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Suggerisci città intermedie su un percorso
|
||||
* @route GET /api/viaggi/geo/suggest-waypoints
|
||||
*/
|
||||
const suggestWaypoints = async (req, res) => {
|
||||
try {
|
||||
const { startLat, startLng, endLat, endLng } = req.query;
|
||||
|
||||
if (!startLat || !startLng || !endLat || !endLng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Coordinate di partenza e arrivo richieste'
|
||||
});
|
||||
}
|
||||
|
||||
// Prima ottieni il percorso
|
||||
const routeUrl = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=full&geometries=geojson`;
|
||||
|
||||
const routeData = await makeRequest(routeUrl);
|
||||
|
||||
if (!routeData || routeData.code !== 'Ok') {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impossibile calcolare il percorso'
|
||||
});
|
||||
}
|
||||
|
||||
// Prendi punti lungo il percorso (ogni ~50km circa)
|
||||
const coordinates = routeData.routes[0].geometry.coordinates;
|
||||
const totalPoints = coordinates.length;
|
||||
const step = Math.max(1, Math.floor(totalPoints / 6)); // ~5 punti intermedi
|
||||
|
||||
const sampledPoints = [];
|
||||
for (let i = step; i < totalPoints - step; i += step) {
|
||||
sampledPoints.push(coordinates[i]);
|
||||
}
|
||||
|
||||
// Fai reverse geocoding per ogni punto
|
||||
const cities = [];
|
||||
const seenCities = new Set();
|
||||
|
||||
for (const point of sampledPoints.slice(0, 5)) { // Limita a 5 richieste
|
||||
try {
|
||||
const reverseUrl = `${NOMINATIM_BASE}/reverse?format=json&lat=${point[1]}&lon=${point[0]}&addressdetails=1&zoom=10`;
|
||||
const data = await makeRequest(reverseUrl);
|
||||
|
||||
if (data && data.address) {
|
||||
const cityName = data.address.city || data.address.town || data.address.village;
|
||||
if (cityName && !seenCities.has(cityName.toLowerCase())) {
|
||||
seenCities.add(cityName.toLowerCase());
|
||||
cities.push({
|
||||
city: cityName,
|
||||
province: data.address.county || data.address.province,
|
||||
region: data.address.state,
|
||||
coordinates: {
|
||||
lat: point[1],
|
||||
lng: point[0]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limiting - aspetta 1 secondo tra le richieste (requisito Nominatim)
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
} catch (e) {
|
||||
console.log('Errore reverse per punto:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: cities
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore suggerimento waypoints:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il suggerimento delle tappe',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Cerca città italiane (ottimizzato per Italia)
|
||||
* @route GET /api/viaggi/geo/cities/it
|
||||
*/
|
||||
const searchItalianCities = async (req, res) => {
|
||||
try {
|
||||
const { q, limit = 10 } = req.query;
|
||||
|
||||
if (!q || q.length < 2) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Query deve essere almeno 2 caratteri'
|
||||
});
|
||||
}
|
||||
|
||||
// Usa Nominatim con filtro Italia
|
||||
const url = `${NOMINATIM_BASE}/search?format=json&q=${encodeURIComponent(q)}&countrycodes=it&limit=${limit}&addressdetails=1&featuretype=city`;
|
||||
|
||||
const data = await makeRequest(url);
|
||||
|
||||
const results = data
|
||||
.filter(item =>
|
||||
item.address &&
|
||||
(item.address.city || item.address.town || item.address.village)
|
||||
)
|
||||
.map(item => ({
|
||||
city: item.address.city || item.address.town || item.address.village,
|
||||
province: item.address.county || item.address.province,
|
||||
region: item.address.state,
|
||||
postalCode: item.address.postcode,
|
||||
coordinates: {
|
||||
lat: parseFloat(item.lat),
|
||||
lng: parseFloat(item.lon)
|
||||
},
|
||||
displayName: `${item.address.city || item.address.town || item.address.village}, ${item.address.county || item.address.state}`
|
||||
}));
|
||||
|
||||
// Rimuovi duplicati
|
||||
const unique = results.filter((v, i, a) =>
|
||||
a.findIndex(t => t.city.toLowerCase() === v.city.toLowerCase()) === i
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: unique
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore ricerca città italiane:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante la ricerca',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Calcola distanza e durata tra due punti
|
||||
* @route GET /api/viaggi/geo/distance
|
||||
*/
|
||||
const getDistance = async (req, res) => {
|
||||
try {
|
||||
const { startLat, startLng, endLat, endLng } = req.query;
|
||||
|
||||
if (!startLat || !startLng || !endLat || !endLng) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Tutte le coordinate sono richieste'
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${OSRM_BASE}/route/v1/driving/${startLng},${startLat};${endLng},${endLat}?overview=false`;
|
||||
|
||||
const data = await makeRequest(url);
|
||||
|
||||
if (!data || data.code !== 'Ok' || !data.routes || data.routes.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impossibile calcolare la distanza'
|
||||
});
|
||||
}
|
||||
|
||||
const route = data.routes[0];
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
distance: Math.round(route.distance / 1000 * 10) / 10, // km
|
||||
duration: Math.round(route.duration / 60), // minuti
|
||||
durationFormatted: formatDuration(route.duration)
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Errore calcolo distanza:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore durante il calcolo della distanza',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Helper per formattare durata
|
||||
const formatDuration = (seconds) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.round((seconds % 3600) / 60);
|
||||
|
||||
if (hours === 0) {
|
||||
return `${minutes} min`;
|
||||
} else if (minutes === 0) {
|
||||
return `${hours} h`;
|
||||
} else {
|
||||
return `${hours} h ${minutes} min`;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
autocomplete,
|
||||
geocode,
|
||||
reverseGeocode,
|
||||
getRoute,
|
||||
suggestWaypoints,
|
||||
searchItalianCities,
|
||||
getDistance
|
||||
};
|
||||
@@ -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
|
||||
},
|
||||
|
||||
2069
src/controllers/rideController.js
Normal file
2069
src/controllers/rideController.js
Normal file
File diff suppressed because it is too large
Load Diff
1059
src/controllers/rideRequestController.js
Normal file
1059
src/controllers/rideRequestController.js
Normal file
File diff suppressed because it is too large
Load Diff
847
src/controllers/viaggi/TrasportiNotifications.js
Normal file
847
src/controllers/viaggi/TrasportiNotifications.js
Normal file
@@ -0,0 +1,847 @@
|
||||
/**
|
||||
* TrasportiNotifications.js
|
||||
*
|
||||
* Servizio notifiche centralizzato per Trasporti Solidali.
|
||||
* USA il telegrambot.js esistente per Telegram, AGGIUNGE Email e Push.
|
||||
*
|
||||
* NON MODIFICA telegrambot.js - lo importa e usa i suoi metodi.
|
||||
*/
|
||||
|
||||
const nodemailer = require('nodemailer');
|
||||
const webpush = require('web-push');
|
||||
|
||||
// Importa il tuo telegrambot esistente
|
||||
const MyTelegramBot = require('../../telegram/telegrambot');
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURAZIONE
|
||||
// =============================================================================
|
||||
|
||||
const config = {
|
||||
// Email SMTP
|
||||
smtp: {
|
||||
host: process.env.SMTP_HOST || 'smtp.gmail.com',
|
||||
port: parseInt(process.env.SMTP_PORT) || 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS
|
||||
}
|
||||
},
|
||||
emailFrom: process.env.SMTP_FROM || 'noreply@trasporti.app',
|
||||
|
||||
// Push VAPID
|
||||
vapidPublicKey: process.env.VAPID_PUBLIC_KEY,
|
||||
vapidPrivateKey: process.env.VAPID_PRIVATE_KEY,
|
||||
vapidEmail: process.env.VAPID_EMAIL || 'admin@trasporti.app',
|
||||
|
||||
// App
|
||||
appName: process.env.APP_NAME || 'Trasporti Solidali',
|
||||
appUrl: process.env.APP_URL || 'https://trasporti.app'
|
||||
};
|
||||
|
||||
// Configura web-push se le chiavi sono presenti
|
||||
if (config.vapidPublicKey && config.vapidPrivateKey) {
|
||||
webpush.setVapidDetails(
|
||||
`mailto:${config.vapidEmail}`,
|
||||
config.vapidPublicKey,
|
||||
config.vapidPrivateKey
|
||||
);
|
||||
}
|
||||
|
||||
// Crea transporter email
|
||||
let emailTransporter = null;
|
||||
if (config.smtp.auth.user && config.smtp.auth.pass) {
|
||||
emailTransporter = nodemailer.createTransport(config.smtp);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TIPI DI NOTIFICA
|
||||
// =============================================================================
|
||||
|
||||
const NotificationType = {
|
||||
// Viaggi
|
||||
NEW_RIDE_REQUEST: 'new_ride_request',
|
||||
REQUEST_ACCEPTED: 'request_accepted',
|
||||
REQUEST_REJECTED: 'request_rejected',
|
||||
RIDE_REMINDER_24H: 'ride_reminder_24h',
|
||||
RIDE_REMINDER_2H: 'ride_reminder_2h',
|
||||
RIDE_CANCELLED: 'ride_cancelled',
|
||||
RIDE_MODIFIED: 'ride_modified',
|
||||
|
||||
// Messaggi
|
||||
NEW_MESSAGE: 'new_message',
|
||||
|
||||
// Community
|
||||
NEW_COMMUNITY_RIDE: 'new_community_ride',
|
||||
|
||||
// Sistema
|
||||
WEEKLY_DIGEST: 'weekly_digest',
|
||||
TEST: 'test',
|
||||
WELCOME: 'welcome'
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// EMOJI PER NOTIFICHE
|
||||
// =============================================================================
|
||||
|
||||
const emo = {
|
||||
CAR: '🚗',
|
||||
PASSENGER: '🧑🤝🧑',
|
||||
CHECK: '✅',
|
||||
CROSS: '❌',
|
||||
BELL: '🔔',
|
||||
CLOCK: '⏰',
|
||||
CALENDAR: '📅',
|
||||
PIN: '📍',
|
||||
ARROW: '➡️',
|
||||
MESSAGE: '💬',
|
||||
STAR: '⭐',
|
||||
WARNING: '⚠️',
|
||||
INFO: 'ℹ️',
|
||||
WAVE: '👋',
|
||||
HEART: '❤️'
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// TRADUZIONI NOTIFICHE
|
||||
// =============================================================================
|
||||
|
||||
const translations = {
|
||||
it: {
|
||||
// Richieste
|
||||
NEW_RIDE_REQUEST_TITLE: 'Nuova richiesta di passaggio',
|
||||
NEW_RIDE_REQUEST_BODY: '{{passengerName}} chiede un passaggio per il viaggio {{departure}} → {{destination}} del {{date}}',
|
||||
NEW_RIDE_REQUEST_ACTION: 'Visualizza richiesta',
|
||||
|
||||
// Accettazione
|
||||
REQUEST_ACCEPTED_TITLE: 'Richiesta accettata!',
|
||||
REQUEST_ACCEPTED_BODY: '{{driverName}} ha accettato la tua richiesta per {{departure}} → {{destination}} del {{date}}',
|
||||
REQUEST_ACCEPTED_ACTION: 'Visualizza viaggio',
|
||||
|
||||
// Rifiuto
|
||||
REQUEST_REJECTED_TITLE: 'Richiesta non accettata',
|
||||
REQUEST_REJECTED_BODY: '{{driverName}} non ha potuto accettare la tua richiesta per {{departure}} → {{destination}}',
|
||||
|
||||
// Promemoria
|
||||
RIDE_REMINDER_24H_TITLE: 'Viaggio domani!',
|
||||
RIDE_REMINDER_24H_BODY: 'Promemoria: domani hai un viaggio {{departure}} → {{destination}} alle {{time}}',
|
||||
RIDE_REMINDER_2H_TITLE: 'Viaggio tra 2 ore!',
|
||||
RIDE_REMINDER_2H_BODY: 'Il tuo viaggio {{departure}} → {{destination}} parte tra 2 ore alle {{time}}',
|
||||
|
||||
// Cancellazione
|
||||
RIDE_CANCELLED_TITLE: 'Viaggio cancellato',
|
||||
RIDE_CANCELLED_BODY: 'Il viaggio {{departure}} → {{destination}} del {{date}} è stato cancellato',
|
||||
RIDE_CANCELLED_REASON: 'Motivo: {{reason}}',
|
||||
|
||||
// Modifica
|
||||
RIDE_MODIFIED_TITLE: 'Viaggio modificato',
|
||||
RIDE_MODIFIED_BODY: 'Il viaggio {{departure}} → {{destination}} è stato modificato. Verifica i nuovi dettagli.',
|
||||
|
||||
// Messaggi
|
||||
NEW_MESSAGE_TITLE: 'Nuovo messaggio',
|
||||
NEW_MESSAGE_BODY: '{{senderName}}: {{preview}}',
|
||||
|
||||
// Community
|
||||
NEW_COMMUNITY_RIDE_TITLE: 'Nuovo viaggio nella tua zona',
|
||||
NEW_COMMUNITY_RIDE_BODY: 'Nuovo viaggio disponibile: {{departure}} → {{destination}} il {{date}}',
|
||||
|
||||
// Test
|
||||
TEST_TITLE: 'Notifica di test',
|
||||
TEST_BODY: 'Questa è una notifica di test da Trasporti Solidali. Se la vedi, tutto funziona!',
|
||||
|
||||
// Welcome
|
||||
WELCOME_TITLE: 'Benvenuto su Trasporti Solidali!',
|
||||
WELCOME_BODY: 'Le notifiche sono state attivate correttamente. Riceverai aggiornamenti sui tuoi viaggi.',
|
||||
|
||||
// Common
|
||||
VIEW_DETAILS: 'Visualizza dettagli',
|
||||
REPLY: 'Rispondi'
|
||||
},
|
||||
|
||||
en: {
|
||||
NEW_RIDE_REQUEST_TITLE: 'New ride request',
|
||||
NEW_RIDE_REQUEST_BODY: '{{passengerName}} requests a ride for {{departure}} → {{destination}} on {{date}}',
|
||||
NEW_RIDE_REQUEST_ACTION: 'View request',
|
||||
|
||||
REQUEST_ACCEPTED_TITLE: 'Request accepted!',
|
||||
REQUEST_ACCEPTED_BODY: '{{driverName}} accepted your request for {{departure}} → {{destination}} on {{date}}',
|
||||
REQUEST_ACCEPTED_ACTION: 'View ride',
|
||||
|
||||
REQUEST_REJECTED_TITLE: 'Request not accepted',
|
||||
REQUEST_REJECTED_BODY: '{{driverName}} could not accept your request for {{departure}} → {{destination}}',
|
||||
|
||||
RIDE_REMINDER_24H_TITLE: 'Ride tomorrow!',
|
||||
RIDE_REMINDER_24H_BODY: 'Reminder: tomorrow you have a ride {{departure}} → {{destination}} at {{time}}',
|
||||
RIDE_REMINDER_2H_TITLE: 'Ride in 2 hours!',
|
||||
RIDE_REMINDER_2H_BODY: 'Your ride {{departure}} → {{destination}} leaves in 2 hours at {{time}}',
|
||||
|
||||
RIDE_CANCELLED_TITLE: 'Ride cancelled',
|
||||
RIDE_CANCELLED_BODY: 'The ride {{departure}} → {{destination}} on {{date}} has been cancelled',
|
||||
RIDE_CANCELLED_REASON: 'Reason: {{reason}}',
|
||||
|
||||
RIDE_MODIFIED_TITLE: 'Ride modified',
|
||||
RIDE_MODIFIED_BODY: 'The ride {{departure}} → {{destination}} has been modified. Check the new details.',
|
||||
|
||||
NEW_MESSAGE_TITLE: 'New message',
|
||||
NEW_MESSAGE_BODY: '{{senderName}}: {{preview}}',
|
||||
|
||||
NEW_COMMUNITY_RIDE_TITLE: 'New ride in your area',
|
||||
NEW_COMMUNITY_RIDE_BODY: 'New ride available: {{departure}} → {{destination}} on {{date}}',
|
||||
|
||||
TEST_TITLE: 'Test notification',
|
||||
TEST_BODY: 'This is a test notification from Trasporti Solidali. If you see this, everything works!',
|
||||
|
||||
WELCOME_TITLE: 'Welcome to Trasporti Solidali!',
|
||||
WELCOME_BODY: 'Notifications have been enabled successfully. You will receive updates about your rides.',
|
||||
|
||||
VIEW_DETAILS: 'View details',
|
||||
REPLY: 'Reply'
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Ottiene traduzione con sostituzione variabili
|
||||
*/
|
||||
function getTranslation(lang, key, data = {}) {
|
||||
const langTranslations = translations[lang] || translations['it'];
|
||||
let text = langTranslations[key] || translations['it'][key] || key;
|
||||
|
||||
// Sostituisci {{variabile}}
|
||||
Object.keys(data).forEach(varName => {
|
||||
const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g');
|
||||
text = text.replace(regex, data[varName] || '');
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappa tipo notifica a chiave preferenze
|
||||
*/
|
||||
function getPreferenceKey(type) {
|
||||
const map = {
|
||||
[NotificationType.NEW_RIDE_REQUEST]: 'newRideRequest',
|
||||
[NotificationType.REQUEST_ACCEPTED]: 'requestAccepted',
|
||||
[NotificationType.REQUEST_REJECTED]: 'requestRejected',
|
||||
[NotificationType.RIDE_REMINDER_24H]: 'rideReminder24h',
|
||||
[NotificationType.RIDE_REMINDER_2H]: 'rideReminder2h',
|
||||
[NotificationType.RIDE_CANCELLED]: 'rideCancelled',
|
||||
[NotificationType.RIDE_MODIFIED]: 'rideCancelled',
|
||||
[NotificationType.NEW_MESSAGE]: 'newMessage',
|
||||
[NotificationType.NEW_COMMUNITY_RIDE]: 'newCommunityRide',
|
||||
[NotificationType.WEEKLY_DIGEST]: 'weeklyDigest',
|
||||
[NotificationType.TEST]: null, // Sempre inviato
|
||||
[NotificationType.WELCOME]: null // Sempre inviato
|
||||
};
|
||||
return map[type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se inviare notifica su un canale
|
||||
*/
|
||||
function shouldSend(prefs, channel, type) {
|
||||
if (!prefs) return false;
|
||||
|
||||
const channelPrefs = prefs[channel];
|
||||
if (!channelPrefs || !channelPrefs.enabled) return false;
|
||||
|
||||
// Test e Welcome sempre inviati se canale abilitato
|
||||
if (type === NotificationType.TEST || type === NotificationType.WELCOME) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const prefKey = getPreferenceKey(type);
|
||||
if (!prefKey) return true; // Se non mappato, invia
|
||||
|
||||
return channelPrefs[prefKey] !== false; // Default true
|
||||
}
|
||||
|
||||
/**
|
||||
* Tronca testo
|
||||
*/
|
||||
function truncate(text, maxLength = 100) {
|
||||
if (!text || text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EMAIL TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
function buildEmailHtml(type, data, lang = 'it') {
|
||||
const t = (key) => getTranslation(lang, key, data);
|
||||
|
||||
const title = t(`${type.toUpperCase()}_TITLE`);
|
||||
const body = t(`${type.toUpperCase()}_BODY`);
|
||||
|
||||
// Colori per tipo
|
||||
const colors = {
|
||||
[NotificationType.NEW_RIDE_REQUEST]: '#667eea',
|
||||
[NotificationType.REQUEST_ACCEPTED]: '#21ba45',
|
||||
[NotificationType.REQUEST_REJECTED]: '#c10015',
|
||||
[NotificationType.RIDE_REMINDER_24H]: '#f2711c',
|
||||
[NotificationType.RIDE_REMINDER_2H]: '#db2828',
|
||||
[NotificationType.RIDE_CANCELLED]: '#c10015',
|
||||
[NotificationType.NEW_MESSAGE]: '#2185d0',
|
||||
[NotificationType.NEW_COMMUNITY_RIDE]: '#a333c8',
|
||||
[NotificationType.TEST]: '#667eea',
|
||||
[NotificationType.WELCOME]: '#21ba45'
|
||||
};
|
||||
|
||||
const color = colors[type] || '#667eea';
|
||||
|
||||
// Emoji per tipo
|
||||
const emojis = {
|
||||
[NotificationType.NEW_RIDE_REQUEST]: emo.PASSENGER,
|
||||
[NotificationType.REQUEST_ACCEPTED]: emo.CHECK,
|
||||
[NotificationType.REQUEST_REJECTED]: emo.CROSS,
|
||||
[NotificationType.RIDE_REMINDER_24H]: emo.CALENDAR,
|
||||
[NotificationType.RIDE_REMINDER_2H]: emo.CLOCK,
|
||||
[NotificationType.RIDE_CANCELLED]: emo.WARNING,
|
||||
[NotificationType.NEW_MESSAGE]: emo.MESSAGE,
|
||||
[NotificationType.NEW_COMMUNITY_RIDE]: emo.CAR,
|
||||
[NotificationType.TEST]: emo.BELL,
|
||||
[NotificationType.WELCOME]: emo.WAVE
|
||||
};
|
||||
|
||||
const emoji = emojis[type] || emo.BELL;
|
||||
|
||||
// CTA button
|
||||
let ctaHtml = '';
|
||||
if (data.actionUrl) {
|
||||
const actionText = data.actionText || t('VIEW_DETAILS');
|
||||
ctaHtml = `
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${data.actionUrl}"
|
||||
style="display: inline-block; padding: 14px 32px; background: ${color};
|
||||
color: white; text-decoration: none; border-radius: 8px;
|
||||
font-weight: 600; font-size: 16px;">
|
||||
${actionText}
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Info viaggio
|
||||
let rideInfoHtml = '';
|
||||
if (data.departure && data.destination) {
|
||||
rideInfoHtml = `
|
||||
<div style="background: #f8f9fa; border-radius: 12px; padding: 20px; margin: 20px 0;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
|
||||
<span style="font-size: 24px;">${emo.PIN}</span>
|
||||
<div>
|
||||
<div style="color: #666; font-size: 12px;">Partenza</div>
|
||||
<div style="font-weight: 600; font-size: 16px;">${data.departure}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center; color: #999; margin: 10px 0;">${emo.ARROW}</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 24px;">${emo.PIN}</span>
|
||||
<div>
|
||||
<div style="color: #666; font-size: 12px;">Destinazione</div>
|
||||
<div style="font-weight: 600; font-size: 16px;">${data.destination}</div>
|
||||
</div>
|
||||
</div>
|
||||
${data.date ? `
|
||||
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e0e0e0;">
|
||||
<span style="color: #666;">${emo.CALENDAR} ${data.date}</span>
|
||||
${data.time ? `<span style="margin-left: 15px; color: #666;">${emo.CLOCK} ${data.time}</span>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5;">
|
||||
<div style="max-width: 600px; margin: 0 auto; background: white;">
|
||||
<!-- Header -->
|
||||
<div style="background: linear-gradient(135deg, ${color} 0%, ${color}dd 100%); padding: 30px 20px; text-align: center;">
|
||||
<div style="font-size: 48px; margin-bottom: 10px;">${emoji}</div>
|
||||
<h1 style="margin: 0; color: white; font-size: 24px; font-weight: 600;">${title}</h1>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div style="padding: 30px 20px;">
|
||||
<p style="font-size: 16px; line-height: 1.6; color: #333; margin: 0 0 20px;">
|
||||
${body}
|
||||
</p>
|
||||
|
||||
${rideInfoHtml}
|
||||
|
||||
${data.reason ? `<p style="color: #666; font-style: italic;">${t('RIDE_CANCELLED_REASON')}</p>` : ''}
|
||||
|
||||
${ctaHtml}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="background: #f8f9fa; padding: 20px; text-align: center; border-top: 1px solid #e0e0e0;">
|
||||
<p style="margin: 0 0 10px; color: #666; font-size: 14px;">
|
||||
${config.appName}
|
||||
</p>
|
||||
<p style="margin: 0; color: #999; font-size: 12px;">
|
||||
Ricevi questa email perché hai attivato le notifiche.
|
||||
<a href="${config.appUrl}/impostazioni" style="color: ${color};">Gestisci preferenze</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TELEGRAM MESSAGE BUILDER
|
||||
// =============================================================================
|
||||
|
||||
function buildTelegramMessage(type, data, lang = 'it') {
|
||||
const t = (key) => getTranslation(lang, key, data);
|
||||
|
||||
const title = t(`${type.toUpperCase()}_TITLE`);
|
||||
const body = t(`${type.toUpperCase()}_BODY`);
|
||||
|
||||
// Emoji per tipo
|
||||
const emojis = {
|
||||
[NotificationType.NEW_RIDE_REQUEST]: emo.PASSENGER,
|
||||
[NotificationType.REQUEST_ACCEPTED]: emo.CHECK,
|
||||
[NotificationType.REQUEST_REJECTED]: emo.CROSS,
|
||||
[NotificationType.RIDE_REMINDER_24H]: emo.CALENDAR,
|
||||
[NotificationType.RIDE_REMINDER_2H]: emo.CLOCK,
|
||||
[NotificationType.RIDE_CANCELLED]: emo.WARNING,
|
||||
[NotificationType.NEW_MESSAGE]: emo.MESSAGE,
|
||||
[NotificationType.NEW_COMMUNITY_RIDE]: emo.CAR,
|
||||
[NotificationType.TEST]: emo.BELL,
|
||||
[NotificationType.WELCOME]: emo.WAVE
|
||||
};
|
||||
|
||||
const emoji = emojis[type] || emo.BELL;
|
||||
|
||||
let message = `${emoji} <b>${title}</b>\n\n${body}`;
|
||||
|
||||
// Aggiungi info viaggio
|
||||
if (data.departure && data.destination) {
|
||||
message += `\n\n${emo.PIN} <b>Percorso:</b>\n${data.departure} ${emo.ARROW} ${data.destination}`;
|
||||
if (data.date) {
|
||||
message += `\n${emo.CALENDAR} ${data.date}`;
|
||||
}
|
||||
if (data.time) {
|
||||
message += ` ${emo.CLOCK} ${data.time}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Motivo cancellazione
|
||||
if (data.reason) {
|
||||
message += `\n\n<i>${t('RIDE_CANCELLED_REASON')}</i>`;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PUSH NOTIFICATION BUILDER
|
||||
// =============================================================================
|
||||
|
||||
function buildPushPayload(type, data, lang = 'it') {
|
||||
const t = (key) => getTranslation(lang, key, data);
|
||||
|
||||
const title = t(`${type.toUpperCase()}_TITLE`);
|
||||
let body = t(`${type.toUpperCase()}_BODY`);
|
||||
|
||||
// Tronca body per push
|
||||
body = truncate(body, 150);
|
||||
|
||||
// Icone per tipo
|
||||
const icons = {
|
||||
[NotificationType.NEW_RIDE_REQUEST]: '/icons/request.png',
|
||||
[NotificationType.REQUEST_ACCEPTED]: '/icons/accepted.png',
|
||||
[NotificationType.REQUEST_REJECTED]: '/icons/rejected.png',
|
||||
[NotificationType.RIDE_REMINDER_24H]: '/icons/reminder.png',
|
||||
[NotificationType.RIDE_REMINDER_2H]: '/icons/urgent.png',
|
||||
[NotificationType.RIDE_CANCELLED]: '/icons/cancelled.png',
|
||||
[NotificationType.NEW_MESSAGE]: '/icons/message.png',
|
||||
[NotificationType.NEW_COMMUNITY_RIDE]: '/icons/community.png',
|
||||
[NotificationType.TEST]: '/icons/notification.png',
|
||||
[NotificationType.WELCOME]: '/icons/welcome.png'
|
||||
};
|
||||
|
||||
return {
|
||||
title,
|
||||
body,
|
||||
icon: icons[type] || '/icons/notification.png',
|
||||
badge: '/icons/badge.png',
|
||||
tag: type,
|
||||
data: {
|
||||
type,
|
||||
url: data.actionUrl || config.appUrl,
|
||||
...data
|
||||
},
|
||||
actions: data.actionUrl ? [
|
||||
{ action: 'open', title: t('VIEW_DETAILS') }
|
||||
] : []
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN SERVICE
|
||||
// =============================================================================
|
||||
|
||||
const TrasportiNotifications = {
|
||||
|
||||
// Esponi tipi e emoji
|
||||
NotificationType,
|
||||
emo,
|
||||
|
||||
// Esponi config
|
||||
config,
|
||||
|
||||
/**
|
||||
* Invia notifica su tutti i canali abilitati
|
||||
*
|
||||
* @param {Object} user - Utente destinatario (con notificationPreferences)
|
||||
* @param {string} type - Tipo notifica (da NotificationType)
|
||||
* @param {Object} data - Dati per template
|
||||
* @param {string} idapp - ID app (per Telegram)
|
||||
* @returns {Object} { success, results: { email, telegram, push } }
|
||||
*/
|
||||
async sendNotification(user, type, data, idapp) {
|
||||
const results = {
|
||||
email: null,
|
||||
telegram: null,
|
||||
push: null
|
||||
};
|
||||
|
||||
const prefs = user.notificationPreferences || {};
|
||||
const lang = user.lang || 'it';
|
||||
|
||||
// Aggiungi URL azione se non presente
|
||||
if (!data.actionUrl && data.rideId) {
|
||||
data.actionUrl = `${config.appUrl}/trasporti/viaggio/${data.rideId}`;
|
||||
}
|
||||
if (!data.actionUrl && data.requestId) {
|
||||
data.actionUrl = `${config.appUrl}/trasporti/richieste/${data.requestId}`;
|
||||
}
|
||||
if (!data.actionUrl && data.chatId) {
|
||||
data.actionUrl = `${config.appUrl}/trasporti/chat/${data.chatId}`;
|
||||
}
|
||||
|
||||
// EMAIL
|
||||
if (shouldSend(prefs, 'email', type) && user.email) {
|
||||
results.email = await this.sendEmail(user.email, type, data, lang);
|
||||
}
|
||||
|
||||
// TELEGRAM (usa il tuo telegrambot.js esistente!)
|
||||
const telegId = user.profile?.teleg_id || prefs.telegram?.chatId;
|
||||
if (shouldSend(prefs, 'telegram', type) && telegId) {
|
||||
results.telegram = await this.sendTelegram(idapp, telegId, type, data, lang);
|
||||
}
|
||||
|
||||
// PUSH
|
||||
const pushSub = prefs.push?.subscription;
|
||||
if (shouldSend(prefs, 'push', type) && pushSub) {
|
||||
results.push = await this.sendPush(pushSub, type, data, lang);
|
||||
}
|
||||
|
||||
return {
|
||||
success: Object.values(results).some(r => r?.success),
|
||||
results
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Invia notifica a multipli utenti
|
||||
*/
|
||||
async sendNotificationToMany(users, type, data, idapp) {
|
||||
const results = [];
|
||||
|
||||
for (const user of users) {
|
||||
try {
|
||||
const result = await this.sendNotification(user, type, data, idapp);
|
||||
results.push({ userId: user._id, ...result });
|
||||
|
||||
// Delay per evitare rate limiting
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} catch (error) {
|
||||
results.push({ userId: user._id, success: false, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// EMAIL
|
||||
// ===========================================================================
|
||||
|
||||
async sendEmail(to, type, data, lang = 'it') {
|
||||
if (!emailTransporter) {
|
||||
return { success: false, error: 'Email not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
const t = (key) => getTranslation(lang, key, data);
|
||||
const subject = `${config.appName} - ${t(`${type.toUpperCase()}_TITLE`)}`;
|
||||
const html = buildEmailHtml(type, data, lang);
|
||||
|
||||
const info = await emailTransporter.sendMail({
|
||||
from: `"${config.appName}" <${config.emailFrom}>`,
|
||||
to,
|
||||
subject,
|
||||
html
|
||||
});
|
||||
|
||||
return { success: true, messageId: info.messageId };
|
||||
} catch (error) {
|
||||
console.error('Email send error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// TELEGRAM (usa il tuo MyTelegramBot!)
|
||||
// ===========================================================================
|
||||
|
||||
async sendTelegram(idapp, chatId, type, data, lang = 'it') {
|
||||
try {
|
||||
const message = buildTelegramMessage(type, data, lang);
|
||||
|
||||
// USA IL TUO METODO ESISTENTE!
|
||||
const result = await MyTelegramBot.local_sendMsgTelegramByIdTelegram(
|
||||
idapp,
|
||||
chatId,
|
||||
message,
|
||||
null, // message_id
|
||||
null, // chat_id reply
|
||||
false, // ripr_menuPrec
|
||||
null, // MyForm (bottoni)
|
||||
'' // img
|
||||
);
|
||||
|
||||
return { success: true, messageId: result?.message_id };
|
||||
} catch (error) {
|
||||
console.error('Telegram send error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Invia notifica Telegram con bottoni inline
|
||||
*/
|
||||
async sendTelegramWithButtons(idapp, chatId, type, data, buttons, lang = 'it') {
|
||||
try {
|
||||
const message = buildTelegramMessage(type, data, lang);
|
||||
|
||||
// Crea inline keyboard
|
||||
const cl = MyTelegramBot.getclTelegByidapp(idapp);
|
||||
if (!cl) {
|
||||
return { success: false, error: 'Telegram client not found' };
|
||||
}
|
||||
|
||||
const keyboard = cl.getInlineKeyboard(lang, buttons);
|
||||
|
||||
const result = await MyTelegramBot.local_sendMsgTelegramByIdTelegram(
|
||||
idapp,
|
||||
chatId,
|
||||
message,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
keyboard,
|
||||
''
|
||||
);
|
||||
|
||||
return { success: true, messageId: result?.message_id };
|
||||
} catch (error) {
|
||||
console.error('Telegram send error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// PUSH
|
||||
// ===========================================================================
|
||||
|
||||
async sendPush(subscription, type, data, lang = 'it') {
|
||||
if (!config.vapidPublicKey || !config.vapidPrivateKey) {
|
||||
return { success: false, error: 'Push not configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.stringify(buildPushPayload(type, data, lang));
|
||||
|
||||
await webpush.sendNotification(subscription, payload);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Push send error:', error);
|
||||
|
||||
// Subscription scaduta
|
||||
if (error.statusCode === 410 || error.statusCode === 404) {
|
||||
return { success: false, error: 'Subscription expired', expired: true };
|
||||
}
|
||||
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// METODI SPECIFICI PER TRASPORTI
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* Notifica nuova richiesta passaggio al conducente
|
||||
*/
|
||||
async notifyNewRideRequest(driver, passenger, ride, request, idapp) {
|
||||
return this.sendNotification(driver, NotificationType.NEW_RIDE_REQUEST, {
|
||||
passengerName: `${passenger.name} ${passenger.surname}`,
|
||||
departure: ride.departure?.city || ride.departure?.address,
|
||||
destination: ride.destination?.city || ride.destination?.address,
|
||||
date: formatDate(ride.departureTime),
|
||||
time: formatTime(ride.departureTime),
|
||||
seats: request.seats || 1,
|
||||
rideId: ride._id,
|
||||
requestId: request._id,
|
||||
actionUrl: `${config.appUrl}/trasporti/richieste/${request._id}`
|
||||
}, idapp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifica richiesta accettata al passeggero
|
||||
*/
|
||||
async notifyRequestAccepted(passenger, driver, ride, idapp) {
|
||||
return this.sendNotification(passenger, NotificationType.REQUEST_ACCEPTED, {
|
||||
driverName: `${driver.name} ${driver.surname}`,
|
||||
departure: ride.departure?.city || ride.departure?.address,
|
||||
destination: ride.destination?.city || ride.destination?.address,
|
||||
date: formatDate(ride.departureTime),
|
||||
time: formatTime(ride.departureTime),
|
||||
rideId: ride._id,
|
||||
actionUrl: `${config.appUrl}/trasporti/viaggio/${ride._id}`
|
||||
}, idapp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifica richiesta rifiutata al passeggero
|
||||
*/
|
||||
async notifyRequestRejected(passenger, driver, ride, reason, idapp) {
|
||||
return this.sendNotification(passenger, NotificationType.REQUEST_REJECTED, {
|
||||
driverName: `${driver.name} ${driver.surname}`,
|
||||
departure: ride.departure?.city || ride.departure?.address,
|
||||
destination: ride.destination?.city || ride.destination?.address,
|
||||
reason
|
||||
}, idapp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifica promemoria viaggio
|
||||
*/
|
||||
async notifyRideReminder(user, ride, hoursBefor, idapp) {
|
||||
const type = hoursBefor === 24
|
||||
? NotificationType.RIDE_REMINDER_24H
|
||||
: NotificationType.RIDE_REMINDER_2H;
|
||||
|
||||
return this.sendNotification(user, type, {
|
||||
departure: ride.departure?.city || ride.departure?.address,
|
||||
destination: ride.destination?.city || ride.destination?.address,
|
||||
date: formatDate(ride.departureTime),
|
||||
time: formatTime(ride.departureTime),
|
||||
rideId: ride._id,
|
||||
actionUrl: `${config.appUrl}/trasporti/viaggio/${ride._id}`
|
||||
}, idapp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifica viaggio cancellato
|
||||
*/
|
||||
async notifyRideCancelled(user, ride, reason, idapp) {
|
||||
return this.sendNotification(user, NotificationType.RIDE_CANCELLED, {
|
||||
departure: ride.departure?.city || ride.departure?.address,
|
||||
destination: ride.destination?.city || ride.destination?.address,
|
||||
date: formatDate(ride.departureTime),
|
||||
reason,
|
||||
rideId: ride._id
|
||||
}, idapp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifica nuovo messaggio
|
||||
*/
|
||||
async notifyNewMessage(recipient, sender, message, chatId, idapp) {
|
||||
return this.sendNotification(recipient, NotificationType.NEW_MESSAGE, {
|
||||
senderName: `${sender.name} ${sender.surname}`,
|
||||
preview: truncate(message.text, 100),
|
||||
chatId,
|
||||
actionUrl: `${config.appUrl}/trasporti/chat/${chatId}`
|
||||
}, idapp);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invia notifica di test
|
||||
*/
|
||||
async sendTestNotification(user, channel, idapp) {
|
||||
const type = NotificationType.TEST;
|
||||
const data = {
|
||||
actionUrl: `${config.appUrl}/trasporti/impostazioni`
|
||||
};
|
||||
const lang = user.lang || 'it';
|
||||
|
||||
if (channel === 'email' && user.email) {
|
||||
return this.sendEmail(user.email, type, data, lang);
|
||||
}
|
||||
|
||||
const telegId = user.profile?.teleg_id || user.notificationPreferences?.telegram?.chatId;
|
||||
if (channel === 'telegram' && telegId) {
|
||||
return this.sendTelegram(idapp, telegId, type, data, lang);
|
||||
}
|
||||
|
||||
const pushSub = user.notificationPreferences?.push?.subscription;
|
||||
if (channel === 'push' && pushSub) {
|
||||
return this.sendPush(pushSub, type, data, lang);
|
||||
}
|
||||
|
||||
if (channel === 'all') {
|
||||
return this.sendNotification(user, type, data, idapp);
|
||||
}
|
||||
|
||||
return { success: false, error: 'Invalid channel or not configured' };
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// HELPER DATE
|
||||
// =============================================================================
|
||||
|
||||
function formatDate(date) {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString('it-IT', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'short'
|
||||
});
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
return d.toLocaleTimeString('it-IT', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT
|
||||
// =============================================================================
|
||||
|
||||
module.exports = TrasportiNotifications;
|
||||
422
src/controllers/viaggi/settingsController.js
Normal file
422
src/controllers/viaggi/settingsController.js
Normal file
@@ -0,0 +1,422 @@
|
||||
// ============================================================
|
||||
// 🔧 SETTINGS CONTROLLER - Trasporti Solidali
|
||||
// ============================================================
|
||||
// File: server/controllers/viaggi/settingsController.js
|
||||
|
||||
const UserSettings = require('../../models/viaggi/UserSettings');
|
||||
|
||||
/**
|
||||
* 📄 GET /api/viaggi/settings
|
||||
* Ottieni le impostazioni dell'utente
|
||||
*/
|
||||
exports.getSettings = async (req, res) => {
|
||||
try {
|
||||
const { idapp } = req.query;
|
||||
const userId = req.user._id;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Ottieni o crea impostazioni
|
||||
const settings = await UserSettings.getOrCreateSettings(idapp, userId);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: settings.toClientJSON()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore getSettings:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel caricamento delle impostazioni',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📝 PUT /api/viaggi/settings
|
||||
* Aggiorna le impostazioni dell'utente
|
||||
*/
|
||||
exports.updateSettings = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const idapp = req.user.idapp;
|
||||
const updates = req.body;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
if (!updates || Object.keys(updates).length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Nessuna modifica specificata'
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna impostazioni
|
||||
const settings = await UserSettings.updateSettings(idapp, userId, updates);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Impostazioni aggiornate con successo',
|
||||
data: settings.toClientJSON()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore updateSettings:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'aggiornamento delle impostazioni',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📝 PATCH /api/viaggi/settings/notifications
|
||||
* Aggiorna solo le impostazioni notifiche
|
||||
*/
|
||||
exports.updateNotifications = async (req, res) => {
|
||||
try {
|
||||
const idapp = req.user.idapp;
|
||||
const userId = req.user._id;
|
||||
const { notifications } = req.body;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
if (!notifications) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'notifications è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna solo notifiche
|
||||
const settings = await UserSettings.updateSettings(idapp, userId, { notifications });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Notifiche aggiornate',
|
||||
data: settings.notifications
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore updateNotifications:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'aggiornamento delle notifiche',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📝 PATCH /api/viaggi/settings/privacy
|
||||
* Aggiorna solo le impostazioni privacy
|
||||
*/
|
||||
exports.updatePrivacy = async (req, res) => {
|
||||
try {
|
||||
const idapp = req.user.idapp;
|
||||
const userId = req.user._id;
|
||||
const { privacy } = req.body;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
if (!privacy) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'privacy è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna solo privacy
|
||||
const settings = await UserSettings.updateSettings(idapp, userId, { privacy });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Privacy aggiornata',
|
||||
data: settings.privacy
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore updatePrivacy:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'aggiornamento della privacy',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📝 PATCH /api/viaggi/settings/ride-preferences
|
||||
* Aggiorna solo le preferenze viaggi
|
||||
*/
|
||||
exports.updateRidePreferences = async (req, res) => {
|
||||
try {
|
||||
const idapp = req.user.idapp;
|
||||
const userId = req.user._id;
|
||||
const { ridePreferences } = req.body;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
if (!ridePreferences) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'ridePreferences è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna preferenze viaggi
|
||||
const settings = await UserSettings.updateSettings(idapp, userId, { ridePreferences });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Preferenze viaggi aggiornate',
|
||||
data: settings.ridePreferences
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore updateRidePreferences:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'aggiornamento delle preferenze',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📝 PATCH /api/viaggi/settings/interface
|
||||
* Aggiorna solo le impostazioni interfaccia
|
||||
*/
|
||||
exports.updateInterface = async (req, res) => {
|
||||
try {
|
||||
const idapp = req.user.idapp;
|
||||
const userId = req.user._id;
|
||||
const { interface: interfaceSettings } = req.body;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
if (!interfaceSettings) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'interface è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna interfaccia
|
||||
const settings = await UserSettings.updateSettings(idapp, userId, {
|
||||
interface: interfaceSettings
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Interfaccia aggiornata',
|
||||
data: settings.interface
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore updateInterface:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'aggiornamento dell\'interfaccia',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 🔄 POST /api/viaggi/settings/reset
|
||||
* Reset impostazioni ai valori predefiniti
|
||||
*/
|
||||
exports.resetSettings = async (req, res) => {
|
||||
try {
|
||||
const idapp = req.user.idapp;
|
||||
const userId = req.user._id;
|
||||
const { section } = req.body; // Opzionale: resetta solo una sezione
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Trova impostazioni esistenti
|
||||
let settings = await UserSettings.findOne({ idapp, userId });
|
||||
|
||||
if (!settings) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impostazioni non trovate'
|
||||
});
|
||||
}
|
||||
|
||||
if (section) {
|
||||
// Reset solo di una sezione specifica
|
||||
const schema = UserSettings.schema.paths[section];
|
||||
if (!schema) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Sezione non valida'
|
||||
});
|
||||
}
|
||||
|
||||
// Ottieni valori predefiniti dalla schema
|
||||
settings[section] = schema.defaultValue || {};
|
||||
} else {
|
||||
// Reset completo - cancella e ricrea
|
||||
await UserSettings.deleteOne({ idapp, userId });
|
||||
settings = await UserSettings.getOrCreateSettings(idapp, userId);
|
||||
}
|
||||
|
||||
await settings.save();
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: section
|
||||
? `Sezione ${section} resettata`
|
||||
: 'Impostazioni resettate ai valori predefiniti',
|
||||
data: settings.toClientJSON()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore resetSettings:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel reset delle impostazioni',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 GET /api/viaggi/settings/export
|
||||
* Esporta tutte le impostazioni (per backup o trasferimento)
|
||||
*/
|
||||
exports.exportSettings = async (req, res) => {
|
||||
try {
|
||||
const { idapp } = req.query;
|
||||
const userId = req.user._id;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
const settings = await UserSettings.findOne({ idapp, userId });
|
||||
|
||||
if (!settings) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Impostazioni non trovate'
|
||||
});
|
||||
}
|
||||
|
||||
// Esporta in formato JSON pulito
|
||||
const exportData = {
|
||||
exportDate: new Date().toISOString(),
|
||||
userId: userId.toString(),
|
||||
idapp,
|
||||
settings: settings.toClientJSON()
|
||||
};
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: exportData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore exportSettings:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'esportazione delle impostazioni',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📥 POST /api/viaggi/settings/import
|
||||
* Importa impostazioni da backup
|
||||
*/
|
||||
exports.importSettings = async (req, res) => {
|
||||
try {
|
||||
const idapp = req.user.idapp;
|
||||
const userId = req.user._id;
|
||||
const { settings: importedSettings } = req.body;
|
||||
|
||||
// Validazione
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
if (!importedSettings) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'settings è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna con le impostazioni importate
|
||||
const settings = await UserSettings.updateSettings(idapp, userId, importedSettings);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Impostazioni importate con successo',
|
||||
data: settings.toClientJSON()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore importSettings:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nell\'importazione delle impostazioni',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
506
src/controllers/viaggi/trasportiNotificationsController.js
Normal file
506
src/controllers/viaggi/trasportiNotificationsController.js
Normal file
@@ -0,0 +1,506 @@
|
||||
/**
|
||||
* trasportiNotificationsController.js
|
||||
*
|
||||
* Controller API per gestire le preferenze di notifica utente.
|
||||
* Funziona insieme a TrasportiNotifications.js
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const TrasportiNotifications = require('./TrasportiNotifications');
|
||||
|
||||
// =============================================================================
|
||||
// SCHEMA PREFERENZE (da aggiungere al model User)
|
||||
// =============================================================================
|
||||
|
||||
const notificationPreferencesSchema = new mongoose.Schema({
|
||||
email: {
|
||||
enabled: { type: Boolean, default: true },
|
||||
newRideRequest: { type: Boolean, default: true },
|
||||
requestAccepted: { type: Boolean, default: true },
|
||||
requestRejected: { type: Boolean, default: true },
|
||||
rideReminder24h: { type: Boolean, default: true },
|
||||
rideReminder2h: { type: Boolean, default: true },
|
||||
rideCancelled: { type: Boolean, default: true },
|
||||
newMessage: { type: Boolean, default: true },
|
||||
newCommunityRide: { type: Boolean, default: false },
|
||||
weeklyDigest: { type: Boolean, default: false }
|
||||
},
|
||||
telegram: {
|
||||
enabled: { type: Boolean, default: false },
|
||||
chatId: { type: Number, default: 0 },
|
||||
username: { type: String, default: '' },
|
||||
connectedAt: { type: Date },
|
||||
newRideRequest: { type: Boolean, default: true },
|
||||
requestAccepted: { type: Boolean, default: true },
|
||||
requestRejected: { type: Boolean, default: true },
|
||||
rideReminder24h: { type: Boolean, default: true },
|
||||
rideReminder2h: { type: Boolean, default: true },
|
||||
rideCancelled: { type: Boolean, default: true },
|
||||
newMessage: { type: Boolean, default: true }
|
||||
},
|
||||
push: {
|
||||
enabled: { type: Boolean, default: false },
|
||||
subscription: { type: mongoose.Schema.Types.Mixed },
|
||||
subscribedAt: { type: Date },
|
||||
newRideRequest: { type: Boolean, default: true },
|
||||
requestAccepted: { type: Boolean, default: true },
|
||||
requestRejected: { type: Boolean, default: true },
|
||||
rideReminder24h: { type: Boolean, default: true },
|
||||
rideReminder2h: { type: Boolean, default: true },
|
||||
rideCancelled: { type: Boolean, default: true },
|
||||
newMessage: { type: Boolean, default: true }
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// =============================================================================
|
||||
// STORAGE CODICI TELEGRAM (in-memory, usa Redis in produzione)
|
||||
// =============================================================================
|
||||
|
||||
const telegramConnectCodes = new Map();
|
||||
|
||||
// Pulizia codici scaduti ogni 5 minuti
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [code, data] of telegramConnectCodes) {
|
||||
if (now - data.createdAt > 10 * 60 * 1000) { // 10 minuti
|
||||
telegramConnectCodes.delete(code);
|
||||
}
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
/**
|
||||
* Genera codice random 6 caratteri
|
||||
*/
|
||||
function generateCode() {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Escludo caratteri ambigui
|
||||
let code = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTROLLER
|
||||
// =============================================================================
|
||||
|
||||
const trasportiNotificationsController = {
|
||||
|
||||
// Esponi schema per User model
|
||||
notificationPreferencesSchema,
|
||||
|
||||
/**
|
||||
* GET /api/trasporti/notifications/preferences
|
||||
* Ottiene preferenze notifiche utente
|
||||
*/
|
||||
async getNotificationPreferences(req, res) {
|
||||
try {
|
||||
const user = req.user;
|
||||
|
||||
// Default preferences se non esistono
|
||||
const defaultPrefs = {
|
||||
email: {
|
||||
enabled: true,
|
||||
newRideRequest: true,
|
||||
requestAccepted: true,
|
||||
requestRejected: true,
|
||||
rideReminder24h: true,
|
||||
rideReminder2h: true,
|
||||
rideCancelled: true,
|
||||
newMessage: true,
|
||||
newCommunityRide: false,
|
||||
weeklyDigest: false
|
||||
},
|
||||
telegram: {
|
||||
enabled: false,
|
||||
chatId: user.profile?.teleg_id || 0,
|
||||
username: user.profile?.teleg_username || '',
|
||||
newRideRequest: true,
|
||||
requestAccepted: true,
|
||||
requestRejected: true,
|
||||
rideReminder24h: true,
|
||||
rideReminder2h: true,
|
||||
rideCancelled: true,
|
||||
newMessage: true
|
||||
},
|
||||
push: {
|
||||
enabled: false,
|
||||
newRideRequest: true,
|
||||
requestAccepted: true,
|
||||
requestRejected: true,
|
||||
rideReminder24h: true,
|
||||
rideReminder2h: true,
|
||||
rideCancelled: true,
|
||||
newMessage: true
|
||||
}
|
||||
};
|
||||
|
||||
// Merge con preferenze salvate
|
||||
const prefs = user.notificationPreferences || {};
|
||||
const mergedPrefs = {
|
||||
email: { ...defaultPrefs.email, ...prefs.email },
|
||||
telegram: { ...defaultPrefs.telegram, ...prefs.telegram },
|
||||
push: { ...defaultPrefs.push, ...prefs.push }
|
||||
};
|
||||
|
||||
// Sync chatId da profile se presente
|
||||
if (user.profile?.teleg_id && !mergedPrefs.telegram.chatId) {
|
||||
mergedPrefs.telegram.chatId = user.profile.teleg_id;
|
||||
mergedPrefs.telegram.enabled = true;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
email: user.email,
|
||||
preferences: mergedPrefs,
|
||||
vapidPublicKey: TrasportiNotifications.config.vapidPublicKey,
|
||||
telegramBotUsername: process.env.TELEGRAM_BOT_USERNAME || 'TrasportiSolidaliBot'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getNotificationPreferences error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT /api/trasporti/notifications/preferences
|
||||
* Aggiorna preferenze notifiche
|
||||
*/
|
||||
async updateNotificationPreferences(req, res) {
|
||||
try {
|
||||
const { User } = require('../../models/user');
|
||||
const { email, telegram, push } = req.body;
|
||||
|
||||
const updateData = {};
|
||||
|
||||
// Email preferences
|
||||
if (email) {
|
||||
Object.keys(email).forEach(key => {
|
||||
if (key !== 'enabled' || typeof email[key] === 'boolean') {
|
||||
updateData[`notificationPreferences.email.${key}`] = email[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Telegram preferences (escludi chatId, username - gestiti via connect)
|
||||
if (telegram) {
|
||||
const allowedKeys = ['enabled', 'newRideRequest', 'requestAccepted', 'requestRejected',
|
||||
'rideReminder24h', 'rideReminder2h', 'rideCancelled', 'newMessage'];
|
||||
Object.keys(telegram).forEach(key => {
|
||||
if (allowedKeys.includes(key)) {
|
||||
updateData[`notificationPreferences.telegram.${key}`] = telegram[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Push preferences (escludi subscription - gestito via subscribe)
|
||||
if (push) {
|
||||
const allowedKeys = ['enabled', 'newRideRequest', 'requestAccepted', 'requestRejected',
|
||||
'rideReminder24h', 'rideReminder2h', 'rideCancelled', 'newMessage'];
|
||||
Object.keys(push).forEach(key => {
|
||||
if (allowedKeys.includes(key)) {
|
||||
updateData[`notificationPreferences.push.${key}`] = push[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await User.updateOne(
|
||||
{ _id: req.user._id },
|
||||
{ $set: updateData }
|
||||
);
|
||||
|
||||
res.json({ success: true, message: 'Preferenze aggiornate' });
|
||||
} catch (error) {
|
||||
console.error('updateNotificationPreferences error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/trasporti/notifications/telegram/code
|
||||
* Genera codice per connessione Telegram
|
||||
*/
|
||||
async generateTelegramCode(req, res) {
|
||||
try {
|
||||
const userId = req.user._id.toString();
|
||||
|
||||
// Rimuovi codici esistenti per questo utente
|
||||
for (const [code, data] of telegramConnectCodes) {
|
||||
if (data.userId === userId) {
|
||||
telegramConnectCodes.delete(code);
|
||||
}
|
||||
}
|
||||
|
||||
// Genera nuovo codice
|
||||
let code;
|
||||
do {
|
||||
code = generateCode();
|
||||
} while (telegramConnectCodes.has(code));
|
||||
|
||||
// Salva
|
||||
telegramConnectCodes.set(code, {
|
||||
userId,
|
||||
createdAt: Date.now(),
|
||||
chatId: null,
|
||||
username: null
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
code,
|
||||
expiresIn: 600, // 10 minuti
|
||||
botUsername: process.env.TELEGRAM_BOT_USERNAME || 'TrasportiSolidaliBot',
|
||||
instructions: `Invia "${code}" al bot @${process.env.TELEGRAM_BOT_USERNAME || 'TrasportiSolidaliBot'} su Telegram`
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('generateTelegramCode error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/trasporti/notifications/telegram/connect
|
||||
* Completa connessione Telegram dopo validazione codice dal bot
|
||||
*/
|
||||
async connectTelegram(req, res) {
|
||||
try {
|
||||
const { User } = require('../../models/user');
|
||||
const { code } = req.body;
|
||||
|
||||
if (!code) {
|
||||
return res.status(400).json({ success: false, message: 'Codice richiesto' });
|
||||
}
|
||||
|
||||
const codeData = telegramConnectCodes.get(code.toUpperCase());
|
||||
|
||||
if (!codeData) {
|
||||
return res.status(400).json({ success: false, message: 'Codice non valido o scaduto' });
|
||||
}
|
||||
|
||||
if (codeData.userId !== req.user._id.toString()) {
|
||||
return res.status(400).json({ success: false, message: 'Codice non valido' });
|
||||
}
|
||||
|
||||
// Verifica che il bot abbia validato il codice (impostando chatId)
|
||||
if (!codeData.chatId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invia prima il codice al bot su Telegram',
|
||||
needsBotInteraction: true
|
||||
});
|
||||
}
|
||||
|
||||
// Aggiorna utente
|
||||
await User.updateOne(
|
||||
{ _id: req.user._id },
|
||||
{
|
||||
$set: {
|
||||
'notificationPreferences.telegram.enabled': true,
|
||||
'notificationPreferences.telegram.chatId': codeData.chatId,
|
||||
'notificationPreferences.telegram.username': codeData.username || '',
|
||||
'notificationPreferences.telegram.connectedAt': new Date(),
|
||||
// Retrocompatibilità con profile.teleg_id
|
||||
'profile.teleg_id': codeData.chatId,
|
||||
'profile.teleg_username': codeData.username || ''
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Rimuovi codice usato
|
||||
telegramConnectCodes.delete(code.toUpperCase());
|
||||
|
||||
// Invia messaggio benvenuto
|
||||
const idapp = req.user.idapp;
|
||||
await TrasportiNotifications.sendTelegram(
|
||||
idapp,
|
||||
codeData.chatId,
|
||||
TrasportiNotifications.NotificationType.WELCOME,
|
||||
{},
|
||||
req.user.lang || 'it'
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Telegram connesso!',
|
||||
data: {
|
||||
chatId: codeData.chatId,
|
||||
username: codeData.username
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('connectTelegram error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE /api/trasporti/notifications/telegram/disconnect
|
||||
* Disconnette Telegram
|
||||
*/
|
||||
async disconnectTelegram(req, res) {
|
||||
try {
|
||||
const { User } = require('../../models/user');
|
||||
|
||||
// Ottieni chatId prima di disconnettere per inviare messaggio
|
||||
const chatId = req.user.notificationPreferences?.telegram?.chatId || req.user.profile?.teleg_id;
|
||||
const idapp = req.user.idapp;
|
||||
|
||||
// Aggiorna utente
|
||||
await User.updateOne(
|
||||
{ _id: req.user._id },
|
||||
{
|
||||
$set: {
|
||||
'notificationPreferences.telegram.enabled': false,
|
||||
'notificationPreferences.telegram.chatId': 0,
|
||||
'notificationPreferences.telegram.username': '',
|
||||
'notificationPreferences.telegram.connectedAt': null,
|
||||
'profile.teleg_id': 0,
|
||||
'profile.teleg_username': ''
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Invia messaggio di disconnessione
|
||||
if (chatId && idapp) {
|
||||
const MyTelegramBot = require('./telegram/telegrambot');
|
||||
await MyTelegramBot.local_sendMsgTelegramByIdTelegram(
|
||||
idapp,
|
||||
chatId,
|
||||
'👋 Telegram disconnesso da Trasporti Solidali.\n\nPuoi riconnettere in qualsiasi momento dalla pagina impostazioni.',
|
||||
null, null, false, null, ''
|
||||
);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Telegram disconnesso' });
|
||||
} catch (error) {
|
||||
console.error('disconnectTelegram error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/trasporti/notifications/push/subscribe
|
||||
* Registra subscription push
|
||||
*/
|
||||
async subscribePushNotifications(req, res) {
|
||||
try {
|
||||
const { User } = require('../../models/user');
|
||||
const { subscription } = req.body;
|
||||
|
||||
if (!subscription || !subscription.endpoint) {
|
||||
return res.status(400).json({ success: false, message: 'Subscription non valida' });
|
||||
}
|
||||
|
||||
await User.updateOne(
|
||||
{ _id: req.user._id },
|
||||
{
|
||||
$set: {
|
||||
'notificationPreferences.push.enabled': true,
|
||||
'notificationPreferences.push.subscription': subscription,
|
||||
'notificationPreferences.push.subscribedAt': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
res.json({ success: true, message: 'Push notifications attivate' });
|
||||
} catch (error) {
|
||||
console.error('subscribePushNotifications error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE /api/trasporti/notifications/push/unsubscribe
|
||||
* Rimuove subscription push
|
||||
*/
|
||||
async unsubscribePushNotifications(req, res) {
|
||||
try {
|
||||
const { User } = require('../../models/user');
|
||||
|
||||
await User.updateOne(
|
||||
{ _id: req.user._id },
|
||||
{
|
||||
$set: {
|
||||
'notificationPreferences.push.enabled': false,
|
||||
'notificationPreferences.push.subscription': null
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
res.json({ success: true, message: 'Push notifications disattivate' });
|
||||
} catch (error) {
|
||||
console.error('unsubscribePushNotifications error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/trasporti/notifications/test
|
||||
* Invia notifica di test
|
||||
*/
|
||||
async sendTestNotification(req, res) {
|
||||
try {
|
||||
const { channel } = req.body; // 'email', 'telegram', 'push', 'all'
|
||||
const idapp = req.user.idapp;
|
||||
|
||||
const result = await TrasportiNotifications.sendTestNotification(req.user, channel, idapp);
|
||||
|
||||
if (result.success) {
|
||||
res.json({ success: true, message: `Notifica di test inviata su ${channel}` });
|
||||
} else {
|
||||
res.status(400).json({ success: false, message: result.error });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('sendTestNotification error:', error);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler per il bot Telegram quando riceve un codice
|
||||
* Chiamare questa funzione dal tuo telegrambot.js
|
||||
*/
|
||||
handleTelegramCodeFromBot(code, chatId, username) {
|
||||
const codeUpper = code.toUpperCase();
|
||||
const codeData = telegramConnectCodes.get(codeUpper);
|
||||
|
||||
if (!codeData) {
|
||||
return { success: false, error: 'Codice non valido o scaduto' };
|
||||
}
|
||||
|
||||
// Aggiorna con chatId e username
|
||||
codeData.chatId = chatId;
|
||||
codeData.username = username;
|
||||
telegramConnectCodes.set(codeUpper, codeData);
|
||||
|
||||
return { success: true, userId: codeData.userId };
|
||||
},
|
||||
|
||||
/**
|
||||
* Rimuovi subscription push scadute
|
||||
* Chiamare quando si riceve errore 410/404
|
||||
*/
|
||||
async removePushSubscription(userId) {
|
||||
try {
|
||||
const { User } = require('../../models/user');
|
||||
await User.updateOne(
|
||||
{ _id: userId },
|
||||
{
|
||||
$set: {
|
||||
'notificationPreferences.push.subscription': null,
|
||||
'notificationPreferences.push.enabled': false
|
||||
}
|
||||
}
|
||||
);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = trasportiNotificationsController;
|
||||
219
src/controllers/viaggi/widgetController.js
Normal file
219
src/controllers/viaggi/widgetController.js
Normal file
@@ -0,0 +1,219 @@
|
||||
// ============================================================
|
||||
// 📊 WIDGET & STATS CONTROLLER - Trasporti Solidali
|
||||
// ============================================================
|
||||
// File: server/controllers/viaggi/widgetController.js
|
||||
|
||||
const Ride = require('../../models/viaggi/Ride');
|
||||
const RideRequest = require('../../models/viaggi/RideRequest');
|
||||
const Feedback = require('../../models/viaggi/Feedback');
|
||||
const Chat = require('../../models/viaggi/Chat');
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
/**
|
||||
* 📊 GET /api/viaggi/widget/data
|
||||
* Ottieni dati per il widget dashboard
|
||||
*/
|
||||
exports.getWidgetData = async (req, res) => {
|
||||
try {
|
||||
const { idapp } = req.query;
|
||||
const userId = req.user._id;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Query parallele per ottimizzare
|
||||
const [
|
||||
offersCount,
|
||||
requestsCount,
|
||||
recentRides,
|
||||
myActiveRides,
|
||||
pendingRequestsCount,
|
||||
unreadMessagesCount
|
||||
] = await Promise.all([
|
||||
// Conta offerte attive
|
||||
Ride.countDocuments({
|
||||
idapp,
|
||||
type: 'offer',
|
||||
status: 'active',
|
||||
departureDate: { $gte: now }
|
||||
}),
|
||||
|
||||
// Conta richieste attive
|
||||
Ride.countDocuments({
|
||||
idapp,
|
||||
type: 'request',
|
||||
status: 'active',
|
||||
departureDate: { $gte: now }
|
||||
}),
|
||||
|
||||
// Ultimi viaggi pubblicati (non propri)
|
||||
Ride.find({
|
||||
idapp,
|
||||
userId: { $ne: userId },
|
||||
status: 'active',
|
||||
departureDate: { $gte: now }
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(5)
|
||||
.populate('userId', 'name surname profile')
|
||||
.lean(),
|
||||
|
||||
// I miei viaggi attivi
|
||||
Ride.find({
|
||||
idapp,
|
||||
userId: userId,
|
||||
status: 'active',
|
||||
departureDate: { $gte: now }
|
||||
})
|
||||
.sort({ departureDate: 1 })
|
||||
.limit(3)
|
||||
.lean(),
|
||||
|
||||
// Richieste pendenti ricevute (per i miei viaggi)
|
||||
RideRequest.countDocuments({
|
||||
idapp,
|
||||
driverUserId: userId,
|
||||
status: 'pending'
|
||||
}),
|
||||
|
||||
// Messaggi non letti
|
||||
Chat.countDocuments({
|
||||
idapp,
|
||||
participants: userId,
|
||||
isDeleted: false,
|
||||
[`deletedBy.${userId}`]: { $ne: true },
|
||||
'messages': {
|
||||
$elemMatch: {
|
||||
senderId: { $ne: userId },
|
||||
readBy: { $ne: userId }
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
// Calcola "matches" - viaggi compatibili con le mie richieste
|
||||
let matchesCount = 0;
|
||||
const myRequests = await Ride.find({
|
||||
idapp,
|
||||
userId: userId,
|
||||
type: 'request',
|
||||
status: 'active',
|
||||
departureDate: { $gte: now }
|
||||
}).select('departure destination departureDate').lean();
|
||||
|
||||
if (myRequests.length > 0) {
|
||||
// Per ogni mia richiesta, cerca offerte compatibili
|
||||
for (const request of myRequests) {
|
||||
const compatibleOffers = await Ride.countDocuments({
|
||||
idapp,
|
||||
userId: { $ne: userId },
|
||||
type: 'offer',
|
||||
status: 'active',
|
||||
departureDate: {
|
||||
$gte: new Date(request.departureDate.getTime() - 2 * 60 * 60 * 1000), // -2h
|
||||
$lte: new Date(request.departureDate.getTime() + 2 * 60 * 60 * 1000) // +2h
|
||||
},
|
||||
// Potresti aggiungere filtri geografici qui
|
||||
'departure.city': request.departure?.city,
|
||||
'destination.city': request.destination?.city
|
||||
});
|
||||
matchesCount += compatibleOffers;
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
stats: {
|
||||
offers: offersCount,
|
||||
requests: requestsCount,
|
||||
matches: matchesCount
|
||||
},
|
||||
recentRides: recentRides,
|
||||
myActiveRides: myActiveRides,
|
||||
pendingRequests: pendingRequestsCount,
|
||||
unreadMessages: unreadMessagesCount
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore getWidgetData:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel caricamento dei dati widget',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 GET /api/viaggi/stats/quick
|
||||
* Statistiche rapide per badge/notifiche
|
||||
*/
|
||||
exports.getQuickStats = async (req, res) => {
|
||||
try {
|
||||
const { idapp } = req.query;
|
||||
const userId = req.user._id;
|
||||
|
||||
if (!idapp) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'idapp è richiesto'
|
||||
});
|
||||
}
|
||||
|
||||
const [pendingRequests, unreadMessages, activeRides] = await Promise.all([
|
||||
RideRequest.countDocuments({
|
||||
idapp,
|
||||
driverUserId: userId,
|
||||
status: 'pending'
|
||||
}),
|
||||
|
||||
Chat.countDocuments({
|
||||
idapp,
|
||||
participants: userId,
|
||||
isDeleted: false,
|
||||
[`deletedBy.${userId}`]: { $ne: true },
|
||||
'messages': {
|
||||
$elemMatch: {
|
||||
senderId: { $ne: userId },
|
||||
readBy: { $ne: userId }
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
Ride.countDocuments({
|
||||
idapp,
|
||||
userId: userId,
|
||||
status: 'active',
|
||||
departureDate: { $gte: new Date() }
|
||||
})
|
||||
]);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
pendingRequests,
|
||||
unreadMessages,
|
||||
activeRides,
|
||||
totalNotifications: pendingRequests + unreadMessages
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Errore getQuickStats:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Errore nel caricamento delle statistiche rapide',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = exports;
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
77
src/helpers/recurrenceHelper.js
Normal file
77
src/helpers/recurrenceHelper.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// Helper per calcolare le date dalle ricorrenze
|
||||
const getRecurrenceDates = (ride, startRange, endRange) => {
|
||||
const { recurrence, departureDate } = ride;
|
||||
|
||||
if (!recurrence || recurrence.type === 'once') {
|
||||
return [new Date(departureDate)];
|
||||
}
|
||||
|
||||
const dates = [];
|
||||
const start = new Date(startRange || recurrence.startDate || departureDate);
|
||||
const end = new Date(endRange || recurrence.endDate || new Date(start.getTime() + 365 * 24 * 60 * 60 * 1000)); // Default 1 anno
|
||||
|
||||
const excludedDatesSet = new Set(
|
||||
(recurrence.excludedDates || []).map(d => new Date(d).toISOString().split('T')[0])
|
||||
);
|
||||
|
||||
switch (recurrence.type) {
|
||||
case 'weekly':
|
||||
if (!recurrence.daysOfWeek || recurrence.daysOfWeek.length === 0) break;
|
||||
|
||||
let current = new Date(start);
|
||||
while (current <= end) {
|
||||
const dayOfWeek = current.getDay();
|
||||
if (recurrence.daysOfWeek.includes(dayOfWeek)) {
|
||||
const dateStr = current.toISOString().split('T')[0];
|
||||
if (!excludedDatesSet.has(dateStr)) {
|
||||
dates.push(new Date(current));
|
||||
}
|
||||
}
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'custom_days':
|
||||
if (!recurrence.daysOfWeek || recurrence.daysOfWeek.length === 0) break;
|
||||
|
||||
let curr = new Date(start);
|
||||
while (curr <= end) {
|
||||
const dayOfWeek = curr.getDay();
|
||||
if (recurrence.daysOfWeek.includes(dayOfWeek)) {
|
||||
const dateStr = curr.toISOString().split('T')[0];
|
||||
if (!excludedDatesSet.has(dateStr)) {
|
||||
dates.push(new Date(curr));
|
||||
}
|
||||
}
|
||||
curr.setDate(curr.getDate() + 1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'custom_dates':
|
||||
if (!recurrence.customDates || recurrence.customDates.length === 0) break;
|
||||
|
||||
recurrence.customDates.forEach(date => {
|
||||
const d = new Date(date);
|
||||
if (d >= start && d <= end) {
|
||||
const dateStr = d.toISOString().split('T')[0];
|
||||
if (!excludedDatesSet.has(dateStr)) {
|
||||
dates.push(d);
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return dates.length > 0 ? dates : [new Date(departureDate)];
|
||||
};
|
||||
|
||||
const isRideActiveOnDate = (ride, targetDate) => {
|
||||
const dates = getRecurrenceDates(ride, targetDate, targetDate);
|
||||
const targetStr = new Date(targetDate).toISOString().split('T')[0];
|
||||
return dates.some(d => d.toISOString().split('T')[0] === targetStr);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getRecurrenceDates,
|
||||
isRideActiveOnDate
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
241
src/models/Message.js
Normal file
241
src/models/Message.js
Normal file
@@ -0,0 +1,241 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const MessageSchema = new Schema({
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
chatId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Chat',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
senderId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 2000
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['text', 'ride_share', 'location', 'image', 'voice', 'system', 'ride_request', 'ride_accepted', 'ride_rejected'],
|
||||
default: 'text'
|
||||
},
|
||||
metadata: {
|
||||
// Per messaggi speciali (condivisione viaggio, posizione, ecc.)
|
||||
rideId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Ride'
|
||||
},
|
||||
rideRequestId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'RideRequest'
|
||||
},
|
||||
location: {
|
||||
lat: Number,
|
||||
lng: Number,
|
||||
address: String
|
||||
},
|
||||
imageUrl: String,
|
||||
voiceUrl: String,
|
||||
voiceDuration: Number,
|
||||
systemAction: String
|
||||
},
|
||||
readBy: [{
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
readAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}],
|
||||
replyTo: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Message'
|
||||
},
|
||||
isEdited: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
editedAt: {
|
||||
type: Date
|
||||
},
|
||||
isDeleted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
deletedAt: {
|
||||
type: Date
|
||||
},
|
||||
reactions: [{
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
emoji: String,
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indici per query efficienti
|
||||
MessageSchema.index({ chatId: 1, createdAt: -1 });
|
||||
MessageSchema.index({ senderId: 1, createdAt: -1 });
|
||||
MessageSchema.index({ idapp: 1, chatId: 1 });
|
||||
|
||||
// Virtual per verificare se il messaggio è stato letto da tutti
|
||||
MessageSchema.virtual('isReadByAll').get(function() {
|
||||
// Logica da implementare confrontando con partecipanti chat
|
||||
return false;
|
||||
});
|
||||
|
||||
// Metodo per marcare come letto da un utente
|
||||
MessageSchema.methods.markAsReadBy = function(userId) {
|
||||
const alreadyRead = this.readBy.some(
|
||||
r => r.userId.toString() === userId.toString()
|
||||
);
|
||||
|
||||
if (!alreadyRead) {
|
||||
this.readBy.push({
|
||||
userId,
|
||||
readAt: new Date()
|
||||
});
|
||||
return this.save();
|
||||
}
|
||||
return Promise.resolve(this);
|
||||
};
|
||||
|
||||
// Metodo per verificare se è stato letto da un utente
|
||||
MessageSchema.methods.isReadBy = function(userId) {
|
||||
return this.readBy.some(
|
||||
r => r.userId.toString() === userId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
// Metodo per aggiungere reazione
|
||||
MessageSchema.methods.addReaction = function(userId, emoji) {
|
||||
// Rimuovi eventuale reazione precedente dello stesso utente
|
||||
this.reactions = this.reactions.filter(
|
||||
r => r.userId.toString() !== userId.toString()
|
||||
);
|
||||
|
||||
this.reactions.push({
|
||||
userId,
|
||||
emoji,
|
||||
createdAt: new Date()
|
||||
});
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per rimuovere reazione
|
||||
MessageSchema.methods.removeReaction = function(userId) {
|
||||
this.reactions = this.reactions.filter(
|
||||
r => r.userId.toString() !== userId.toString()
|
||||
);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per soft delete
|
||||
MessageSchema.methods.softDelete = function() {
|
||||
this.isDeleted = true;
|
||||
this.deletedAt = new Date();
|
||||
this.text = '[Messaggio eliminato]';
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per modificare testo
|
||||
MessageSchema.methods.editText = function(newText) {
|
||||
this.text = newText;
|
||||
this.isEdited = true;
|
||||
this.editedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere messaggi di una chat con paginazione
|
||||
// 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.img')
|
||||
.populate('replyTo', 'text senderId')
|
||||
.sort({ createdAt: -1 }) // -1 = più recente prima
|
||||
.limit(limit)
|
||||
};
|
||||
|
||||
// Metodo statico per creare messaggio di sistema
|
||||
MessageSchema.statics.createSystemMessage = async function(idapp, chatId, text, action = null) {
|
||||
const message = new this({
|
||||
idapp,
|
||||
chatId,
|
||||
senderId: null, // Sistema
|
||||
text,
|
||||
type: 'system',
|
||||
metadata: {
|
||||
systemAction: action
|
||||
}
|
||||
});
|
||||
return message.save();
|
||||
};
|
||||
|
||||
// Metodo statico per contare messaggi non letti
|
||||
MessageSchema.statics.countUnreadForUser = async function(idapp, chatId, userId) {
|
||||
return this.countDocuments({
|
||||
idapp,
|
||||
chatId,
|
||||
isDeleted: false,
|
||||
senderId: { $ne: userId },
|
||||
'readBy.userId': { $ne: userId }
|
||||
});
|
||||
};
|
||||
|
||||
// Hook post-save per aggiornare la chat
|
||||
MessageSchema.post('save', async function(doc) {
|
||||
try {
|
||||
const Chat = mongoose.model('Chat');
|
||||
const chat = await Chat.findById(doc.chatId);
|
||||
|
||||
if (chat) {
|
||||
await chat.updateLastMessage(doc);
|
||||
await chat.incrementUnread(doc.senderId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Errore aggiornamento chat dopo messaggio:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const Message = mongoose.model('Message', MessageSchema);
|
||||
|
||||
module.exports = Message;
|
||||
@@ -56,7 +56,7 @@ ContribtypeSchema.statics.findAllIdApp = async function (idapp) {
|
||||
return await Contribtype.find(myfind).lean();
|
||||
};
|
||||
|
||||
const Contribtype = mongoose.model('Contribtype', ContribtypeSchema);
|
||||
const Contribtype = mongoose.models.Contribtype || mongoose.model('Contribtype', ContribtypeSchema);
|
||||
|
||||
Contribtype.createIndexes()
|
||||
.then(() => { })
|
||||
|
||||
@@ -175,6 +175,7 @@ const SiteSchema = new Schema({
|
||||
enableEcommerce: { type: Boolean, default: false },
|
||||
enableAI: { type: Boolean, default: false },
|
||||
enablePoster: { type: Boolean, default: false },
|
||||
enableTrasporti: { type: Boolean, default: false },
|
||||
enableGroups: { type: Boolean, default: false },
|
||||
enableCircuits: { type: Boolean, default: false },
|
||||
enableGoods: { type: Boolean, default: false },
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const mongoose = require('mongoose').set('debug', false);
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const validator = require('validator');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const _ = require('lodash');
|
||||
@@ -30,6 +32,8 @@ const i18n = require('i18n');
|
||||
|
||||
const shared_consts = require('../tools/shared_nodejs');
|
||||
|
||||
const { notificationPreferencesSchema } = require('../controllers/viaggi/trasportiNotificationsController');
|
||||
|
||||
mongoose.Promise = global.Promise;
|
||||
|
||||
mongoose.level = 'F';
|
||||
@@ -285,6 +289,10 @@ const UserSchema = new mongoose.Schema(
|
||||
cell: {
|
||||
type: String,
|
||||
},
|
||||
cellVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
country_pay: {
|
||||
type: String,
|
||||
},
|
||||
@@ -584,6 +592,266 @@ const UserSchema = new mongoose.Schema(
|
||||
],
|
||||
version: { type: Number },
|
||||
insert_circuito_ita: { type: Boolean },
|
||||
|
||||
// ============ DRIVER PROFILE ============
|
||||
driverProfile: {
|
||||
isDriver: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bio: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500,
|
||||
},
|
||||
vehicles: [
|
||||
{
|
||||
type: {
|
||||
type: String,
|
||||
default: 'auto',
|
||||
},
|
||||
brand: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
colorHex: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
year: {
|
||||
type: Number,
|
||||
},
|
||||
seats: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 50,
|
||||
},
|
||||
licensePlate: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
features: [
|
||||
{
|
||||
type: String,
|
||||
},
|
||||
],
|
||||
photos: [
|
||||
{
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
],
|
||||
isDefault: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
ridesCompletedAsDriver: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
ridesCompletedAsPassenger: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
averageRating: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 5,
|
||||
},
|
||||
totalRatings: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
verifiedDriver: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
licenseVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
licenseNumber: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
licenseExpiry: {
|
||||
type: Date,
|
||||
},
|
||||
memberSince: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
responseRate: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
responseTime: {
|
||||
type: String,
|
||||
default: 'within_day',
|
||||
},
|
||||
totalKmShared: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
co2Saved: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
// kg di CO2 risparmiati
|
||||
},
|
||||
badges: [
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
earnedAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
},
|
||||
],
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
min: 1,
|
||||
},
|
||||
points: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
// ============ PREFERENCES ============
|
||||
preferences: {
|
||||
// Preferenze di viaggio
|
||||
smoking: {
|
||||
type: String,
|
||||
default: 'no',
|
||||
},
|
||||
pets: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
},
|
||||
music: {
|
||||
type: String,
|
||||
default: 'moderate',
|
||||
},
|
||||
conversation: {
|
||||
type: String,
|
||||
default: 'moderate',
|
||||
},
|
||||
|
||||
// Notifiche
|
||||
notifications: {
|
||||
rideRequests: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rideAccepted: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
rideReminders: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
messages: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
marketing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
pushEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
emailEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Privacy
|
||||
privacy: {
|
||||
showEmail: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showPhone: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showLastName: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showRides: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Località preferite
|
||||
favoriteLocations: [
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
city: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
coordinates: {
|
||||
lat: Number,
|
||||
lng: Number,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'other',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// Lingue parlate
|
||||
languages: [
|
||||
{
|
||||
type: String,
|
||||
},
|
||||
],
|
||||
|
||||
// Metodo di pagamento preferito
|
||||
preferredContribType: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Contribtype',
|
||||
},
|
||||
},
|
||||
},
|
||||
notificationPreferences: {
|
||||
type: notificationPreferencesSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
updatedAt: { type: Date, default: Date.now },
|
||||
},
|
||||
@@ -6810,7 +7078,9 @@ UserSchema.statics.addNewSite = async function (idappPass, body) {
|
||||
}
|
||||
|
||||
if (arrSite && arrSite.length === 1 && numutenti < 2) {
|
||||
const MyTelegramBot = require('../telegram/telegrambot');
|
||||
//const MyTelegramBot = require('../telegram/telegrambot');
|
||||
const MyTelegramBot = require('../telegram');
|
||||
|
||||
|
||||
// Nessun Sito Installato e Nessun Utente installato !
|
||||
let myuser = new User();
|
||||
@@ -7016,8 +7286,6 @@ UserSchema.statics.getUsersList = function (idapp) {
|
||||
}).lean();
|
||||
};
|
||||
|
||||
const User = mongoose.model('User', UserSchema);
|
||||
|
||||
class Hero {
|
||||
constructor(name, level) {
|
||||
this.name = name;
|
||||
@@ -7074,6 +7342,8 @@ const FuncUsers = {
|
||||
|
||||
UserSchema.index({ 'tokens.token': 1, 'tokens.access': 1, idapp: 1, deleted: 1, updatedAt: 1 });
|
||||
|
||||
const User = mongoose.models.User || mongoose.model('User', UserSchema);
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
Hero,
|
||||
|
||||
237
src/models/viaggi/Chat.js
Normal file
237
src/models/viaggi/Chat.js
Normal file
@@ -0,0 +1,237 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const LastMessageSchema = new Schema(
|
||||
{
|
||||
text: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
senderId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
timestamp: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
type: String,
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const ChatSchema = new Schema(
|
||||
{
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
participants: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
rideId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Ride',
|
||||
index: true,
|
||||
// Opzionale: chat collegata a un viaggio specifico
|
||||
},
|
||||
rideRequestId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'RideRequest',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['direct', 'ride', 'group'],
|
||||
default: 'direct',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
trim: true,
|
||||
// Solo per chat di gruppo
|
||||
},
|
||||
lastMessage: {
|
||||
type: LastMessageSchema,
|
||||
},
|
||||
unreadCount: {
|
||||
type: Map,
|
||||
of: Number,
|
||||
default: new Map(),
|
||||
// { odIdUtente: numeroMessaggiNonLetti }
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
mutedBy: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
],
|
||||
blockedBy: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
],
|
||||
deletedBy: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
],
|
||||
clearedBefore: {
|
||||
type: Map,
|
||||
of: Date,
|
||||
},
|
||||
metadata: {
|
||||
type: Schema.Types.Mixed,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true },
|
||||
}
|
||||
);
|
||||
|
||||
// Indici
|
||||
ChatSchema.index({ participants: 1 });
|
||||
ChatSchema.index({ idapp: 1, participants: 1 });
|
||||
ChatSchema.index({ idapp: 1, updatedAt: -1 });
|
||||
|
||||
// Virtual per contare messaggi non letti totali
|
||||
ChatSchema.virtual('totalUnread').get(function () {
|
||||
if (!this.unreadCount) return 0;
|
||||
let total = 0;
|
||||
this.unreadCount.forEach((count) => {
|
||||
total += count;
|
||||
});
|
||||
return total;
|
||||
});
|
||||
|
||||
// Metodo per ottenere unread count per un utente specifico
|
||||
ChatSchema.methods.getUnreadForUser = function (userId) {
|
||||
if (!this.unreadCount) return 0;
|
||||
return this.unreadCount.get(userId.toString()) || 0;
|
||||
};
|
||||
|
||||
// ✅ 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) {
|
||||
this.unreadCount.set(userId.toString(), 0);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per aggiornare ultimo messaggio
|
||||
ChatSchema.methods.updateLastMessage = function (message) {
|
||||
this.lastMessage = {
|
||||
text: message.text,
|
||||
senderId: message.senderId,
|
||||
timestamp: message.createdAt || new Date(),
|
||||
type: message.type || 'text',
|
||||
};
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per verificare se un utente è partecipante
|
||||
// ✅ 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
|
||||
// ✅ 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) {
|
||||
// Cerca chat esistente tra i due utenti
|
||||
let chat = await this.findOne({
|
||||
idapp,
|
||||
type: 'direct',
|
||||
participants: { $all: [userId1, userId2], $size: 2 },
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
chat = new this({
|
||||
idapp,
|
||||
type: 'direct',
|
||||
participants: [userId1, userId2],
|
||||
rideId,
|
||||
unreadCount: new Map(),
|
||||
});
|
||||
await chat.save();
|
||||
} else if (rideId && !chat.rideId) {
|
||||
// Aggiorna con rideId se fornito
|
||||
chat.rideId = rideId;
|
||||
await chat.save();
|
||||
}
|
||||
|
||||
return chat;
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere tutte le chat di un utente
|
||||
ChatSchema.statics.getChatsForUser = function (idapp, userId) {
|
||||
return this.find({
|
||||
idapp,
|
||||
participants: userId,
|
||||
isActive: true,
|
||||
blockedBy: { $ne: userId },
|
||||
})
|
||||
.populate('participants', 'username name surname profile.avatar')
|
||||
.populate('rideId', 'departure destination dateTime')
|
||||
.sort({ updatedAt: -1 });
|
||||
};
|
||||
|
||||
// Metodo statico per creare chat di gruppo per un viaggio
|
||||
ChatSchema.statics.createRideGroupChat = async function (idapp, rideId, title, participantIds) {
|
||||
const chat = new this({
|
||||
idapp,
|
||||
type: 'group',
|
||||
rideId,
|
||||
title,
|
||||
participants: participantIds,
|
||||
unreadCount: new Map(),
|
||||
});
|
||||
return chat.save();
|
||||
};
|
||||
|
||||
const Chat = mongoose.model('Chat', ChatSchema);
|
||||
|
||||
module.exports = Chat;
|
||||
357
src/models/viaggi/Feedback.js
Normal file
357
src/models/viaggi/Feedback.js
Normal file
@@ -0,0 +1,357 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const FeedbackCategoriesSchema = new Schema(
|
||||
{
|
||||
punctuality: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
cleanliness: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
communication: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
driving: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
// Solo per feedback a conducenti
|
||||
},
|
||||
respect: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
reliability: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const FeedbackSchema = new Schema(
|
||||
{
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
rideId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Ride',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
rideRequestId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'RideRequest',
|
||||
},
|
||||
fromUserId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
toUserId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: ['driver', 'passenger'],
|
||||
required: true,
|
||||
// Il ruolo dell'utente che RICEVE il feedback
|
||||
// 'driver' = sto valutando il conducente
|
||||
// 'passenger' = sto valutando il passeggero
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 1,
|
||||
max: 5,
|
||||
},
|
||||
categories: {
|
||||
type: FeedbackCategoriesSchema,
|
||||
},
|
||||
comment: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 1000,
|
||||
},
|
||||
pros: [
|
||||
{
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
],
|
||||
cons: [
|
||||
{
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
],
|
||||
tags: [
|
||||
{
|
||||
type: String,
|
||||
enum: [
|
||||
'puntuale',
|
||||
'gentile',
|
||||
'auto_pulita',
|
||||
'guida_sicura',
|
||||
'buona_conversazione',
|
||||
'silenzioso',
|
||||
'flessibile',
|
||||
'rispettoso',
|
||||
'affidabile',
|
||||
'consigliato',
|
||||
// Tag negativi
|
||||
'in_ritardo',
|
||||
'scortese',
|
||||
'guida_pericolosa',
|
||||
'auto_sporca',
|
||||
'non_rispettoso',
|
||||
],
|
||||
},
|
||||
],
|
||||
isPublic: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
// Feedback verificato (viaggio effettivamente completato)
|
||||
},
|
||||
response: {
|
||||
text: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500,
|
||||
},
|
||||
respondedAt: {
|
||||
type: Date,
|
||||
},
|
||||
},
|
||||
helpful: {
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
users: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
],
|
||||
},
|
||||
reported: {
|
||||
isReported: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
reason: String,
|
||||
reportedBy: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
reportedAt: Date,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true },
|
||||
}
|
||||
);
|
||||
|
||||
// Indici composti
|
||||
FeedbackSchema.index({ toUserId: 1, role: 1 });
|
||||
FeedbackSchema.index({ rideId: 1, fromUserId: 1 });
|
||||
FeedbackSchema.index({ idapp: 1, toUserId: 1 });
|
||||
|
||||
// Vincolo: un utente può lasciare un solo feedback per viaggio verso un altro utente
|
||||
FeedbackSchema.index({ rideId: 1, fromUserId: 1, toUserId: 1 }, { unique: true });
|
||||
|
||||
// Virtual per calcolare media categorie
|
||||
FeedbackSchema.virtual('categoryAverage').get(function () {
|
||||
if (!this.categories) return null;
|
||||
const cats = this.categories.toObject ? this.categories.toObject() : this.categories;
|
||||
const values = Object.values(cats).filter((v) => typeof v === 'number');
|
||||
if (values.length === 0) return null;
|
||||
return values.reduce((a, b) => a + b, 0) / values.length;
|
||||
});
|
||||
|
||||
// Metodo per aggiungere risposta
|
||||
FeedbackSchema.methods.addResponse = function (text) {
|
||||
this.response = {
|
||||
text,
|
||||
respondedAt: new Date(),
|
||||
};
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per segnare come utile
|
||||
FeedbackSchema.methods.markAsHelpful = function (userId) {
|
||||
if (!this.helpful.users.includes(userId)) {
|
||||
this.helpful.users.push(userId);
|
||||
this.helpful.count = this.helpful.users.length;
|
||||
return this.save();
|
||||
}
|
||||
return Promise.resolve(this);
|
||||
};
|
||||
|
||||
// Metodo per segnalare feedback
|
||||
FeedbackSchema.methods.report = function (userId, reason) {
|
||||
this.reported = {
|
||||
isReported: true,
|
||||
reason,
|
||||
reportedBy: userId,
|
||||
reportedAt: new Date(),
|
||||
};
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere feedback di un utente
|
||||
FeedbackSchema.statics.getForUser = function (idapp, userId, options = {}) {
|
||||
const query = {
|
||||
idapp,
|
||||
toUserId: userId,
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
if (options.role) {
|
||||
query.role = options.role;
|
||||
}
|
||||
|
||||
return this.find(query)
|
||||
.populate('fromUserId', 'username name surname profile.avatar')
|
||||
.populate('rideId', 'departure destination dateTime')
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(options.limit || 20);
|
||||
};
|
||||
|
||||
// Metodo statico per calcolare statistiche
|
||||
FeedbackSchema.statics.getStatsForUser = async function (idapp, userId) {
|
||||
const stats = await this.aggregate([
|
||||
{
|
||||
$match: {
|
||||
idapp,
|
||||
toUserId: new mongoose.Types.ObjectId(userId),
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$role',
|
||||
averageRating: { $avg: '$rating' },
|
||||
totalFeedbacks: { $sum: 1 },
|
||||
avgPunctuality: { $avg: '$categories.punctuality' },
|
||||
avgCleanliness: { $avg: '$categories.cleanliness' },
|
||||
avgCommunication: { $avg: '$categories.communication' },
|
||||
avgDriving: { $avg: '$categories.driving' },
|
||||
avgRespect: { $avg: '$categories.respect' },
|
||||
avgReliability: { $avg: '$categories.reliability' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Trasforma in oggetto più leggibile
|
||||
const result = {
|
||||
asDriver: null,
|
||||
asPassenger: null,
|
||||
overall: {
|
||||
averageRating: 0,
|
||||
totalFeedbacks: 0,
|
||||
},
|
||||
};
|
||||
|
||||
stats.forEach((stat) => {
|
||||
const data = {
|
||||
averageRating: Math.round(stat.averageRating * 10) / 10,
|
||||
totalFeedbacks: stat.totalFeedbacks,
|
||||
categories: {
|
||||
punctuality: stat.avgPunctuality ? Math.round(stat.avgPunctuality * 10) / 10 : null,
|
||||
cleanliness: stat.avgCleanliness ? Math.round(stat.avgCleanliness * 10) / 10 : null,
|
||||
communication: stat.avgCommunication ? Math.round(stat.avgCommunication * 10) / 10 : null,
|
||||
driving: stat.avgDriving ? Math.round(stat.avgDriving * 10) / 10 : null,
|
||||
respect: stat.avgRespect ? Math.round(stat.avgRespect * 10) / 10 : null,
|
||||
reliability: stat.avgReliability ? Math.round(stat.avgReliability * 10) / 10 : null,
|
||||
},
|
||||
};
|
||||
|
||||
if (stat._id === 'driver') {
|
||||
result.asDriver = data;
|
||||
} else if (stat._id === 'passenger') {
|
||||
result.asPassenger = data;
|
||||
}
|
||||
});
|
||||
|
||||
// Calcola overall
|
||||
const allStats = stats.reduce(
|
||||
(acc, s) => {
|
||||
acc.total += s.totalFeedbacks;
|
||||
acc.sum += s.averageRating * s.totalFeedbacks;
|
||||
return acc;
|
||||
},
|
||||
{ total: 0, sum: 0 }
|
||||
);
|
||||
|
||||
if (allStats.total > 0) {
|
||||
result.overall = {
|
||||
averageRating: Math.round((allStats.sum / allStats.total) * 10) / 10,
|
||||
totalFeedbacks: allStats.total,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Metodo statico per contare distribuzioni rating
|
||||
FeedbackSchema.statics.getRatingDistribution = async function (idapp, userId, role = null) {
|
||||
const match = {
|
||||
idapp,
|
||||
toUserId: new mongoose.Types.ObjectId(userId),
|
||||
};
|
||||
if (role) match.role = role;
|
||||
|
||||
return this.aggregate([
|
||||
{ $match: match },
|
||||
{
|
||||
$group: {
|
||||
_id: '$rating',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: -1 } },
|
||||
]);
|
||||
};
|
||||
|
||||
// Hook post-save per aggiornare rating utente
|
||||
FeedbackSchema.post('save', async function (doc) {
|
||||
try {
|
||||
const { User } = require('../User');
|
||||
|
||||
const stats = await mongoose.model('Feedback').getStatsForUser(doc.idapp, doc.toUserId);
|
||||
|
||||
await User.findByIdAndUpdate(doc.toUserId, {
|
||||
'profile.driverProfile.averageRating': stats.overall.averageRating,
|
||||
'profile.driverProfile.totalRatings': stats.overall.totalFeedbacks,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Errore aggiornamento rating utente:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const Feedback = mongoose.model('Feedback', FeedbackSchema);
|
||||
|
||||
module.exports = Feedback;
|
||||
538
src/models/viaggi/Ride.js
Normal file
538
src/models/viaggi/Ride.js
Normal file
@@ -0,0 +1,538 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
// Schema per le coordinate geografiche
|
||||
const CoordinatesSchema = new Schema(
|
||||
{
|
||||
lat: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema per una località (partenza, destinazione, waypoint)
|
||||
const LocationSchema = new Schema(
|
||||
{
|
||||
city: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
province: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
region: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
country: {
|
||||
type: String,
|
||||
default: 'Italia',
|
||||
trim: true,
|
||||
},
|
||||
postalCode: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
coordinates: {
|
||||
type: CoordinatesSchema,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema per i waypoint (tappe intermedie)
|
||||
const WaypointSchema = new Schema({
|
||||
/*_id: {
|
||||
type: String,
|
||||
required: false
|
||||
},*/
|
||||
location: {
|
||||
type: LocationSchema,
|
||||
required: true,
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
estimatedArrival: {
|
||||
type: Date,
|
||||
},
|
||||
stopDuration: {
|
||||
type: Number, // minuti di sosta
|
||||
default: 0,
|
||||
},
|
||||
}, { _id: false }); // 👈 AGGIUNGI QUESTO
|
||||
|
||||
// Schema per la ricorrenza del viaggio
|
||||
const RecurrenceSchema = new Schema(
|
||||
{
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['once', 'weekly', 'custom_days', 'custom_dates'],
|
||||
default: 'once',
|
||||
},
|
||||
daysOfWeek: [
|
||||
{
|
||||
type: Number,
|
||||
min: 0,
|
||||
max: 6,
|
||||
// 0 = Domenica, 1 = Lunedì, ..., 6 = Sabato
|
||||
},
|
||||
],
|
||||
customDates: [
|
||||
{
|
||||
type: Date,
|
||||
},
|
||||
],
|
||||
startDate: {
|
||||
type: Date,
|
||||
},
|
||||
endDate: {
|
||||
type: Date,
|
||||
},
|
||||
excludedDates: [
|
||||
{
|
||||
type: Date,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema per i passeggeri
|
||||
const PassengersSchema = new Schema(
|
||||
{
|
||||
available: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema per il veicolo
|
||||
const VehicleSchema = new Schema(
|
||||
{
|
||||
type: {
|
||||
type: String,
|
||||
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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema per le preferenze di viaggio
|
||||
const RidePreferencesSchema = new Schema(
|
||||
{
|
||||
smoking: {
|
||||
type: String,
|
||||
enum: ['yes', 'no', 'outside_only'],
|
||||
default: 'no',
|
||||
},
|
||||
pets: {
|
||||
type: String,
|
||||
enum: ['no', 'small', 'medium', 'large', 'all'],
|
||||
default: 'no',
|
||||
},
|
||||
luggage: {
|
||||
type: String,
|
||||
enum: ['none', 'small', 'medium', 'large'],
|
||||
default: 'medium',
|
||||
},
|
||||
packages: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxPackageSize: {
|
||||
type: String,
|
||||
enum: ['small', 'medium', 'large', 'xlarge'],
|
||||
default: 'medium',
|
||||
},
|
||||
music: {
|
||||
type: String,
|
||||
enum: ['no_music', 'quiet', 'moderate', 'loud', 'passenger_choice'],
|
||||
default: 'moderate',
|
||||
},
|
||||
conversation: {
|
||||
type: String,
|
||||
enum: ['quiet', 'moderate', 'chatty'],
|
||||
default: 'moderate',
|
||||
},
|
||||
foodAllowed: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
childrenFriendly: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
wheelchairAccessible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
otherPreferences: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema per il contributo/pagamento
|
||||
const ContributionItemSchema = new Schema({
|
||||
contribTypeId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Contribtype',
|
||||
required: true,
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
pricePerKm: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
});
|
||||
|
||||
const ContributionSchema = new Schema(
|
||||
{
|
||||
contribTypes: [ContributionItemSchema],
|
||||
negotiable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
freeForStudents: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
freeForElders: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema principale del Ride
|
||||
const RideSchema = new Schema(
|
||||
{
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['offer', 'request'],
|
||||
required: true,
|
||||
index: true,
|
||||
// offer = 🟢 Offerta passaggio (sono conducente)
|
||||
// request = 🔴 Richiesta passaggio (cerco passaggio)
|
||||
},
|
||||
departure: {
|
||||
type: LocationSchema,
|
||||
required: true,
|
||||
},
|
||||
destination: {
|
||||
type: LocationSchema,
|
||||
required: true,
|
||||
},
|
||||
waypoints: [WaypointSchema],
|
||||
departureDate: {
|
||||
type: Date,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
flexibleTime: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
flexibleMinutes: {
|
||||
type: Number,
|
||||
default: 30,
|
||||
min: 0,
|
||||
max: 180,
|
||||
},
|
||||
recurrence: {
|
||||
type: RecurrenceSchema,
|
||||
default: () => ({ type: 'once' }),
|
||||
},
|
||||
passengers: {
|
||||
type: PassengersSchema,
|
||||
required: function () {
|
||||
return this.type === 'offer';
|
||||
},
|
||||
},
|
||||
seatsNeeded: {
|
||||
type: Number,
|
||||
min: 1,
|
||||
default: 1,
|
||||
// Solo per type = 'request'
|
||||
},
|
||||
vehicle: {
|
||||
type: VehicleSchema,
|
||||
required: function () {
|
||||
return this.type === 'offer';
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
type: RidePreferencesSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
contribution: {
|
||||
type: ContributionSchema,
|
||||
default: () => ({ contribTypes: [] }),
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['draft', 'active', 'full', 'in_progress', 'completed', 'cancelled', 'expired'],
|
||||
default: 'active',
|
||||
index: true,
|
||||
},
|
||||
estimatedDistance: {
|
||||
type: Number, // in km
|
||||
min: 0,
|
||||
},
|
||||
estimatedDuration: {
|
||||
type: Number, // in minuti
|
||||
min: 0,
|
||||
},
|
||||
routePolyline: {
|
||||
type: String, // Polyline encoded per visualizzare il percorso
|
||||
},
|
||||
confirmedPassengers: [
|
||||
{
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
seats: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pickupPoint: LocationSchema,
|
||||
dropoffPoint: LocationSchema,
|
||||
confirmedAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
},
|
||||
],
|
||||
views: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
isFeatured: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 1000,
|
||||
},
|
||||
cancellationReason: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
cancelledAt: {
|
||||
type: Date,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true },
|
||||
}
|
||||
);
|
||||
|
||||
// Indici per ricerche ottimizzate
|
||||
RideSchema.index({ 'departure.city': 1, 'destination.city': 1 });
|
||||
RideSchema.index({ 'departure.coordinates': '2dsphere' });
|
||||
RideSchema.index({ 'destination.coordinates': '2dsphere' });
|
||||
RideSchema.index({ 'waypoints.location.city': 1 });
|
||||
RideSchema.index({ departureDate: 1, status: 1 });
|
||||
RideSchema.index({ idapp: 1, status: 1, departureDate: 1 });
|
||||
|
||||
// Virtual per verificare se il viaggio è pieno
|
||||
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 () {
|
||||
if (!this.confirmedPassengers) return 0;
|
||||
return this.confirmedPassengers.reduce((sum, p) => sum + (p.seats || 1), 0);
|
||||
});
|
||||
|
||||
// Virtual per ottenere tutte le città del percorso
|
||||
RideSchema.virtual('allCities').get(function () {
|
||||
const cities = [this.departure.city];
|
||||
if (this.waypoints && this.waypoints.length > 0) {
|
||||
this.waypoints.sort((a, b) => a.order - b.order).forEach((wp) => cities.push(wp.location.city));
|
||||
}
|
||||
cities.push(this.destination.city);
|
||||
return cities;
|
||||
});
|
||||
|
||||
// Metodo per verificare se passa per una città
|
||||
RideSchema.methods.passesThrough = function (cityName) {
|
||||
const normalizedCity = cityName.toLowerCase().trim();
|
||||
return this.allCities.some(
|
||||
(city) => city.toLowerCase().trim().includes(normalizedCity) || normalizedCity.includes(city.toLowerCase().trim())
|
||||
);
|
||||
};
|
||||
|
||||
// Metodo per aggiornare posti disponibili
|
||||
RideSchema.methods.updateAvailableSeats = function () {
|
||||
// ⚠️ 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) {
|
||||
this.status = 'full';
|
||||
} else if (this.status === 'full') {
|
||||
this.status = 'active';
|
||||
}
|
||||
}
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Pre-save hook
|
||||
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) {
|
||||
this.status = 'full';
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Metodi statici per ricerche comuni
|
||||
RideSchema.statics.findActiveByCity = function (idapp, departureCity, destinationCity, options = {}) {
|
||||
const query = {
|
||||
idapp,
|
||||
status: { $in: ['active', 'full'] },
|
||||
departureDate: { $gte: new Date() },
|
||||
};
|
||||
|
||||
if (departureCity) {
|
||||
query['departure.city'] = new RegExp(departureCity, 'i');
|
||||
}
|
||||
if (destinationCity) {
|
||||
query['destination.city'] = new RegExp(destinationCity, 'i');
|
||||
}
|
||||
if (options.type) {
|
||||
query.type = options.type;
|
||||
}
|
||||
if (options.date) {
|
||||
const startOfDay = new Date(options.date);
|
||||
startOfDay.setHours(0, 0, 0, 0);
|
||||
const endOfDay = new Date(options.date);
|
||||
endOfDay.setHours(23, 59, 59, 999);
|
||||
query.departureDate = { $gte: startOfDay, $lte: endOfDay };
|
||||
}
|
||||
|
||||
return this.find(query)
|
||||
.populate('userId', 'username name surname profile.driverProfile.averageRating')
|
||||
.sort({ departureDate: 1 });
|
||||
};
|
||||
|
||||
// Ricerca viaggi che passano per una città intermedia
|
||||
RideSchema.statics.findPassingThrough = function (idapp, cityName, options = {}) {
|
||||
const cityRegex = new RegExp(cityName, 'i');
|
||||
const query = {
|
||||
idapp,
|
||||
status: { $in: ['active'] },
|
||||
departureDate: { $gte: new Date() },
|
||||
$or: [{ 'departure.city': cityRegex }, { 'destination.city': cityRegex }, { 'waypoints.location.city': cityRegex }],
|
||||
};
|
||||
|
||||
if (options.type) {
|
||||
query.type = options.type;
|
||||
}
|
||||
|
||||
return this.find(query)
|
||||
.populate('userId', 'username name surname profile.driverProfile.averageRating')
|
||||
.sort({ departureDate: 1 });
|
||||
};
|
||||
|
||||
const Ride = mongoose.model('Ride', RideSchema);
|
||||
|
||||
module.exports = Ride;
|
||||
296
src/models/viaggi/RideRequest.js
Normal file
296
src/models/viaggi/RideRequest.js
Normal file
@@ -0,0 +1,296 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
// Schema per le coordinate
|
||||
const CoordinatesSchema = new Schema({
|
||||
lat: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
// Schema per località
|
||||
const LocationSchema = new Schema({
|
||||
city: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
province: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
coordinates: {
|
||||
type: CoordinatesSchema,
|
||||
required: true
|
||||
}
|
||||
}, { _id: false });
|
||||
|
||||
const RideRequestSchema = new Schema({
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
rideId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Ride',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
passengerId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
driverId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500
|
||||
},
|
||||
pickupPoint: {
|
||||
type: LocationSchema
|
||||
},
|
||||
dropoffPoint: {
|
||||
type: LocationSchema
|
||||
},
|
||||
useOriginalRoute: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
// true = usa partenza/destinazione originali del ride
|
||||
},
|
||||
seatsRequested: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 1,
|
||||
default: 1
|
||||
},
|
||||
hasLuggage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
luggageSize: {
|
||||
type: String,
|
||||
enum: ['small', 'medium', 'large'],
|
||||
default: 'small'
|
||||
},
|
||||
hasPackages: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
packageDescription: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 200
|
||||
},
|
||||
hasPets: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
petType: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
petSize: {
|
||||
type: String,
|
||||
enum: ['small', 'medium', 'large']
|
||||
},
|
||||
specialNeeds: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 300
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'accepted', 'rejected', 'cancelled', 'expired', 'completed'],
|
||||
default: 'pending',
|
||||
index: true
|
||||
},
|
||||
responseMessage: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 500
|
||||
},
|
||||
respondedAt: {
|
||||
type: Date
|
||||
},
|
||||
contribution: {
|
||||
agreed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
contribTypeId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Contribtype'
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
min: 0
|
||||
},
|
||||
notes: {
|
||||
type: String,
|
||||
trim: true
|
||||
}
|
||||
},
|
||||
cancelledBy: {
|
||||
type: String,
|
||||
enum: ['passenger', 'driver']
|
||||
},
|
||||
cancellationReason: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
cancelledAt: {
|
||||
type: Date
|
||||
},
|
||||
completedAt: {
|
||||
type: Date
|
||||
},
|
||||
feedbackGiven: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
// Indici composti per ricerche ottimizzate
|
||||
RideRequestSchema.index({ rideId: 1, status: 1 });
|
||||
RideRequestSchema.index({ passengerId: 1, status: 1 });
|
||||
RideRequestSchema.index({ driverId: 1, status: 1 });
|
||||
RideRequestSchema.index({ idapp: 1, createdAt: -1 });
|
||||
|
||||
// Virtual per verificare se la richiesta può essere cancellata
|
||||
RideRequestSchema.virtual('canCancel').get(function() {
|
||||
return ['pending', 'accepted'].includes(this.status);
|
||||
});
|
||||
|
||||
// Virtual per verificare se è in attesa
|
||||
RideRequestSchema.virtual('isPending').get(function() {
|
||||
return this.status === 'pending';
|
||||
});
|
||||
|
||||
// Metodo per accettare la richiesta
|
||||
RideRequestSchema.methods.accept = async function(responseMessage = '') {
|
||||
this.status = 'accepted';
|
||||
this.responseMessage = responseMessage;
|
||||
this.respondedAt = new Date();
|
||||
|
||||
// Aggiorna il ride con il passeggero confermato
|
||||
const Ride = mongoose.model('Ride');
|
||||
const ride = await Ride.findById(this.rideId);
|
||||
|
||||
if (ride) {
|
||||
ride.confirmedPassengers.push({
|
||||
userId: this.passengerId,
|
||||
seats: this.seatsRequested,
|
||||
pickupPoint: this.pickupPoint || ride.departure,
|
||||
dropoffPoint: this.dropoffPoint || ride.destination,
|
||||
confirmedAt: new Date()
|
||||
});
|
||||
await ride.updateAvailableSeats();
|
||||
}
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per rifiutare la richiesta
|
||||
RideRequestSchema.methods.reject = function(responseMessage = '') {
|
||||
this.status = 'rejected';
|
||||
this.responseMessage = responseMessage;
|
||||
this.respondedAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo per cancellare la richiesta
|
||||
RideRequestSchema.methods.cancel = async function(cancelledBy, reason = '') {
|
||||
this.status = 'cancelled';
|
||||
this.cancelledBy = cancelledBy;
|
||||
this.cancellationReason = reason;
|
||||
this.cancelledAt = new Date();
|
||||
|
||||
// Se era accettata, rimuovi il passeggero dal ride
|
||||
if (this.status === 'accepted') {
|
||||
const Ride = mongoose.model('Ride');
|
||||
const ride = await Ride.findById(this.rideId);
|
||||
|
||||
if (ride) {
|
||||
ride.confirmedPassengers = ride.confirmedPassengers.filter(
|
||||
p => p.userId.toString() !== this.passengerId.toString()
|
||||
);
|
||||
await ride.updateAvailableSeats();
|
||||
}
|
||||
}
|
||||
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere richieste pendenti di un conducente
|
||||
RideRequestSchema.statics.getPendingForDriver = function(idapp, driverId) {
|
||||
return this.find({
|
||||
idapp,
|
||||
driverId,
|
||||
status: 'pending'
|
||||
})
|
||||
.populate('passengerId', 'username name surname email')
|
||||
.populate('rideId', 'departure destination departureDate')
|
||||
.sort({ createdAt: -1 });
|
||||
};
|
||||
|
||||
// Metodo statico per ottenere richieste di un passeggero
|
||||
RideRequestSchema.statics.getByPassenger = function(idapp, passengerId, status = null) {
|
||||
const query = { idapp, passengerId };
|
||||
if (status) {
|
||||
query.status = status;
|
||||
}
|
||||
return this.find(query)
|
||||
.populate('rideId')
|
||||
.populate('driverId', 'username name surname')
|
||||
.sort({ createdAt: -1 });
|
||||
};
|
||||
|
||||
// Pre-save hook per validazioni
|
||||
RideRequestSchema.pre('save', async function(next) {
|
||||
if (this.isNew) {
|
||||
// Verifica che il ride esista e abbia posti disponibili
|
||||
const Ride = mongoose.model('Ride');
|
||||
const ride = await Ride.findById(this.rideId);
|
||||
|
||||
if (!ride) {
|
||||
throw new Error('Viaggio non trovato');
|
||||
}
|
||||
|
||||
if (ride.type === 'offer' && ride.passengers.available < this.seatsRequested) {
|
||||
throw new Error('Posti non sufficienti per questo viaggio');
|
||||
}
|
||||
|
||||
if (ride.userId.toString() === this.passengerId.toString()) {
|
||||
throw new Error('Non puoi richiedere un passaggio per il tuo stesso viaggio');
|
||||
}
|
||||
|
||||
// Imposta il driverId dal ride
|
||||
this.driverId = ride.userId;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
const RideRequest = mongoose.model('RideRequest', RideRequestSchema);
|
||||
|
||||
module.exports = RideRequest;
|
||||
310
src/models/viaggi/UserSettings.js
Normal file
310
src/models/viaggi/UserSettings.js
Normal file
@@ -0,0 +1,310 @@
|
||||
// ============================================================
|
||||
// 🔧 USER SETTINGS MODEL - Trasporti Solidali
|
||||
// ============================================================
|
||||
// File: server/models/viaggi/UserSettings.js
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const userSettingsSchema = new mongoose.Schema({
|
||||
// ID App e Utente
|
||||
idapp: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
required: true,
|
||||
ref: 'User',
|
||||
index: true
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 🔔 NOTIFICHE
|
||||
// ============================================================
|
||||
notifications: {
|
||||
// Notifiche Email
|
||||
email: {
|
||||
newMessage: { type: Boolean, default: true },
|
||||
rideRequest: { type: Boolean, default: true },
|
||||
rideConfirmation: { type: Boolean, default: true },
|
||||
rideCancellation: { type: Boolean, default: true },
|
||||
rideReminder: { type: Boolean, default: true },
|
||||
feedbackReceived: { type: Boolean, default: true },
|
||||
newsletter: { type: Boolean, default: false }
|
||||
},
|
||||
// Notifiche Push
|
||||
push: {
|
||||
newMessage: { type: Boolean, default: true },
|
||||
rideRequest: { type: Boolean, default: true },
|
||||
rideConfirmation: { type: Boolean, default: true },
|
||||
rideCancellation: { type: Boolean, default: true },
|
||||
rideReminder: { type: Boolean, default: true },
|
||||
feedbackReceived: { type: Boolean, default: true }
|
||||
},
|
||||
// Notifiche In-App
|
||||
inApp: {
|
||||
newMessage: { type: Boolean, default: true },
|
||||
rideRequest: { type: Boolean, default: true },
|
||||
rideConfirmation: { type: Boolean, default: true },
|
||||
rideCancellation: { type: Boolean, default: true }
|
||||
}
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 🔒 PRIVACY
|
||||
// ============================================================
|
||||
privacy: {
|
||||
// Visibilità profilo
|
||||
profileVisibility: {
|
||||
type: String,
|
||||
enum: ['public', 'members', 'private'],
|
||||
default: 'members'
|
||||
},
|
||||
// Mostra informazioni di contatto
|
||||
showPhone: { type: Boolean, default: false },
|
||||
showEmail: { type: Boolean, default: false },
|
||||
// Mostra statistiche profilo
|
||||
showStats: { type: Boolean, default: true },
|
||||
// Mostra feedback ricevuti
|
||||
showFeedbacks: { type: Boolean, default: true },
|
||||
// Condividi posizione durante viaggio
|
||||
shareLocation: { type: Boolean, default: true },
|
||||
// Chi può contattarmi
|
||||
whoCanContact: {
|
||||
type: String,
|
||||
enum: ['everyone', 'verified', 'afterBooking'],
|
||||
default: 'verified'
|
||||
}
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 🚗 PREFERENZE VIAGGI
|
||||
// ============================================================
|
||||
ridePreferences: {
|
||||
// Preferenze come conducente
|
||||
driver: {
|
||||
// Accetta prenotazioni istantanee
|
||||
instantBooking: { type: Boolean, default: false },
|
||||
// Richiede verifica documento passeggeri
|
||||
requireVerification: { type: Boolean, default: false },
|
||||
// Conversazione durante il viaggio
|
||||
chattiness: {
|
||||
type: String,
|
||||
enum: ['silent', 'moderate', 'chatty', 'any'],
|
||||
default: 'any'
|
||||
},
|
||||
// Musica
|
||||
music: {
|
||||
type: String,
|
||||
enum: ['no', 'soft', 'any'],
|
||||
default: 'any'
|
||||
},
|
||||
// Fumatori
|
||||
smoking: {
|
||||
type: String,
|
||||
enum: ['no', 'outside', 'yes'],
|
||||
default: 'no'
|
||||
},
|
||||
// Animali
|
||||
pets: {
|
||||
type: String,
|
||||
enum: ['no', 'small', 'yes'],
|
||||
default: 'no'
|
||||
},
|
||||
// Bagagli extra
|
||||
luggage: {
|
||||
type: String,
|
||||
enum: ['small', 'medium', 'large'],
|
||||
default: 'medium'
|
||||
}
|
||||
},
|
||||
// Preferenze come passeggero
|
||||
passenger: {
|
||||
// Conversazione
|
||||
chattiness: {
|
||||
type: String,
|
||||
enum: ['silent', 'moderate', 'chatty', 'any'],
|
||||
default: 'any'
|
||||
},
|
||||
// Musica
|
||||
music: {
|
||||
type: String,
|
||||
enum: ['no', 'soft', 'any'],
|
||||
default: 'any'
|
||||
},
|
||||
// Fumatori
|
||||
smokingTolerance: {
|
||||
type: String,
|
||||
enum: ['no', 'outside', 'yes'],
|
||||
default: 'no'
|
||||
},
|
||||
// Viaggio con animali
|
||||
comfortableWithPets: { type: Boolean, default: true }
|
||||
}
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 🔍 RICERCA & FILTRI PREDEFINITI
|
||||
// ============================================================
|
||||
searchPreferences: {
|
||||
// Raggio di ricerca predefinito (km)
|
||||
defaultRadius: { type: Number, default: 50 },
|
||||
// Ordine risultati
|
||||
defaultSortBy: {
|
||||
type: String,
|
||||
enum: ['date', 'price', 'distance', 'rating'],
|
||||
default: 'date'
|
||||
},
|
||||
// Solo viaggi verificati
|
||||
verifiedOnly: { type: Boolean, default: false },
|
||||
// Solo con recensioni positive
|
||||
minRating: { type: Number, min: 0, max: 5, default: 0 }
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 💳 PAGAMENTI & DONAZIONI
|
||||
// ============================================================
|
||||
payment: {
|
||||
// Metodo di pagamento predefinito
|
||||
defaultMethod: {
|
||||
type: String,
|
||||
enum: ['cash', 'card', 'app', 'none'],
|
||||
default: 'cash'
|
||||
},
|
||||
// Contributo suggerito automatico
|
||||
autoSuggestContribution: { type: Boolean, default: true },
|
||||
// Accetta pagamenti anticipati
|
||||
acceptAdvancePayment: { type: Boolean, default: false }
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 📱 INTERFACCIA
|
||||
// ============================================================
|
||||
interface: {
|
||||
// Tema
|
||||
theme: {
|
||||
type: String,
|
||||
enum: ['light', 'dark', 'auto'],
|
||||
default: 'auto'
|
||||
},
|
||||
// Lingua
|
||||
language: {
|
||||
type: String,
|
||||
enum: ['it', 'en', 'de', 'fr', 'es'],
|
||||
default: 'it'
|
||||
},
|
||||
// Mostra tutorial
|
||||
showTutorials: { type: Boolean, default: true },
|
||||
// Vista mappa predefinita
|
||||
defaultMapView: { type: Boolean, default: false }
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// 🔐 SICUREZZA
|
||||
// ============================================================
|
||||
security: {
|
||||
// Richiedi verifica telefono per prenotazioni
|
||||
requirePhoneVerification: { type: Boolean, default: true },
|
||||
// Autenticazione a due fattori
|
||||
twoFactorAuth: { type: Boolean, default: false },
|
||||
// Logout automatico dopo inattività (minuti)
|
||||
autoLogout: { type: Number, default: 30 },
|
||||
// Richiedi conferma prima di cancellare viaggio
|
||||
confirmBeforeCancel: { type: Boolean, default: true }
|
||||
}
|
||||
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 📊 INDICI
|
||||
// ============================================================
|
||||
userSettingsSchema.index({ idapp: 1, userId: 1 }, { unique: true });
|
||||
|
||||
// ============================================================
|
||||
// 🎯 METODI STATICI
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Ottieni o crea impostazioni utente con valori predefiniti
|
||||
*/
|
||||
userSettingsSchema.statics.getOrCreateSettings = async function(idapp, userId) {
|
||||
let settings = await this.findOne({ idapp, userId });
|
||||
|
||||
if (!settings) {
|
||||
settings = await this.create({
|
||||
idapp,
|
||||
userId,
|
||||
// I valori predefiniti sono già definiti nello schema
|
||||
});
|
||||
}
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
/**
|
||||
* Aggiorna impostazioni parziali
|
||||
*/
|
||||
userSettingsSchema.statics.updateSettings = async function(idapp, userId, updates) {
|
||||
const settings = await this.getOrCreateSettings(idapp, userId);
|
||||
|
||||
// Merge delle impostazioni
|
||||
Object.keys(updates).forEach(section => {
|
||||
if (settings[section] && typeof updates[section] === 'object') {
|
||||
settings[section] = {
|
||||
...settings[section],
|
||||
...updates[section]
|
||||
};
|
||||
} else {
|
||||
settings[section] = updates[section];
|
||||
}
|
||||
});
|
||||
|
||||
await settings.save();
|
||||
return settings;
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 🎯 METODI ISTANZA
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Verifica se una notifica è abilitata
|
||||
*/
|
||||
userSettingsSchema.methods.isNotificationEnabled = function(type, channel) {
|
||||
if (!this.notifications[channel]) return false;
|
||||
return this.notifications[channel][type] !== false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ottieni preferenze compatibilità viaggio
|
||||
*/
|
||||
userSettingsSchema.methods.getCompatibilityPreferences = function(asRole = 'passenger') {
|
||||
if (asRole === 'driver') {
|
||||
return this.ridePreferences.driver;
|
||||
}
|
||||
return this.ridePreferences.passenger;
|
||||
};
|
||||
|
||||
/**
|
||||
* Esporta impostazioni per frontend
|
||||
*/
|
||||
userSettingsSchema.methods.toClientJSON = function() {
|
||||
return {
|
||||
notifications: this.notifications,
|
||||
privacy: this.privacy,
|
||||
ridePreferences: this.ridePreferences,
|
||||
searchPreferences: this.searchPreferences,
|
||||
payment: this.payment,
|
||||
interface: this.interface,
|
||||
security: {
|
||||
requirePhoneVerification: this.security.requirePhoneVerification,
|
||||
twoFactorAuth: this.security.twoFactorAuth,
|
||||
confirmBeforeCancel: this.security.confirmBeforeCancel
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('TrasportiUserSettings', userSettingsSchema);
|
||||
@@ -463,6 +463,8 @@ async function aggiornaCategorieESottoCategorie() {
|
||||
|
||||
async function runMigration() {
|
||||
try {
|
||||
const { User } = require('../models/user');
|
||||
|
||||
const idapp = 0; // TUTTI
|
||||
|
||||
console.log('🚀 Controllo Versioni Tabelle (runMigration)');
|
||||
@@ -471,6 +473,10 @@ async function runMigration() {
|
||||
idapp,
|
||||
shared_consts.JOB_TO_EXECUTE.MIGRATION_SECTORS_DIC25
|
||||
);
|
||||
const isMigratione30Dic2025Telegram = await Version.isJobExecuted(
|
||||
idapp,
|
||||
shared_consts.JOB_TO_EXECUTE.MIGRATION_TELEGRAM_30DIC25
|
||||
);
|
||||
|
||||
const vers_server_str = await tools.getVersServer();
|
||||
|
||||
@@ -522,6 +528,18 @@ async function runMigration() {
|
||||
console.log('\n✅ Migrazione DIC 2025 completata con successo!');
|
||||
}
|
||||
|
||||
if (isMigratione30Dic2025Telegram) {
|
||||
await User.updateMany({ 'profile.teleg_id': { $exists: true, $ne: 0 } }, [
|
||||
{
|
||||
$set: {
|
||||
'notificationPreferences.telegram.enabled': true,
|
||||
'notificationPreferences.telegram.chatId': '$profile.teleg_id',
|
||||
},
|
||||
},
|
||||
]);
|
||||
await Version.setJobExecuted(idapp, shared_consts.JOB_TO_EXECUTE.MIGRATION_TELEGRAM_30DIC25);
|
||||
}
|
||||
|
||||
await Version.setLastVersionRun(idapp, version_server);
|
||||
} catch (error) {
|
||||
console.error('❌ Errore durante la migrazione:', error);
|
||||
|
||||
@@ -13,7 +13,6 @@ const PageView = require('../models/PageView');
|
||||
|
||||
const fal = require('@fal-ai/client');
|
||||
|
||||
|
||||
const imageGenerator = require('../services/imageGenerator'); // Assicurati che il percorso sia corretto
|
||||
|
||||
const posterEditor = require('../services/PosterEditor'); // <--- Importa la nuova classe
|
||||
@@ -33,13 +32,22 @@ const { MyElem } = require('../models/myelem');
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
const settingsRoutes = require('../routes/viaggi/settingsRoutes');
|
||||
router.use('/viaggi/settings', settingsRoutes);
|
||||
|
||||
const widgetRoutes = require('../routes/viaggi/widgetRoutes');
|
||||
router.use('/viaggi/widget', widgetRoutes);
|
||||
|
||||
const viaggiRoutes = require('../routes/viaggiRoutes');
|
||||
router.use('/viaggi', viaggiRoutes);
|
||||
|
||||
|
||||
// Importa le routes video
|
||||
const videoRoutes = require('../routes/videoRoutes');
|
||||
|
||||
// Monta le routes video
|
||||
router.use('/video', videoRoutes);
|
||||
|
||||
|
||||
router.use('/templates', authenticate, templatesRouter);
|
||||
router.use('/posters', authenticate, postersRouter);
|
||||
router.use('/assets', authenticate, assetsRouter);
|
||||
@@ -520,8 +528,15 @@ router.post('/chatbot', authenticate, async (req, res) => {
|
||||
|
||||
router.post('/generateposter', async (req, res) => {
|
||||
const {
|
||||
titolo, data, ora, luogo, descrizione, contatti, fotoDescrizione, stile,
|
||||
provider = 'hf' // Default a HF (Gratis)
|
||||
titolo,
|
||||
data,
|
||||
ora,
|
||||
luogo,
|
||||
descrizione,
|
||||
contatti,
|
||||
fotoDescrizione,
|
||||
stile,
|
||||
provider = 'hf', // Default a HF (Gratis)
|
||||
} = req.body;
|
||||
|
||||
// 1. Prompt per l'AI: Chiediamo SOLO lo sfondo, VIETIAMO il testo.
|
||||
@@ -543,21 +558,50 @@ router.post('/generateposter', async (req, res) => {
|
||||
data,
|
||||
ora,
|
||||
luogo,
|
||||
contatti
|
||||
contatti,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
imageUrl: finalPosterBase64, // Restituisce l'immagine completa in base64
|
||||
step: 'AI + Canvas Composition'
|
||||
step: 'AI + Canvas Composition',
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('Errore:', err.message);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/users/search', authenticate, async (req, res) => {
|
||||
try {
|
||||
const { User } = require('../models/user');
|
||||
|
||||
const { q, idapp } = req.query;
|
||||
|
||||
if (!q || q.length < 2) {
|
||||
return res.status(400).json({ success: false, message: 'Query too short' });
|
||||
}
|
||||
|
||||
const query = q.trim();
|
||||
|
||||
const users = await User.find({
|
||||
idapp,
|
||||
$or: [
|
||||
{ name: { $regex: query, $options: 'i' } },
|
||||
{ surname: { $regex: query, $options: 'i' } },
|
||||
{ username: { $regex: query, $options: 'i' } },
|
||||
],
|
||||
_id: { $ne: req.user?._id }, // escludi l'utente corrente se autenticato
|
||||
})
|
||||
.select('_id name surname username profile') // solo campi necessari
|
||||
.limit(10); // evita overload
|
||||
|
||||
res.json({ success: true, data: users });
|
||||
} catch (error) {
|
||||
console.error('User search error:', error);
|
||||
res.status(500).json({ success: false, message: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -317,7 +317,11 @@ router.post('/', async (req, res) => {
|
||||
await telegrambot.askConfirmationUser(myuser.idapp, shared_consts.CallFunz.REGISTRATION, myuser);
|
||||
|
||||
const { token, refreshToken, browser_random } = await myuser.generateAuthToken(req, browser_random);
|
||||
res.header('x-auth', token).header('x-refrtok', refreshToken).header('x-browser-random', browser_random).send(myuser);
|
||||
res
|
||||
.header('x-auth', token)
|
||||
.header('x-refrtok', refreshToken)
|
||||
.header('x-browser-random', browser_random)
|
||||
.send(myuser);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -368,7 +372,11 @@ router.post('/', async (req, res) => {
|
||||
// if (!tools.testing()) {
|
||||
await sendemail.sendEmail_Registration(user.lang, user.email, user, user.idapp, user.linkreg);
|
||||
// }
|
||||
res.header('x-auth', ris.token).header('x-refrtok', ris.refreshToken).header('x-browser-random', ris.browser_random).send(user);
|
||||
res
|
||||
.header('x-auth', ris.token)
|
||||
.header('x-refrtok', ris.refreshToken)
|
||||
.header('x-browser-random', ris.browser_random)
|
||||
.send(user);
|
||||
return true;
|
||||
});
|
||||
})
|
||||
@@ -411,7 +419,9 @@ router.patch('/:id', authenticate, (req, res) => {
|
||||
|
||||
if (!User.isAdmin(req.user.perm)) {
|
||||
// If without permissions, exit
|
||||
return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||
return res
|
||||
.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED)
|
||||
.send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||
}
|
||||
|
||||
User.findByIdAndUpdate(id, { $set: body })
|
||||
@@ -591,9 +601,14 @@ router.post('/panel', authenticate, async (req, res) => {
|
||||
idapp = req.body.idapp;
|
||||
locale = req.body.locale;
|
||||
|
||||
if (!req.user || !User.isAdmin(req.user.perm) && !User.isManager(req.user.perm) && !User.isFacilitatore(req.user.perm)) {
|
||||
if (
|
||||
!req.user ||
|
||||
(!User.isAdmin(req.user.perm) && !User.isManager(req.user.perm) && !User.isFacilitatore(req.user.perm))
|
||||
) {
|
||||
// If without permissions, exit
|
||||
return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||
return res
|
||||
.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED)
|
||||
.send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -953,7 +968,9 @@ router.post('/friends/cmd', authenticate, async (req, res) => {
|
||||
usernameDest !== usernameLogged &&
|
||||
(cmd === shared_consts.FRIENDSCMD.SETFRIEND || cmd === shared_consts.FRIENDSCMD.SETHANDSHAKE)
|
||||
) {
|
||||
return res.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED).send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||
return res
|
||||
.status(server_constants.RIS_CODE_ERR_UNAUTHORIZED)
|
||||
.send({ code: server_constants.RIS_CODE_ERR_UNAUTHORIZED, msg: '' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1119,7 +1136,10 @@ async function eseguiDbOpUser(idapp, mydata, locale, req, res) {
|
||||
} else if (mydata.dbop === 'noNameSurname') {
|
||||
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noNameSurname': mydata.value } });
|
||||
} else if (mydata.dbop === 'telegram_verification_skipped') {
|
||||
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.telegram_verification_skipped': mydata.value } });
|
||||
await User.findOneAndUpdate(
|
||||
{ _id: mydata._id },
|
||||
{ $set: { 'profile.telegram_verification_skipped': mydata.value } }
|
||||
);
|
||||
} else if (mydata.dbop === 'pwdLikeAdmin') {
|
||||
await User.setPwdComeQuellaDellAdmin(mydata);
|
||||
} else if (mydata.dbop === 'ripristinaPwdPrec') {
|
||||
@@ -1129,7 +1149,7 @@ async function eseguiDbOpUser(idapp, mydata, locale, req, res) {
|
||||
} else if (mydata.dbop === 'noComune') {
|
||||
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'profile.noComune': mydata.value } });
|
||||
} else if (mydata.dbop === 'verifiedemail') {
|
||||
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { 'verified_email': mydata.value } });
|
||||
await User.findOneAndUpdate({ _id: mydata._id }, { $set: { verified_email: mydata.value } });
|
||||
} else if (mydata.dbop === 'resendVerificationEmail') {
|
||||
// Invia la email di Verifica email
|
||||
const ris = await sendemail.sendEmail_ReVerifyingEmail(mydata, idapp);
|
||||
|
||||
37
src/routes/geoRoutes.js
Normal file
37
src/routes/geoRoutes.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
autocomplete,
|
||||
geocode,
|
||||
reverseGeocode,
|
||||
getRoute,
|
||||
getMatrix,
|
||||
suggestWaypoints,
|
||||
searchItalianCities,
|
||||
getDistance,
|
||||
getIsochrone
|
||||
} = require('../controllers/geocodingController');
|
||||
|
||||
// Rate limiting opzionale
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const geoLimiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 minuto
|
||||
max: 60, // 60 richieste per minuto
|
||||
message: { success: false, message: 'Troppe richieste, riprova tra poco' }
|
||||
});
|
||||
|
||||
router.use(geoLimiter);
|
||||
|
||||
// Routes
|
||||
router.get('/autocomplete', autocomplete);
|
||||
router.get('/geocode', geocode);
|
||||
router.get('/reverse', reverseGeocode);
|
||||
router.get('/route', getRoute);
|
||||
router.post('/matrix', getMatrix);
|
||||
router.get('/suggest-waypoints', suggestWaypoints);
|
||||
router.get('/cities/it', searchItalianCities);
|
||||
router.get('/distance', getDistance);
|
||||
router.get('/isochrone', getIsochrone);
|
||||
|
||||
module.exports = router;
|
||||
100
src/routes/viaggi/settingsRoutes.js
Normal file
100
src/routes/viaggi/settingsRoutes.js
Normal file
@@ -0,0 +1,100 @@
|
||||
// ============================================================
|
||||
// 🔧 SETTINGS ROUTES - Trasporti Solidali
|
||||
// ============================================================
|
||||
// File: server/routes/viaggi/settingsRoutes.js
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const settingsController = require('../../controllers/viaggi/settingsController');
|
||||
const { authenticate } = require('../../middleware/authenticate');
|
||||
|
||||
// ============================================================
|
||||
// 🔐 TUTTE LE ROUTES RICHIEDONO AUTENTICAZIONE
|
||||
// ============================================================
|
||||
router.use(authenticate);
|
||||
|
||||
// ============================================================
|
||||
// 📄 IMPOSTAZIONI GENERALI
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* GET /api/viaggi/settings
|
||||
* Ottieni tutte le impostazioni dell'utente
|
||||
*/
|
||||
router.get('/', settingsController.getSettings);
|
||||
|
||||
/**
|
||||
* PUT /api/viaggi/settings
|
||||
* Aggiorna le impostazioni (completo)
|
||||
*/
|
||||
router.put('/', settingsController.updateSettings);
|
||||
|
||||
/**
|
||||
* POST /api/viaggi/settings/reset
|
||||
* Reset impostazioni ai valori predefiniti
|
||||
*/
|
||||
router.post('/reset', settingsController.resetSettings);
|
||||
|
||||
// ============================================================
|
||||
// 📝 AGGIORNAMENTI PARZIALI (per sezione)
|
||||
// ============================================================
|
||||
|
||||
const notifController = require('../../controllers/viaggi/trasportiNotificationsController');
|
||||
|
||||
// Preferenze
|
||||
router.get('/notifications/preferences', authenticate, notifController.getNotificationPreferences);
|
||||
router.put('/notifications/preferences', authenticate, notifController.updateNotificationPreferences);
|
||||
|
||||
// Telegram
|
||||
router.post('/notifications/telegram/code', authenticate, notifController.generateTelegramCode);
|
||||
router.post('/notifications/telegram/connect', authenticate, notifController.connectTelegram);
|
||||
router.delete('/notifications/telegram/disconnect', authenticate, notifController.disconnectTelegram);
|
||||
|
||||
// Push
|
||||
router.post('/notifications/push/subscribe', authenticate, notifController.subscribePushNotifications);
|
||||
router.delete('/notifications/push/unsubscribe', authenticate, notifController.unsubscribePushNotifications);
|
||||
|
||||
// Test
|
||||
router.post('/notifications/test', authenticate, notifController.sendTestNotification);
|
||||
|
||||
/**
|
||||
* PATCH /api/viaggi/settings/notifications
|
||||
* Aggiorna solo le notifiche
|
||||
*/
|
||||
router.patch('/notifications', settingsController.updateNotifications);
|
||||
|
||||
/**
|
||||
* PATCH /api/viaggi/settings/privacy
|
||||
* Aggiorna solo la privacy
|
||||
*/
|
||||
router.patch('/privacy', settingsController.updatePrivacy);
|
||||
|
||||
/**
|
||||
* PATCH /api/viaggi/settings/ride-preferences
|
||||
* Aggiorna solo le preferenze viaggi
|
||||
*/
|
||||
router.patch('/ride-preferences', settingsController.updateRidePreferences);
|
||||
|
||||
/**
|
||||
* PATCH /api/viaggi/settings/interface
|
||||
* Aggiorna solo l'interfaccia
|
||||
*/
|
||||
router.patch('/interface', settingsController.updateInterface);
|
||||
|
||||
// ============================================================
|
||||
// 📊 EXPORT / IMPORT
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* GET /api/viaggi/settings/export
|
||||
* Esporta tutte le impostazioni
|
||||
*/
|
||||
router.get('/export', settingsController.exportSettings);
|
||||
|
||||
/**
|
||||
* POST /api/viaggi/settings/import
|
||||
* Importa impostazioni da backup
|
||||
*/
|
||||
router.post('/import', settingsController.importSettings);
|
||||
|
||||
module.exports = router;
|
||||
32
src/routes/viaggi/widgetRoutes.js
Normal file
32
src/routes/viaggi/widgetRoutes.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// ============================================================
|
||||
// 📊 WIDGET & STATS ROUTES - Trasporti Solidali
|
||||
// ============================================================
|
||||
// File: server/routes/viaggi/widgetRoutes.js
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const widgetController = require('../../controllers/viaggi/widgetController');
|
||||
const { authenticate } = require('../../middleware/authenticate');
|
||||
|
||||
// ============================================================
|
||||
// 🔐 TUTTE LE ROUTES RICHIEDONO AUTENTICAZIONE
|
||||
// ============================================================
|
||||
router.use(authenticate);
|
||||
|
||||
// ============================================================
|
||||
// 📊 WIDGET DATA
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* GET /api/viaggi/widget/data
|
||||
* Ottieni tutti i dati per il widget dashboard
|
||||
*/
|
||||
router.get('/data', widgetController.getWidgetData);
|
||||
|
||||
/**
|
||||
* GET /api/viaggi/widget/stats
|
||||
* Statistiche rapide per badge/notifiche
|
||||
*/
|
||||
router.get('/stats', widgetController.getQuickStats);
|
||||
|
||||
module.exports = router;
|
||||
1052
src/routes/viaggiRoutes.js
Normal file
1052
src/routes/viaggiRoutes.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ function setupExpress(app, corsOptions) {
|
||||
app.use(helmet());
|
||||
app.use(morgan('dev'));
|
||||
app.use(cors(corsOptions));
|
||||
app.set('trust proxy', true);
|
||||
app.set('trust proxy', (process.env.NODE_ENV === 'development') ? false : true);
|
||||
|
||||
// parser
|
||||
app.use(express.json({ limit: '100mb' }));
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const pty = require('node-pty');
|
||||
const { User } = require('../models/user');
|
||||
const { sendMessage } = require('../telegram/api');
|
||||
|
||||
function setupShellWebSocket(ws) {
|
||||
console.log('🔌 Client WebSocket Shell connesso');
|
||||
let scriptProcess = null;
|
||||
let buffer = '';
|
||||
|
||||
ws.on('message', async (message) => {
|
||||
try {
|
||||
const parsed = JSON.parse(message);
|
||||
const { type, user_id, scriptName, data } = parsed;
|
||||
|
||||
if (type === 'start_script' && (await User.isAdminById(user_id))) {
|
||||
if (scriptProcess) scriptProcess.kill();
|
||||
|
||||
const scriptPath = path.join(__dirname, '..', '..', scriptName);
|
||||
if (!fs.existsSync(scriptPath)) {
|
||||
return ws.send(JSON.stringify({ type: 'error', data: 'Script non trovato' }));
|
||||
}
|
||||
|
||||
scriptProcess = pty.spawn('bash', [scriptPath], {
|
||||
name: 'xterm-color',
|
||||
cols: 80,
|
||||
rows: 40,
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
scriptProcess.on('data', (chunk) => {
|
||||
buffer += chunk;
|
||||
ws.send(JSON.stringify({ type: 'output', data: chunk }));
|
||||
if (buffer.length > 4096) buffer = buffer.slice(-2048);
|
||||
});
|
||||
|
||||
scriptProcess.on('exit', (code) => {
|
||||
const msg = code === 0 ? '✅ Script completato' : `❌ Uscito con codice ${code}`;
|
||||
ws.send(JSON.stringify({ type: 'close', data: msg }));
|
||||
});
|
||||
} else if (type === 'input' && scriptProcess) {
|
||||
scriptProcess.write(data + '\n');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Errore WS Shell:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
if (scriptProcess) scriptProcess.kill();
|
||||
console.log('🔌 WS Shell chiuso');
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { setupShellWebSocket };
|
||||
@@ -21,6 +21,7 @@ const registerFonts = async () => {
|
||||
{ file: 'PlayfairDisplay-Regular.ttf', family: 'Playfair Display', weight: '400' }
|
||||
];
|
||||
|
||||
|
||||
for (const font of fontMappings) {
|
||||
const fontPath = path.join(FONTS_DIR, font.file);
|
||||
try {
|
||||
@@ -708,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;
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
const axios = require('axios');
|
||||
const { API_URL, TIMEOUT } = require('./config');
|
||||
|
||||
async function callTelegram(method, params) {
|
||||
try {
|
||||
const { data } = await axios.post(`${API_URL}/${method}`, params, { timeout: TIMEOUT });
|
||||
if (!data.ok) throw new Error(`Telegram error: ${data.description}`);
|
||||
return data.result;
|
||||
} catch (err) {
|
||||
console.error('❌ Telegram API error:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(chatId, text, options = {}) {
|
||||
return callTelegram('sendMessage', {
|
||||
chat_id: chatId,
|
||||
text,
|
||||
parse_mode: options.parse_mode || 'HTML',
|
||||
disable_web_page_preview: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function sendPhoto(chatId, photo, caption = '', options = {}) {
|
||||
return callTelegram('sendPhoto', {
|
||||
chat_id: chatId,
|
||||
photo,
|
||||
caption,
|
||||
parse_mode: 'HTML',
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { sendMessage, sendPhoto };
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
TOKEN: process.env.TELEGRAM_BOT_TOKEN,
|
||||
API_URL: `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}`,
|
||||
ADMIN_GROUP_IDS: process.env.TELEGRAM_ADMIN_GROUPS
|
||||
? process.env.TELEGRAM_ADMIN_GROUPS.split(',')
|
||||
: [],
|
||||
TIMEOUT: 5000,
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
// Ruoli, fasi logiche e costanti admin (adatta gli ID ai tuoi reali)
|
||||
module.exports = {
|
||||
ADMIN_USER_SERVER: process.env.ADMIN_USER_SERVER || 'server_admin',
|
||||
ADMIN_IDTELEGRAM_SERVER: process.env.ADMIN_IDTELEGRAM_SERVER || '',
|
||||
phase: {
|
||||
REGISTRATION: 'REGISTRATION',
|
||||
REGISTRATION_CONFIRMED: 'REGISTRATION_CONFIRMED',
|
||||
RESET_PWD: 'RESET_PWD',
|
||||
NOTIFICATION: 'NOTIFICATION',
|
||||
GENERIC: 'GENERIC',
|
||||
},
|
||||
roles: {
|
||||
ADMIN: 'ADMIN',
|
||||
MANAGER: 'MANAGER',
|
||||
FACILITATORE: 'FACILITATORE',
|
||||
EDITOR: 'EDITOR',
|
||||
},
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
const { sendMessage } = require('../api');
|
||||
const { ADMIN_GROUP_IDS } = require('../config');
|
||||
const { safeExec } = require('../helpers');
|
||||
|
||||
const sendToAdmins = safeExec(async (message) => {
|
||||
for (const id of ADMIN_GROUP_IDS) {
|
||||
await sendMessage(id, message);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { sendToAdmins };
|
||||
@@ -1,101 +0,0 @@
|
||||
// telegram/handlers/callbackHandler.js
|
||||
const tools = require('../../tools/general');
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const { User } = require('../../models/user');
|
||||
const { Circuit } = require('../../models/circuit');
|
||||
const { handleRegistration } = require('./registrationHandler');
|
||||
const { handleFriends } = require('./friendsHandler');
|
||||
const { handleCircuit } = require('./circuitHandler');
|
||||
const { handleZoom } = require('./zoomHandler');
|
||||
const { handlePassword } = require('./passwordHandler');
|
||||
|
||||
async function handleCallback(bot, cl, callbackQuery) {
|
||||
const idapp = cl.idapp;
|
||||
let notifyText = ''; // testo di notifica Telegram (answerCallbackQuery)
|
||||
try {
|
||||
// parsing payload dal tuo formato originale (action|username|userDest|groupId|circuitId|groupname)
|
||||
let data = {
|
||||
action: '',
|
||||
username: '',
|
||||
userDest: '',
|
||||
groupId: '',
|
||||
circuitId: '',
|
||||
groupname: '',
|
||||
};
|
||||
|
||||
const raw = callbackQuery?.data || '';
|
||||
if (raw) {
|
||||
const arr = raw.split(tools.SEP);
|
||||
data = {
|
||||
action: arr[0] || '',
|
||||
username: arr[1] || '',
|
||||
userDest: arr[2] || '',
|
||||
groupId: arr[3] || '',
|
||||
circuitId: arr[4] || '',
|
||||
groupname: arr[5] || '',
|
||||
};
|
||||
}
|
||||
|
||||
// normalizza username reali (come nel sorgente)
|
||||
data.username = await User.getRealUsernameByUsername(idapp, data.username);
|
||||
data.userDest = data.userDest ? await User.getRealUsernameByUsername(idapp, data.userDest) : '';
|
||||
|
||||
const msg = callbackQuery.message;
|
||||
const opts = { chat_id: msg.chat.id, message_id: msg.message_id };
|
||||
|
||||
// contest utente corrente
|
||||
await cl.setInit?.(msg); // se presente nel tuo codice
|
||||
const rec = cl.getRecInMem?.(msg);
|
||||
const username_action = rec?.user ? rec.user.username : '';
|
||||
|
||||
// carica user e userDest compatti (come nel tuo codice)
|
||||
const user = data.username ? await User.getUserShortDataByUsername(idapp, data.username) : null;
|
||||
const userDest = data.userDest ? await User.getUserShortDataByUsername(idapp, data.userDest) : null;
|
||||
|
||||
// routing per ambito
|
||||
const act = data.action || '';
|
||||
|
||||
// 1) REGISTRAZIONE e varianti
|
||||
if (act.includes(shared_consts.CallFunz.REGISTRATION)) {
|
||||
notifyText = await handleRegistration({ bot, cl, idapp, msg, data, user, userDest, username_action });
|
||||
}
|
||||
// 2) AMICIZIA / HANDSHAKE
|
||||
else if (
|
||||
act.includes(shared_consts.CallFunz.RICHIESTA_AMICIZIA) ||
|
||||
act.includes(shared_consts.CallFunz.RICHIESTA_HANDSHAKE)
|
||||
) {
|
||||
notifyText = await handleFriends({ bot, cl, idapp, msg, data, user, userDest, username_action });
|
||||
}
|
||||
// 3) CIRCUITI (aggiunta/rimozione)
|
||||
else if (
|
||||
act.includes(shared_consts.CallFunz.ADDUSERTOCIRCUIT) ||
|
||||
act.includes(shared_consts.CallFunz.REMUSERFROMCIRCUIT)
|
||||
) {
|
||||
notifyText = await handleCircuit({ bot, cl, idapp, msg, data, user, userDest, username_action });
|
||||
}
|
||||
// 4) ZOOM (registrazione/presenze)
|
||||
else if (act.includes(shared_consts.CallFunz.REGISTRATION_TOZOOM) || act.includes('ZOOM')) {
|
||||
notifyText = await handleZoom({ bot, cl, idapp, msg, data, user, userDest, username_action });
|
||||
}
|
||||
// 5) RESET PASSWORD
|
||||
else if (act.includes(shared_consts.CallFunz.RESET_PWD)) {
|
||||
notifyText = await handlePassword({ bot, cl, idapp, msg, data, user, userDest, username_action });
|
||||
} else if (act.includes(shared_consts.CallFunz.RICHIESTA_GRUPPO)) {
|
||||
notifyText = await handleGroup({ bot, cl, idapp, msg, data, user, userDest, username_action });
|
||||
}
|
||||
// default
|
||||
else {
|
||||
notifyText = 'Operazione completata';
|
||||
await cl.sendMsg(msg.chat.id, `⚙️ Azione non riconosciuta: ${act}`);
|
||||
}
|
||||
|
||||
await bot.answerCallbackQuery(callbackQuery.id, { text: notifyText || 'OK' });
|
||||
} catch (err) {
|
||||
console.error('❌ callbackHandler error:', err.message);
|
||||
try {
|
||||
await bot.answerCallbackQuery(callbackQuery.id, { text: 'Errore', show_alert: true });
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { handleCallback };
|
||||
@@ -1,54 +0,0 @@
|
||||
// telegram/handlers/circuitHandler.js
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const tools = require('../../tools/general');
|
||||
const { User } = require('../../models/user');
|
||||
|
||||
const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
|
||||
|
||||
async function handleCircuit({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
|
||||
let notifyText = '';
|
||||
|
||||
// Aggiunta al circuito
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.ADDUSERTOCIRCUIT) {
|
||||
const cmd = shared_consts.CIRCUITCMD.ADDUSERTOCIRCUIT;
|
||||
const req = tools.getReqByPar(idapp, username_action);
|
||||
|
||||
// se viene da gruppo usa ifCircuitAlreadyInGroup, altrimenti ifAlreadyInCircuit (come nel tuo codice)
|
||||
const already = data.groupname
|
||||
? await User.ifCircuitAlreadyInGroup(idapp, data.groupname, data.circuitId)
|
||||
: await User.ifAlreadyInCircuit(idapp, data.username, data.circuitId);
|
||||
|
||||
if (!already) {
|
||||
await User.setCircuitCmd(idapp, data.username, data.circuitId, cmd, 1, username_action, { groupname: data.groupname });
|
||||
await cl.sendMsg(msg.chat.id, `✅ ${data.username} aggiunto al circuito ${data.circuitId}`);
|
||||
notifyText = 'Circuito OK';
|
||||
} else {
|
||||
await cl.sendMsg(msg.chat.id, `ℹ️ ${data.username} è già nel circuito ${data.circuitId}`);
|
||||
notifyText = 'Già presente';
|
||||
}
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// Rimozione dal circuito
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REMUSERFROMCIRCUIT) {
|
||||
const cmd = shared_consts.CIRCUITCMD.REMOVEUSERFROMCIRCUIT;
|
||||
await User.setCircuitCmd(idapp, data.username, data.circuitId, cmd, 0, username_action, { groupname: data.groupname });
|
||||
await cl.sendMsg(msg.chat.id, `🗑️ ${data.username} rimosso dal circuito ${data.circuitId}`);
|
||||
notifyText = 'Rimosso';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// NO / annulla
|
||||
if (
|
||||
data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.ADDUSERTOCIRCUIT ||
|
||||
data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REMUSERFROMCIRCUIT
|
||||
) {
|
||||
await cl.sendMsg(msg.chat.id, '❌ Operazione circuito annullata.');
|
||||
notifyText = 'Annullata';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
module.exports = { handleCircuit };
|
||||
@@ -1,24 +0,0 @@
|
||||
const { sendMessage, sendPhoto } = require('../api');
|
||||
const { safeExec } = require('../helpers');
|
||||
|
||||
const sendMsgTelegram = safeExec(async (user, text) => {
|
||||
if (!user || !user.telegram_id) return null;
|
||||
return sendMessage(user.telegram_id, text);
|
||||
});
|
||||
|
||||
const sendMsgTelegramByIdTelegram = safeExec(async (telegramId, text) => {
|
||||
if (!telegramId) return null;
|
||||
return sendMessage(telegramId, text);
|
||||
});
|
||||
|
||||
const sendPhotoTelegram = safeExec(async (chatIdOrUser, photoUrl, caption = '') => {
|
||||
const chatId = typeof chatIdOrUser === 'object' ? chatIdOrUser?.telegram_id : chatIdOrUser;
|
||||
if (!chatId || !photoUrl) return null;
|
||||
return sendPhoto(chatId, photoUrl, caption);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
sendMsgTelegram,
|
||||
sendMsgTelegramByIdTelegram,
|
||||
sendPhotoTelegram,
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
const { sendMessage } = require('../api');
|
||||
const { ADMIN_GROUP_IDS } = require('../config');
|
||||
const { safeExec } = require('../helpers');
|
||||
|
||||
const reportError = safeExec(async (context, err) => {
|
||||
const msg = `🚨 Errore in <b>${context}</b>\n<pre>${err.stack || err.message}</pre>`;
|
||||
for (const id of ADMIN_GROUP_IDS) {
|
||||
await sendMessage(id, msg);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { reportError };
|
||||
@@ -1,61 +0,0 @@
|
||||
// telegram/handlers/friendsHandler.js
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const tools = require('../../tools/general');
|
||||
const { User } = require('../../models/user');
|
||||
const printf = require('util').format;
|
||||
const { handleRegistration, InlineConferma } = require('./registrationHandler');
|
||||
|
||||
async function handleFriends({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
|
||||
let notifyText = '';
|
||||
|
||||
// SI -> amicizia
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RICHIESTA_AMICIZIA) {
|
||||
if (userDest) {
|
||||
const req = tools.getReqByPar(idapp, username_action);
|
||||
const already = await User.isMyFriend(idapp, data.username, data.userDest);
|
||||
if (!already) await User.setFriendsCmd(req, idapp, data.username, data.userDest, shared_consts.FRIENDSCMD.SETFRIEND);
|
||||
await cl.sendMsg(msg.chat.id, '🤝 Amicizia confermata!');
|
||||
notifyText = 'Amicizia OK';
|
||||
}
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// NO -> amicizia (rimuovi/nega)
|
||||
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.RICHIESTA_AMICIZIA) {
|
||||
if (userDest) {
|
||||
const req = tools.getReqByPar(idapp, username_action);
|
||||
const ris = await User.setFriendsCmd(req, idapp, data.username, data.userDest, shared_consts.FRIENDSCMD.REMOVE_FROM_MYFRIENDS);
|
||||
if (ris) {
|
||||
const msgDest = printf(tools.gettranslate('MSG_FRIENDS_NOT_ACCEPTED_CONFIRMED', user.lang), data.username);
|
||||
await localSendMsgByUsername(idapp, data.userDest, msgDest);
|
||||
}
|
||||
await cl.sendMsg(msg.chat.id, '🚫 Amicizia rifiutata.');
|
||||
notifyText = 'Rifiutata';
|
||||
}
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// SI -> handshake
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RICHIESTA_HANDSHAKE) {
|
||||
if (userDest) {
|
||||
const req = tools.getReqByPar(idapp, username_action);
|
||||
const already = await User.isMyHandShake(idapp, data.userDest, data.username);
|
||||
if (!already) await User.setFriendsCmd(req, idapp, data.userDest, data.username, shared_consts.FRIENDSCMD.SETHANDSHAKE);
|
||||
await cl.sendMsg(msg.chat.id, '🤝 Handshake confermato!');
|
||||
notifyText = 'Handshake OK';
|
||||
}
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
// helper locale (equivalente del tuo local_sendMsgTelegram)
|
||||
async function localSendMsgByUsername(idapp, username, text) {
|
||||
const teleg_id = await User.TelegIdByUsername(idapp, username);
|
||||
const cl = require('../telegram.bot.init').getclTelegByidapp(idapp);
|
||||
if (cl && teleg_id) return await cl.sendMsg(teleg_id, text);
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { handleFriends };
|
||||
@@ -1,70 +0,0 @@
|
||||
// telegram/handlers/groupHandler.js
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const tools = require('../../tools/general');
|
||||
const { User } = require('../../models/user');
|
||||
|
||||
const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
|
||||
|
||||
/**
|
||||
* Gestisce conferma/rifiuto a richieste di GRUPPO
|
||||
* Payload data:
|
||||
* - action
|
||||
* - username (mittente originale)
|
||||
* - userDest (destinatario/utente da aggiungere)
|
||||
* - groupId (id o path del gruppo)
|
||||
* - groupname (nome del gruppo)
|
||||
*/
|
||||
async function handleGroup({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
|
||||
let notifyText = '';
|
||||
|
||||
// SI → accetta richiesta d'ingresso nel gruppo
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RICHIESTA_GRUPPO) {
|
||||
// Se l’app ha funzioni di persistenza specifiche, usale se esistono
|
||||
// (non assumo nomi rigidi per non rompere il deploy)
|
||||
if (typeof User.setGroupCmd === 'function') {
|
||||
try {
|
||||
await User.setGroupCmd(idapp, data.userDest || data.username, data.groupId, shared_consts.GROUPCMD.ADDUSERTOGROUP, 1, username_action, { groupname: data.groupname });
|
||||
} catch (e) {
|
||||
console.warn('handleGroup:setGroupCmd non eseguito:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Messaggi di conferma
|
||||
await cl.sendMsg(msg.chat.id, `✅ ${data.userDest || data.username} è stato aggiunto al gruppo ${data.groupname || data.groupId}.`);
|
||||
// Notifica anche l’utente interessato
|
||||
const targetUsername = data.userDest || data.username;
|
||||
const teleg_id = await User.TelegIdByUsername(idapp, targetUsername);
|
||||
if (teleg_id) {
|
||||
await cl.sendMsg(teleg_id, `👥 Sei stato aggiunto al gruppo: ${data.groupname || data.groupId}`);
|
||||
}
|
||||
|
||||
notifyText = 'Gruppo: aggiunta OK';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// NO → rifiuta/annulla richiesta
|
||||
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.RICHIESTA_GRUPPO) {
|
||||
if (typeof User.setGroupCmd === 'function') {
|
||||
try {
|
||||
await User.setGroupCmd(idapp, data.userDest || data.username, data.groupId, shared_consts.GROUPCMD.REMOVEUSERFROMGROUP, 0, username_action, { groupname: data.groupname });
|
||||
} catch (e) {
|
||||
console.warn('handleGroup:setGroupCmd non eseguito:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
await cl.sendMsg(msg.chat.id, '🚫 Richiesta gruppo rifiutata.');
|
||||
// Avvisa il richiedente
|
||||
const targetUsername = data.userDest || data.username;
|
||||
const teleg_id = await User.TelegIdByUsername(idapp, targetUsername);
|
||||
if (teleg_id) {
|
||||
await cl.sendMsg(teleg_id, `❌ La tua richiesta per il gruppo ${data.groupname || data.groupId} è stata rifiutata.`);
|
||||
}
|
||||
|
||||
notifyText = 'Gruppo: rifiutata';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
module.exports = { handleGroup };
|
||||
@@ -1,47 +0,0 @@
|
||||
const { sendMessage } = require('../api');
|
||||
const { safeExec, eachSeries } = require('../helpers');
|
||||
const tools = require('../../tools/general');
|
||||
const {
|
||||
getAdminTelegramUsers,
|
||||
getManagersTelegramUsers,
|
||||
getAllTelegramUsersByApp,
|
||||
} = require('./userQuery');
|
||||
|
||||
const sendMsgTelegramToTheAdminAllSites = safeExec(async (text, alsoGroups = false) => {
|
||||
const apps = await tools.getApps(); // deve restituire {idapp,...}
|
||||
await eachSeries(apps, async (app) => {
|
||||
const admins = await getAdminTelegramUsers(app.idapp);
|
||||
await eachSeries(admins, async (u) => sendMessage(u.telegram_id, text));
|
||||
if (alsoGroups && app?.telegram_admin_group_id) {
|
||||
await sendMessage(app.telegram_admin_group_id, text);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const sendMsgTelegramByIdApp = safeExec(async (idapp, text) => {
|
||||
const users = await getAllTelegramUsersByApp(idapp);
|
||||
await eachSeries(users, async (u) => sendMessage(u.telegram_id, text));
|
||||
});
|
||||
|
||||
const sendMsgTelegramToTheManagers = safeExec(async (idapp, text) => {
|
||||
const managers = await getManagersTelegramUsers(idapp);
|
||||
await eachSeries(managers, async (u) => sendMessage(u.telegram_id, text));
|
||||
});
|
||||
|
||||
const sendMsgTelegramToTheAdmin = safeExec(async (idapp, text) => {
|
||||
const admins = await getAdminTelegramUsers(idapp);
|
||||
await eachSeries(admins, async (u) => sendMessage(u.telegram_id, text));
|
||||
});
|
||||
|
||||
const sendMsgTelegramToTheGroup = safeExec(async (chatId, text) => {
|
||||
if (!chatId) return null;
|
||||
return sendMessage(chatId, text);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
sendMsgTelegramToTheAdminAllSites,
|
||||
sendMsgTelegramByIdApp,
|
||||
sendMsgTelegramToTheManagers,
|
||||
sendMsgTelegramToTheAdmin,
|
||||
sendMsgTelegramToTheGroup,
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
const { sendMessage } = require('../api');
|
||||
const { safeExec } = require('../helpers');
|
||||
|
||||
const sendNotification = safeExec(async (chatId, title, body) => {
|
||||
const msg = `🔔 <b>${title}</b>\n${body}`;
|
||||
await sendMessage(chatId, msg);
|
||||
});
|
||||
|
||||
module.exports = { sendNotification };
|
||||
@@ -1,35 +0,0 @@
|
||||
// telegram/handlers/passwordHandler.js
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const tools = require('../../tools/general');
|
||||
|
||||
const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
|
||||
|
||||
async function handlePassword({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
|
||||
let notifyText = '';
|
||||
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.RESET_PWD) {
|
||||
// Nel tuo codice usavi anche tools.sendNotificationToUser ecc.
|
||||
await tools.sendNotificationToUser(
|
||||
user?._id || msg.chat.id,
|
||||
'🔑 Reset Password',
|
||||
`La password di ${data.username} è stata resettata.`,
|
||||
'/',
|
||||
'',
|
||||
'server',
|
||||
[]
|
||||
);
|
||||
await cl.sendMsg(msg.chat.id, '✅ Password resettata.');
|
||||
notifyText = 'Reset OK';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.RESET_PWD) {
|
||||
await cl.sendMsg(msg.chat.id, '❌ Reset password annullato.');
|
||||
notifyText = 'Annullato';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
module.exports = { handlePassword };
|
||||
@@ -1,65 +0,0 @@
|
||||
const messages = require('../messages');
|
||||
const { phase } = require('../constants');
|
||||
const { safeExec } = require('../helpers');
|
||||
const { sendMessage } = require('../api');
|
||||
const {
|
||||
getAdminTelegramUsers,
|
||||
getManagersTelegramUsers,
|
||||
} = require('./userQuery');
|
||||
|
||||
// locals: { idapp, username, nomeapp, text, ... }
|
||||
const notifyToTelegram = safeExec(async (ph, locals = {}) => {
|
||||
const idapp = String(locals.idapp || '');
|
||||
let text = '';
|
||||
|
||||
const templ = messages.byPhase[ph] || messages.byPhase.GENERIC;
|
||||
text = templ(locals);
|
||||
|
||||
// router di default: manda agli admin dell'app
|
||||
const admins = await getAdminTelegramUsers(idapp);
|
||||
for (const a of admins) {
|
||||
if (a.telegram_id) await sendMessage(a.telegram_id, text);
|
||||
}
|
||||
});
|
||||
|
||||
const askConfirmationUser = safeExec(async (idapp, phaseCode, user) => {
|
||||
const txt = messages.askConfirmationUser({
|
||||
idapp,
|
||||
username: user?.username,
|
||||
nomeapp: user?.nomeapp,
|
||||
});
|
||||
if (user?.telegram_id) await sendMessage(user.telegram_id, txt);
|
||||
});
|
||||
|
||||
// helper semplici
|
||||
const sendNotifToAdmin = safeExec(async (idapp, title, body = '') => {
|
||||
const admins = await getAdminTelegramUsers(String(idapp));
|
||||
const txt = `📣 <b>${title}</b>\n${body}`;
|
||||
for (const a of admins) {
|
||||
if (a.telegram_id) await sendMessage(a.telegram_id, txt);
|
||||
}
|
||||
});
|
||||
|
||||
const sendNotifToManager = safeExec(async (idapp, title, body = '') => {
|
||||
const managers = await getManagersTelegramUsers(String(idapp));
|
||||
const txt = `📣 <b>${title}</b>\n${body}`;
|
||||
for (const m of managers) {
|
||||
if (m.telegram_id) await sendMessage(m.telegram_id, txt);
|
||||
}
|
||||
});
|
||||
|
||||
const sendNotifToAdminOrManager = safeExec(async (idapp, title, body = '', preferManagers = false) => {
|
||||
if (preferManagers) {
|
||||
return sendNotifToManager(idapp, title, body);
|
||||
}
|
||||
return sendNotifToAdmin(idapp, title, body);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
notifyToTelegram,
|
||||
askConfirmationUser,
|
||||
sendNotifToAdmin,
|
||||
sendNotifToManager,
|
||||
sendNotifToAdminOrManager,
|
||||
phase, // re-export utile
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
// telegram/handlers/registrationHandler.js
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const { User } = require('../../models/user');
|
||||
const telegrambot = require('../telegram.bot.init'); // per sendMsgTelegramToTheAdminAllSites
|
||||
const printf = require('util').format;
|
||||
|
||||
const InlineConferma = {
|
||||
RISPOSTA_SI: 'SI_',
|
||||
RISPOSTA_NO: 'NO_',
|
||||
};
|
||||
|
||||
async function handleRegistration({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
|
||||
let notifyText = '';
|
||||
|
||||
// NO alla registrazione
|
||||
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REGISTRATION) {
|
||||
await cl.sendMsg(msg.chat.id, '❌ Registrazione annullata.');
|
||||
notifyText = 'Annullata';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// SI alla registrazione standard
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REGISTRATION) {
|
||||
// set verified (come da tuo codice)
|
||||
await User.setVerifiedReg(idapp, data.username, data.userDest);
|
||||
await cl.sendMsg(msg.chat.id, '✅ Registrazione confermata.');
|
||||
await telegrambot.sendMsgTelegramToTheAdminAllSites(`🆕 Nuova registrazione confermata: ${data.userDest}`);
|
||||
notifyText = 'Registrazione OK';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// SI/NO alla REGISTRATION_FRIEND
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REGISTRATION_FRIEND) {
|
||||
await User.setVerifiedReg(idapp, data.username, data.userDest);
|
||||
await cl.sendMsg(msg.chat.id, '🤝 Conferma amicizia completata!');
|
||||
notifyText = 'Amicizia OK';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REGISTRATION_FRIEND) {
|
||||
await cl.sendMsg(msg.chat.id, '🚫 Invito amicizia rifiutato.');
|
||||
notifyText = 'Rifiutata';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
// deleghe future (es. REGISTRATION_TOZOOM gestita in zoomHandler)
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
module.exports = { handleRegistration, InlineConferma };
|
||||
@@ -1,15 +0,0 @@
|
||||
const { sendMessage, sendPhoto } = require('../api');
|
||||
const { formatUser, safeExec } = require('../helpers');
|
||||
|
||||
const notifyUser = safeExec(async (user, text) => {
|
||||
if (!user?.telegram_id) return;
|
||||
const msg = `👋 Ciao ${formatUser(user)}\n${text}`;
|
||||
await sendMessage(user.telegram_id, msg);
|
||||
});
|
||||
|
||||
const sendUserPhoto = safeExec(async (user, photoUrl, caption) => {
|
||||
if (!user?.telegram_id) return;
|
||||
await sendPhoto(user.telegram_id, photoUrl, caption);
|
||||
});
|
||||
|
||||
module.exports = { notifyUser, sendUserPhoto };
|
||||
@@ -1,32 +0,0 @@
|
||||
const { User } = require('../../models/user');
|
||||
|
||||
async function getTelegramUsersByQuery(query = {}) {
|
||||
return User.find({
|
||||
...query,
|
||||
telegram_id: { $exists: true, $ne: null },
|
||||
}).lean();
|
||||
}
|
||||
|
||||
async function getAdminTelegramUsers(idapp) {
|
||||
return getTelegramUsersByQuery({ idapp, isAdmin: true });
|
||||
}
|
||||
|
||||
async function getManagersTelegramUsers(idapp) {
|
||||
return getTelegramUsersByQuery({ idapp, isManager: true });
|
||||
}
|
||||
|
||||
async function getFacilitatoriTelegramUsers(idapp) {
|
||||
return getTelegramUsersByQuery({ idapp, isFacilitatore: true });
|
||||
}
|
||||
|
||||
async function getAllTelegramUsersByApp(idapp) {
|
||||
return getTelegramUsersByQuery({ idapp });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTelegramUsersByQuery,
|
||||
getAdminTelegramUsers,
|
||||
getManagersTelegramUsers,
|
||||
getFacilitatoriTelegramUsers,
|
||||
getAllTelegramUsersByApp,
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
// telegram/handlers/zoomHandler.js
|
||||
const shared_consts = require('../../tools/shared_nodejs');
|
||||
const { User } = require('../../models/user');
|
||||
|
||||
const InlineConferma = { RISPOSTA_SI: 'SI_', RISPOSTA_NO: 'NO_' };
|
||||
|
||||
async function handleZoom({ bot, cl, idapp, msg, data, user, userDest, username_action }) {
|
||||
let notifyText = '';
|
||||
|
||||
if (data.action === InlineConferma.RISPOSTA_SI + shared_consts.CallFunz.REGISTRATION_TOZOOM) {
|
||||
// nelle tue callback originale: conferma registrazione + messaggio
|
||||
await User.setVerifiedReg(idapp, data.username, data.userDest);
|
||||
await cl.sendMsg(msg.chat.id, '🟢 Accesso Zoom confermato!');
|
||||
notifyText = 'Zoom OK';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
if (data.action === InlineConferma.RISPOSTA_NO + shared_consts.CallFunz.REGISTRATION_TOZOOM) {
|
||||
await cl.sendMsg(msg.chat.id, '🚫 Accesso Zoom rifiutato.');
|
||||
notifyText = 'Rifiutato';
|
||||
return notifyText;
|
||||
}
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
module.exports = { handleZoom };
|
||||
@@ -1,31 +0,0 @@
|
||||
function formatUser(user) {
|
||||
const u = user || {};
|
||||
const username = u.username || (u.profile && u.profile.username_telegram) || 'no_username';
|
||||
return `${u.name || ''} ${u.surname || ''} (@${username})`.trim();
|
||||
}
|
||||
|
||||
function safeExec(fn) {
|
||||
return async (...args) => {
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (e) {
|
||||
console.error('Telegram helper error:', e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function ensureArray(x) {
|
||||
if (!x) return [];
|
||||
return Array.isArray(x) ? x : [x];
|
||||
}
|
||||
|
||||
// utility semplice per evitare flood (se ti serve rate-limit: usa bottleneck)
|
||||
async function eachSeries(arr, fn) {
|
||||
for (const item of arr) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fn(item);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { formatUser, safeExec, ensureArray, eachSeries };
|
||||
@@ -1,66 +0,0 @@
|
||||
const messages = require('./messages');
|
||||
|
||||
// base api/handlers già creati in precedenza
|
||||
const { sendMessage, sendPhoto } = require('./api');
|
||||
const { sendToAdmins } = require('./handlers/adminHandler');
|
||||
const { notifyUser, sendUserPhoto } = require('./handlers/userHandler');
|
||||
const { sendNotification } = require('./handlers/notificationHandler');
|
||||
const { reportError } = require('./handlers/errorHandler');
|
||||
|
||||
// NUOVI HANDLER aggiunti ora
|
||||
const {
|
||||
sendMsgTelegram,
|
||||
sendMsgTelegramByIdTelegram,
|
||||
sendPhotoTelegram,
|
||||
} = require('./handlers/directHandler');
|
||||
|
||||
const {
|
||||
sendMsgTelegramToTheAdminAllSites,
|
||||
sendMsgTelegramByIdApp,
|
||||
sendMsgTelegramToTheManagers,
|
||||
sendMsgTelegramToTheAdmin,
|
||||
sendMsgTelegramToTheGroup,
|
||||
} = require('./handlers/multiAppHandler');
|
||||
|
||||
const {
|
||||
notifyToTelegram,
|
||||
askConfirmationUser,
|
||||
sendNotifToAdmin,
|
||||
sendNotifToManager,
|
||||
sendNotifToAdminOrManager,
|
||||
phase,
|
||||
} = require('./handlers/phaseHandler');
|
||||
|
||||
module.exports = {
|
||||
// messaggi/template
|
||||
messages,
|
||||
phase,
|
||||
|
||||
// API raw
|
||||
sendMessage,
|
||||
sendPhoto,
|
||||
|
||||
// generico
|
||||
sendToAdmins,
|
||||
notifyUser,
|
||||
sendUserPhoto,
|
||||
sendNotification,
|
||||
reportError,
|
||||
|
||||
// EQUIVALENTI del vecchio file
|
||||
sendMsgTelegram, // (user, text)
|
||||
sendMsgTelegramByIdTelegram, // (telegramId, text)
|
||||
sendPhotoTelegram, // (chatIdOrUser, photoUrl, caption)
|
||||
|
||||
sendMsgTelegramToTheAdminAllSites, // (text, alsoGroups?)
|
||||
sendMsgTelegramByIdApp, // (idapp, text)
|
||||
sendMsgTelegramToTheManagers, // (idapp, text)
|
||||
sendMsgTelegramToTheAdmin, // (idapp, text)
|
||||
sendMsgTelegramToTheGroup, // (chatId, text)
|
||||
|
||||
notifyToTelegram, // (phase, locals)
|
||||
askConfirmationUser, // (idapp, phase, user)
|
||||
sendNotifToAdmin, // (idapp, title, body)
|
||||
sendNotifToManager, // (idapp, title, body)
|
||||
sendNotifToAdminOrManager, // (idapp, title, body, preferManagers?)
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
module.exports = {
|
||||
// messaggi generici
|
||||
serverStarted: (dbName) => `🚀 Il server <b>${dbName}</b> è stato avviato con successo.`,
|
||||
userUnlocked: (user) => `⚠️ L'utente <b>${user.username}</b> (${user.name} ${user.surname}) è stato sbloccato.`,
|
||||
errorOccurred: (context, err) =>
|
||||
`❌ Errore in <b>${context}</b>\n<code>${(err && err.message) || err}</code>`,
|
||||
notifyAdmin: (msg) => `📢 Notifica Admin:\n${msg}`,
|
||||
|
||||
// fasi logiche
|
||||
byPhase: {
|
||||
REGISTRATION: (locals = {}) =>
|
||||
`🆕 Nuova registrazione su <b>${locals.nomeapp || 'App'}</b>\nUtente: <b>${locals.username}</b>`,
|
||||
REGISTRATION_CONFIRMED: (locals = {}) =>
|
||||
`✅ Registrazione confermata su <b>${locals.nomeapp || 'App'}</b> da <b>${locals.username}</b>`,
|
||||
RESET_PWD: (locals = {}) =>
|
||||
`🔁 Reset password richiesto per <b>${locals.username}</b>`,
|
||||
NOTIFICATION: (locals = {}) =>
|
||||
`🔔 Notifica: ${locals.text || ''}`,
|
||||
GENERIC: (locals = {}) =>
|
||||
`${locals.text || ''}`,
|
||||
},
|
||||
|
||||
askConfirmationUser: (locals = {}) =>
|
||||
`👋 Ciao <b>${locals.username}</b>!\nConfermi l'operazione su <b>${locals.nomeapp || 'App'}</b>?`,
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
http://localhost:8084/signup/paoloar77/SuryaArena/5356627050
|
||||
@@ -1,488 +0,0 @@
|
||||
/**
|
||||
* ======================================================
|
||||
* TELEGRAM BOT INIT (derived from telegrambot_OLD.js)
|
||||
* ======================================================
|
||||
* - Gestione multi-bot per app (arrTelegram)
|
||||
* - Classe Telegram con funzioni core
|
||||
* - Invio messaggi, immagini, notifiche
|
||||
* - Callback Query, menu e inline keyboard
|
||||
* - API pubbliche per admin, manager, utenti
|
||||
* - Compatibile con tools, User, Circuit, ecc.
|
||||
* ======================================================
|
||||
*/
|
||||
|
||||
const TelegramBot = require('node-telegram-bot-api');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const sharp = require('sharp');
|
||||
|
||||
const tools = require('../tools/general');
|
||||
const shared_consts = require('../tools/shared_nodejs');
|
||||
const server_constants = require('../tools/server_constants');
|
||||
const { Site } = require('../models/site');
|
||||
|
||||
const { handleCallback } = require('./handlers/callbackHandler');
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 🔹 COSTANTI, ENUM, EMOJI E TEXT
|
||||
// -----------------------------------------------------------------------------
|
||||
const emo = {
|
||||
JOY: '😂',
|
||||
JOY2: '🤣',
|
||||
DANCER: '💃',
|
||||
STARS: '✨',
|
||||
FIRE: '🔥',
|
||||
SUN: '☀️',
|
||||
TV: '📺',
|
||||
NEWSPAPER: '🗞',
|
||||
KISS: '😘',
|
||||
PENCIL: '✏️',
|
||||
DREAM: '🏖',
|
||||
EYES: '😜',
|
||||
DIZZY: '💫',
|
||||
ONE_HUNDRED: '💯',
|
||||
SMILE_STAR: '🤩', // Star-struck
|
||||
LEFT_FACING_FIST: '🤛', // Left-facing fist
|
||||
CHECK_VERDE: '✅', // White check mark (verde)
|
||||
CHECK_GRIGIA: '☑️', // Ballot box with check (grigia)
|
||||
CROSS_ROSSA: '❌', // X (rossa)
|
||||
ENVELOPE: '✉️', // Envelope
|
||||
EXCLAMATION_MARK: '❗', // Exclamation mark
|
||||
QUESTION_MARK: '❓', // Question mark
|
||||
ARROW_RIGHT: '➡️', // Arrow pointing to the right
|
||||
INVITATI: '',
|
||||
HEART: '❤️',
|
||||
BLUE_HEART: '💙',
|
||||
GREEN_HEART: '💚',
|
||||
YELLOW_HEART: '💛',
|
||||
PURPLE_HEART: '💜',
|
||||
GIFT_HEART: '💝',
|
||||
GIFT: '🎁',
|
||||
ROBOT_FACE: '🤖',
|
||||
ADMIN: '💁',
|
||||
MALE: '💁♂️',
|
||||
FEMALE: '👩🦱',
|
||||
INNOCENT: '😇',
|
||||
CREDIT_CARD: '💳',
|
||||
PERSON: '🧑',
|
||||
};
|
||||
|
||||
MsgBot = {
|
||||
OK: ['si', 'ok'],
|
||||
CUORE: ['❤️', '💚️', '💜'],
|
||||
CIAO: ['ciao', 'ciaoo', 'hola', 'holaa', 'hey', 'salve', 'buongiorno', 'buondi', 'ciao ❤️'],
|
||||
CI_SEI: ['ci sei', "c'è qualcuno", "c'è nessuno"],
|
||||
CHI_SONO_IO: ['chi sono io', 'chi sono'],
|
||||
COME_STAI: ['tutto bene', 'come stai', 'come stai', 'come va', 'come butta', 'come va oggi'],
|
||||
COME_TI_CHIAMI: [
|
||||
'chi sei',
|
||||
'come ti chiami',
|
||||
"qual'è il tuo nome",
|
||||
"qual'e' il tuo nome",
|
||||
'che lavoro fai',
|
||||
'di cosa ti occupi',
|
||||
],
|
||||
COSA_FAI: ['cosa fai', 'cosa combini', 'che fai'],
|
||||
QUANTI_ANNI_HAI: ['quanti anni hai', 'che età hai'],
|
||||
SEI_LIBERO_STASERA: [
|
||||
'sei libera stasera',
|
||||
'sei libero stasera',
|
||||
'usciamo insieme',
|
||||
"fare l'amore con me",
|
||||
'fare sesso',
|
||||
'vuoi scopare',
|
||||
'vuoi trombare',
|
||||
],
|
||||
MI_TROVI_UN_MOROSO: [
|
||||
'trovi un moroso',
|
||||
'una morosa',
|
||||
'fidanzato',
|
||||
'fidanzata',
|
||||
'trovi un marito',
|
||||
'trovi una moglie',
|
||||
],
|
||||
CHAT_EMPOWER: ['chat empower'],
|
||||
MASCHIO_FEMMINA: ['sei uomo o donna', 'sei maschio o femmina', 'sei ragazzo o ragazza', 'che sesso hai'],
|
||||
DAMMI_UN_BACIO: ['dammi un bacio', 'baciami'],
|
||||
HAHA: ['hahaha', 'ahah', '😂'],
|
||||
MI_AMI: ['mi ami'],
|
||||
TI_AMO: ['ti amo', 'ti adoro', 'ti lovvo'],
|
||||
PREGO: ['prego', 'Prego ! 💋💋💋'],
|
||||
GRAZIE: ['grazie ainy', 'grazie', 'grazie mille', 'graziee', 'grazie ❤', 'grazie️❤', 'grazie 😘', 'grazie😘'],
|
||||
PRINCIPE_AZZURRO: ['principe azzurro'],
|
||||
START_INV: ['/start inv'],
|
||||
COSE_COVID: ["cos'è il covid", 'cosa è il covid'],
|
||||
COVID: ['covid'],
|
||||
SPOSAMI: ['sposami', 'vuoi sposar', 'sei sposat', 'ci sposiamo', 'ti sposo', 'sei sposat', 'mi sposi'],
|
||||
CHE_TEMPO_FA: ['che tempo'],
|
||||
NON_TROO_INVITATI: ['non trovo invitati', 'non riesco a trovare invitati'],
|
||||
TROVAMI_UN_UOMO_DONNA: ['trovami un uomo', 'trovami una donna', 'esiste una donna per me', 'esiste un uomo per me'],
|
||||
PAROLACCE: ['stronz', 'fanculo', 'fottiti', 'cagare', 'ammazzat', 'muori', 'cretino', 'stupido'],
|
||||
COME_SI_CHIAMA: ['come si chiama'],
|
||||
PROSSIMO_ZOOM: ['prossimo zoom', 'fare lo zoom', 'gli zoom', 'conferenz', 'zoom'],
|
||||
LAVAGNA: ['lavagna', 'Lavagna', 'LAVAGNA'],
|
||||
SEI_LIBERO_DI_RESPIRARE: ['sei libero di respirare'],
|
||||
SEI_LIBERO: ['sei liber', 'sei sposat', 'sei fidanzat', 'sei single'],
|
||||
AIUTO: [
|
||||
'help',
|
||||
'aiuto',
|
||||
'ho bisogno di',
|
||||
'ho problemi',
|
||||
'non riesco',
|
||||
'mi puoi aiutare',
|
||||
'mi aiuti',
|
||||
'aiutami',
|
||||
'posso chiederti',
|
||||
'puoi aiutarmi',
|
||||
],
|
||||
UOMO: ['uomo', 'maschio'],
|
||||
SORPRESA: ['noo', 'davvero', 'sii', 'facciamo festa', 'è qui la festa', 'festa'],
|
||||
UGUALE: ['👍🏻', '✨', '❤🏻', '⭐', '❤', '❤❤', '🤩'],
|
||||
SI: ['si', 'yes'],
|
||||
NO: ['no', 'noo'],
|
||||
DONNA: ['donna', 'femmina'],
|
||||
FARE_DOMANDA: ['fare una domanda', 'posso farti una domanda'],
|
||||
DIVENTERO_RICCA: ['diventerò ricc'],
|
||||
DOVE_VUOI_VIVERE: ['dove vuoi vivere'],
|
||||
MA_ALLORA: ['ma allora'],
|
||||
};
|
||||
|
||||
const MsgRisp = {
|
||||
CHAT_EMPOWER:
|
||||
'Entra nella Chat EMPOWER !!!\n' +
|
||||
'https://t.me/joinchat/C741mkx5QYXu-kyYCYvA8g ' +
|
||||
emo.PURPLE_HEART +
|
||||
emo.GIFT_HEART +
|
||||
emo.BLUE_HEART,
|
||||
};
|
||||
|
||||
function getemojibynumber(number) {
|
||||
if (number === 0) {
|
||||
return '0️⃣'; // zero
|
||||
} else if (number === 1) {
|
||||
return '1️⃣'; // one
|
||||
} else if (number === 2) {
|
||||
return '2️⃣'; // two
|
||||
} else if (number === 3) {
|
||||
return '3️⃣'; // three
|
||||
} else if (number === 4) {
|
||||
return '4️⃣'; // four
|
||||
} else if (number === 5) {
|
||||
return '5️⃣'; // five
|
||||
} else if (number === 6) {
|
||||
return '6️⃣'; // six
|
||||
} else if (number === 7) {
|
||||
return '7️⃣'; // seven
|
||||
} else if (number === 8) {
|
||||
return '8️⃣'; // eight
|
||||
} else if (number === 9) {
|
||||
return '9️⃣'; // nine
|
||||
} else {
|
||||
return number;
|
||||
}
|
||||
}
|
||||
|
||||
const Menu = {
|
||||
LANG_IT: '🇮🇹 Italiano', // Bandiera italiana
|
||||
LANG_EN: '🇬🇧 English', // Bandiera del Regno Unito
|
||||
LANG_ES: '🇪🇸 Español', // Bandiera spagnola
|
||||
LANG_FR: '🇫🇷 Français', // Bandiera francese
|
||||
LANG_SI: '🇸🇮 Slovenski', // Bandiera slovena
|
||||
LANG_PT: '🇵🇹 Português', // Bandiera portoghese
|
||||
LANG: '🌐 Language', // Globo con meridiani
|
||||
CHAT_PERSONALE: '👩💼💻', // Donna impiegata + computer
|
||||
EXIT_TELEGRAM: 'exittotelegram',
|
||||
MSG_TO_USER: 'sendmsgto',
|
||||
ADMIN: '💁♀️ Admin', // Persona al banco informazioni
|
||||
AIUTO: '🔮 Help', // Cristallo magico
|
||||
ALTRO: '📰 Altro', // Giornale
|
||||
SETPICPROFILE: '🖼 SetPicProfile', // Cornice con foto
|
||||
RESETPWD: '🔑 SetResetPwd', // Chiave
|
||||
MSG_SI_INVITATI_NO_7REQ_INVITATI: '📩Inv e NO 7 Req', // Busta
|
||||
MSGSTAFF: '📩 Invia a STAFF', // Busta
|
||||
MSGAPPARTIENE_CIRCUITI_RIS: 'Invia a Utenti dei Circuiti RIS',
|
||||
MSGPAOLO: '📩 Invia a SURYA', // Busta
|
||||
RESTART_SRV: '📩Restart-NodeJs', // Busta
|
||||
REBOOT_SRV: '📩Reboot-VPS!', // Busta
|
||||
EXECSH: '📩ExecSH', // Busta
|
||||
LOG_SRV: '🖥Logserver.sh', // Monitor
|
||||
MSGATUTTI: '📩 Invia a TUTTI', // Busta
|
||||
it: {
|
||||
ACCEDI: '👤 Accedi', // Persona
|
||||
LAVAGNA: '🕉 Lavagna', // Simbolo Om
|
||||
LINK_CONDIVIDERE: '🔗 Link da condividere', // Link
|
||||
ZOOM: 'ℹ️ Zoom (Conferenze)', // Informazione
|
||||
INFO: 'ℹ️ Informazioni', // Informazione
|
||||
ASSISTENZA: '👐 Le Chat', // Mani aperte
|
||||
INDIETRO: '🔙 Indietro', // Freccia indietro
|
||||
SI: '👍 SI', // Pollice su
|
||||
NO: '👎 NO', // Pollice giù
|
||||
ESCI_DA_CHAT: '📩 Esci dalla Conversazione', // Busta
|
||||
NUOVOSITO: '',
|
||||
},
|
||||
es: {
|
||||
ACCEDI: '👤 Entra',
|
||||
LAVAGNA: '🕉 Tablero',
|
||||
LINK_CONDIVIDERE: '🔗 Enlaces para compartir',
|
||||
ZOOM: 'ℹ️ Zoom (Conferencias)',
|
||||
INFO: 'ℹ️ Información',
|
||||
ASSISTENZA: '👐 Chats',
|
||||
INDIETRO: '🔙 Volver',
|
||||
SI: '👍 SÍ',
|
||||
NO: '👎 NO',
|
||||
ESCI_DA_CHAT: '📩 Salir de la conversación',
|
||||
},
|
||||
fr: {
|
||||
ACCEDI: '👤 Entrez',
|
||||
LAVAGNA: '🕉 Tableau de bord',
|
||||
LINK_CONDIVIDERE: '🔗 Liens à partager',
|
||||
ZOOM: 'ℹ️ Zoom (Conférences)',
|
||||
INFO: 'ℹ️ Informations',
|
||||
ASSISTENZA: '👐 Les chats',
|
||||
INDIETRO: '🔙 Retour',
|
||||
SI: '👍 OUI',
|
||||
NO: '👎 NON',
|
||||
ESCI_DA_CHAT: '📩 Quitter la conversation',
|
||||
},
|
||||
si: {
|
||||
ACCEDI: '👤 Prijava',
|
||||
LAVAGNA: '🕉 Tabla',
|
||||
LINK_CONDIVIDERE: '🔗 Link za vpis oseb',
|
||||
ZOOM: 'ℹ️ Zoom (Konference)',
|
||||
INFO: 'ℹ️ Informacije',
|
||||
ASSISTENZA: '👐 jev klepet',
|
||||
INDIETRO: '🔙 Nazaj',
|
||||
SI: '👍 DA',
|
||||
NO: '👎 NE',
|
||||
ESCI_DA_CHAT: '📩 Zaprite pogovor',
|
||||
},
|
||||
pt: {
|
||||
ACCEDI: '👤 Entre',
|
||||
LAVAGNA: '🕉 Tablero',
|
||||
LINK_CONDIVIDERE: '🔗 Links para compartilhar',
|
||||
ZOOM: 'ℹ️ Zoom (Conferências)',
|
||||
INFO: 'ℹ️ Informações',
|
||||
ASSISTENZA: '👐 Chats',
|
||||
INDIETRO: '🔙 Voltar',
|
||||
SI: '👍 SIM',
|
||||
NO: '👎 NÃO',
|
||||
ESCI_DA_CHAT: '📩 Sair da Conversa',
|
||||
},
|
||||
enUs: {
|
||||
ACCEDI: '👤 Enter',
|
||||
LAVAGNA: '🕉 DashBoard',
|
||||
LINK_CONDIVIDERE: '🔗 Link to Share',
|
||||
ZOOM: 'ℹ️ Zoom (Conference)',
|
||||
INFO: 'ℹ️ Info',
|
||||
ASSISTENZA: '👐 Chats',
|
||||
INDIETRO: '🔙 Back',
|
||||
SI: '👍 YES',
|
||||
NO: '👎 NO',
|
||||
ESCI_DA_CHAT: '📩 Exit to the Conversation',
|
||||
},
|
||||
};
|
||||
|
||||
const CONTA_SOLO = 'contasolo';
|
||||
const RICEVI_EMAIL = 'riceviemail';
|
||||
const NOME_COGNOME = 'nomecognome';
|
||||
const CHEDI_SE_IMBARCARTI = 'chiedi_se_imbarcarti';
|
||||
|
||||
const phase = {
|
||||
REGISTRATION: 'REGISTRATION',
|
||||
REGISTRATION_CONFIRMED: 'REGISTRATION_CONFIRMED',
|
||||
RESET_PWD: 'RESET_PWD',
|
||||
NOTIFICATION: 'NOTIFICATION',
|
||||
};
|
||||
|
||||
const roles = {
|
||||
ADMIN: 'ADMIN',
|
||||
MANAGER: 'MANAGER',
|
||||
FACILITATORE: 'FACILITATORE',
|
||||
EDITOR: 'EDITOR',
|
||||
SOCIO: 'SOCIO',
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 🔹 REGISTRY MULTI-BOT (arrTelegram)
|
||||
// -----------------------------------------------------------------------------
|
||||
const arrTelegram = [];
|
||||
|
||||
function getclTelegByidapp(idapp) {
|
||||
const rec = arrTelegram.find((r) => String(r.idapp) === String(idapp));
|
||||
return rec ? rec.cl : null;
|
||||
}
|
||||
function getclTelegBytoken(token) {
|
||||
const rec = arrTelegram.find((r) => r.cl?.token === token);
|
||||
return rec ? rec.cl : null;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 🔹 FUNZIONI LOCALI
|
||||
// -----------------------------------------------------------------------------
|
||||
async function local_sendMsgTelegram(idapp, username, text) {
|
||||
const teleg_id = await User.TelegIdByUsername(idapp, username);
|
||||
const cl = getclTelegByidapp(idapp);
|
||||
if (cl && teleg_id) return await cl.sendMsg(teleg_id, text);
|
||||
return null;
|
||||
}
|
||||
|
||||
async function local_sendMsgTelegramToTheManagers(idapp, text, msg, username_bo) {
|
||||
const managers = await User.getusersManagers(idapp);
|
||||
const username = username_bo || msg?.chat?.username;
|
||||
const fullmsg = `${emo.ROBOT_FACE}: Da ${tools.getNomeCognomeTelegram(msg)} (${username})\n${text}`;
|
||||
if (managers)
|
||||
for (const m of managers) {
|
||||
const cl = getclTelegByidapp(idapp);
|
||||
if (cl && m.profile?.teleg_id)
|
||||
await cl.sendMsg(m.profile.teleg_id, fullmsg, undefined, undefined, undefined, undefined, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 🔹 CLASSE TELEGRAM
|
||||
// -----------------------------------------------------------------------------
|
||||
class Telegram {
|
||||
constructor(idapp, bot) {
|
||||
this.idapp = idapp;
|
||||
this.bot = bot;
|
||||
this.token = bot.token;
|
||||
this.arrUsers = [];
|
||||
this.lastid = 0;
|
||||
}
|
||||
|
||||
// Invia un messaggio base
|
||||
async sendMsg(chatId, text, parseMode = 'HTML', MyForm = null, message_id, chat_id, ripr_menuPrec, img) {
|
||||
if (!chatId) return;
|
||||
try {
|
||||
await this.bot.sendMessage(chatId, text, { parse_mode: parseMode });
|
||||
} catch (err) {
|
||||
console.error('❌ sendMsg error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Invia immagine con fallback
|
||||
async sendImageToTelegram(chatId, imgPath, caption = '') {
|
||||
if (!chatId || !imgPath) return;
|
||||
try {
|
||||
const buffer = fs.readFileSync(imgPath);
|
||||
const sharped = await sharp(buffer).resize({ width: 1280 }).toBuffer();
|
||||
await this.bot.sendPhoto(chatId, sharped, { caption });
|
||||
} catch (err) {
|
||||
console.error('⚠️ sendImageToTelegram fallback:', err.message);
|
||||
try {
|
||||
await this.bot.sendDocument(chatId, imgPath, { caption });
|
||||
} catch (e) {
|
||||
console.error('❌ sendDocument fallback:', e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder per ricezione messaggi
|
||||
async receiveMsg(msg) {
|
||||
console.log('💬', this.idapp, msg.text);
|
||||
}
|
||||
|
||||
// Placeholder per start
|
||||
async start(msg) {
|
||||
await this.sendMsg(msg.chat.id, `${emo.ROBOT_FACE} Benvenuto nel bot ${tools.getNomeAppByIdApp(this.idapp)}!`);
|
||||
}
|
||||
|
||||
// Inline keyboard esempio
|
||||
getInlineKeyboard(buttons) {
|
||||
return { reply_markup: { inline_keyboard: buttons } };
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 🔹 API PUBBLICHE
|
||||
// -----------------------------------------------------------------------------
|
||||
async function sendMsgTelegramToTheAdminAllSites(text, senzaintestazione) {
|
||||
const apps = await tools.getApps();
|
||||
for (const app of apps) {
|
||||
const filled = text.replace('{appname}', tools.getNomeAppByIdApp(app.idapp));
|
||||
await sendMsgTelegramToTheAdmin(app.idapp, filled, senzaintestazione);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMsgTelegramToTheAdmin(idapp, text, senzaintestazione) {
|
||||
const usersadmin = await User.getusersAdmin(idapp);
|
||||
let head = emo.ROBOT_FACE + '[BOT-ADMIN]' + emo.ADMIN + ': ';
|
||||
if (senzaintestazione) head = '';
|
||||
if (usersadmin)
|
||||
for (const rec of usersadmin) {
|
||||
if (User.isAdmin(rec.perm)) {
|
||||
await sendMsgTelegramByIdTelegram(idapp, rec.profile.teleg_id, head + text);
|
||||
await tools.snooze(300);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function sendMsgTelegramByIdTelegram(idapp, idtelegram, text, message_id, chat_id, ripr_menuPrec, MyForm = null, img = '') {
|
||||
if (!idtelegram) return;
|
||||
const cl = getclTelegByidapp(idapp);
|
||||
if (cl) return await cl.sendMsg(idtelegram, text, null, MyForm, message_id, chat_id, ripr_menuPrec, img, { idapp });
|
||||
}
|
||||
|
||||
async function sendMsgTelegramToTheManagers(idapp, text) {
|
||||
const managers = await User.getusersManagers(idapp);
|
||||
if (managers)
|
||||
for (const rec of managers) {
|
||||
await sendMsgTelegramByIdTelegram(idapp, rec.profile.teleg_id, text);
|
||||
await tools.snooze(300);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 🔹 CALLBACK QUERY HANDLER (semplificata ma compatibile)
|
||||
// -----------------------------------------------------------------------------
|
||||
function setupCallback(bot, cl) {
|
||||
bot.on('callback_query', async (callbackQuery) => {
|
||||
await handleCallback(bot, cl, callbackQuery);
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 🔹 INIZIALIZZAZIONE BOT MULTI-APP
|
||||
// -----------------------------------------------------------------------------
|
||||
async function initTelegramBots() {
|
||||
const arrApps = await tools.getApps();
|
||||
for (const app of arrApps) {
|
||||
const idapp = app.idapp;
|
||||
const token = tools.getTelegramKeyByIdApp(idapp);
|
||||
const nomebot = tools.getTelegramBotNameByIdApp(idapp);
|
||||
if (!token) continue;
|
||||
|
||||
console.log(`🤖 Avvio BOT: ${nomebot} (${idapp})`);
|
||||
const bot = new TelegramBot(token, { polling: true });
|
||||
const cl = new Telegram(idapp, bot);
|
||||
arrTelegram.push({ idapp, cl });
|
||||
|
||||
bot.onText(/\/start/, (msg) => cl.start(msg));
|
||||
bot.on('message', async (msg) => cl.receiveMsg(msg));
|
||||
setupCallback(bot, cl);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 🔹 ESPORTAZIONI PUBBLICHE
|
||||
// -----------------------------------------------------------------------------
|
||||
module.exports = {
|
||||
Telegram,
|
||||
initTelegramBots,
|
||||
getclTelegByidapp,
|
||||
getclTelegBytoken,
|
||||
sendMsgTelegramToTheAdminAllSites,
|
||||
sendMsgTelegramToTheAdmin,
|
||||
sendMsgTelegramByIdTelegram,
|
||||
sendMsgTelegramToTheManagers,
|
||||
local_sendMsgTelegram,
|
||||
local_sendMsgTelegramToTheManagers,
|
||||
phase,
|
||||
roles,
|
||||
emo,
|
||||
};
|
||||
@@ -1330,5 +1330,6 @@ module.exports = {
|
||||
|
||||
JOB_TO_EXECUTE: {
|
||||
MIGRATION_SECTORS_DIC25: 'Migration_Sectors_Dic_2025',
|
||||
MIGRATION_TELEGRAM_30DIC25: 'Migration_Telegram_30Dic_2025',
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user