/**
* 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 = `
`;
}
// Info viaggio
let rideInfoHtml = '';
if (data.departure && data.destination) {
rideInfoHtml = `
${emo.PIN}
Partenza
${data.departure}
${emo.ARROW}
${emo.PIN}
Destinazione
${data.destination}
${data.date ? `
${emo.CALENDAR} ${data.date}
${data.time ? `${emo.CLOCK} ${data.time}` : ''}
` : ''}
`;
}
return `
${body}
${rideInfoHtml}
${data.reason ? `
${t('RIDE_CANCELLED_REASON')}
` : ''}
${ctaHtml}
`;
}
// =============================================================================
// 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} ${title}\n\n${body}`;
// Aggiungi info viaggio
if (data.departure && data.destination) {
message += `\n\n${emo.PIN} Percorso:\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${t('RIDE_CANCELLED_REASON')}`;
}
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;