Files
freeplanet_serverside/src/controllers/viaggi/TrasportiNotifications.js
2025-12-30 11:36:42 +01:00

847 lines
28 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;