From 169f4d6baf7122a414263c6035216d99dc0ef07f Mon Sep 17 00:00:00 2001 From: Surya Paolo Date: Tue, 30 Dec 2025 11:36:37 +0100 Subject: [PATCH] - Aggiornamento Viaggi --- quasar.config.ts | 5 +- src-pwa/custom-service-worker.js | 835 +++++--- src/components/CMyElem/CMyElem.ts | 2 +- src/components/CMyElem/CMyElem.vue | 2 - src/components/checkemail/checkemail.ts | 6 +- src/model/GlobalStore.ts | 1 + .../components/ride/CommunityFilters.vue | 561 ++++++ .../components/ride/CommunityRideCard.vue | 655 +++++++ .../components/ride/ContribTypeSelector.ts | 2 +- .../viaggi/components/ride/MyRideCard.vue | 14 +- .../components/ride/RecurrenceSelector.ts | 116 +- .../components/ride/RecurrenceSelector.vue | 55 +- .../viaggi/components/ride/RideCalendar.vue | 920 +++++++++ .../viaggi/components/ride/RideCard.ts | 6 +- src/modules/viaggi/components/ride/RideMap.ts | 2 +- .../viaggi/components/ride/VehicleSelector.ts | 18 +- .../components/ride/VehicleSelector.vue | 2 +- .../widgets/RideWidget/RideWidget.ts | 84 +- .../viaggi/composables/useCommunityrides.ts | 366 ++++ .../viaggi/composables/useContribTypes.ts | 2 +- .../viaggi/composables/useNotifications.ts | 284 +++ .../viaggi/composables/useRecentCities.ts | 8 +- .../composables/useRideNotifications.ts | 56 + src/modules/viaggi/composables/useRides.ts | 106 +- .../viaggi/composables/useSavedFilters.ts | 50 + .../viaggi/pages/CommunityRidesPage.vue | 1725 +++++++++++++++++ src/modules/viaggi/pages/DriverProfilePage.ts | 12 +- src/modules/viaggi/pages/MyRidesPage.ts | 81 +- src/modules/viaggi/pages/MyRidesPage.vue | 404 ++-- .../viaggi/pages/NotificationsPage.vue | 973 ++++++++++ src/modules/viaggi/pages/Requestspage.vue | 7 +- src/modules/viaggi/pages/RideCreatePage.ts | 12 +- src/modules/viaggi/pages/RideDetailPage.ts | 6 +- src/modules/viaggi/pages/RideSearchPage.ts | 4 +- src/modules/viaggi/pages/Settingspage.vue | 1652 ++++++++++------ src/modules/viaggi/pages/Vehicleeditpage.vue | 41 +- src/modules/viaggi/types/viaggi.types.ts | 106 +- src/router/routesViaggi.ts | 325 ++-- src/statics/lang/it.js | 6 +- src/store/globalStore.ts | 12 +- 40 files changed, 8129 insertions(+), 1395 deletions(-) create mode 100644 src/modules/viaggi/components/ride/CommunityFilters.vue create mode 100644 src/modules/viaggi/components/ride/CommunityRideCard.vue create mode 100644 src/modules/viaggi/components/ride/RideCalendar.vue create mode 100644 src/modules/viaggi/composables/useCommunityrides.ts create mode 100644 src/modules/viaggi/composables/useNotifications.ts create mode 100644 src/modules/viaggi/composables/useRideNotifications.ts create mode 100644 src/modules/viaggi/composables/useSavedFilters.ts create mode 100644 src/modules/viaggi/pages/CommunityRidesPage.vue create mode 100644 src/modules/viaggi/pages/NotificationsPage.vue diff --git a/quasar.config.ts b/quasar.config.ts index 1c2dd207..c9cc8c07 100644 --- a/quasar.config.ts +++ b/quasar.config.ts @@ -158,7 +158,10 @@ export default defineConfig((ctx) => { }, framework: { - config: {}, + config: { + notify: { position: 'top' }, + loading: { delay: 200 }, + }, components: [ 'QLayout', 'QDrawer', diff --git a/src-pwa/custom-service-worker.js b/src-pwa/custom-service-worker.js index 293fbebb..5c27b7f8 100755 --- a/src-pwa/custom-service-worker.js +++ b/src-pwa/custom-service-worker.js @@ -117,7 +117,7 @@ self.addEventListener('activate', (event) => { ); }) ); - self.clients.claim(); // SERVE? OPPURE NO ? + self.clients.claim(); }); const USASYNC = false; @@ -263,297 +263,572 @@ if (workbox) { networkTimeoutSeconds: 10, // timeout rapido plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), - new ExpirationPlugin({ maxEntries: 20, maxAgeSeconds: 24 * 60 * 60 }), // 1 giorno + new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 24 * 60 * 60 }), // 1 giorno ], }) ); - // API calls - NetworkFirst con fallback cache e timeout veloce + // API calls - NetworkFirst con timeout breve registerRoute( ({ url }) => url.hostname === API_DOMAIN, new NetworkFirst({ cacheName: `${CACHE_PREFIX}-api-cache-${CACHE_VERSION}`, - networkTimeoutSeconds: 10, - fetchOptions: { credentials: 'include' }, + networkTimeoutSeconds: 5, plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), - new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 5 * 60 }), // 5 minuti + new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 5 * 60 }), // 5 minuti ], }) ); - - registerRoute(new RegExp('/admin/'), new NetworkOnly()); - - function generateUUID() { - return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => - (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16) - ); - } - - const syncStore = {}; - self.addEventListener('message', (event) => { - if ( - event.data && - (event.data.type === 'SKIP_WAITING' || event.data.action === 'skipWaiting') - ) { - self.skipWaiting(); - // Opzionale: rispondi al client - if (event.ports && event.ports[0]) { - event.ports[0].postMessage({ success: true }); - } - } - if (event.data.type === 'sync') { - console.log('addEventListener - message'); - const id = generateUUID(); - syncStore[id] = event.data; - self.registration.sync.register(id); - } - console.log(event.data); - }); - - // Funzione per gestire richieste API - async function handleApiRequest(request) { - try { - const response = await fetch(request); - - // Se la risposta non è valida, restituisci un errore personalizzato - if (!response.ok) { - console.warn('[SW] API Response Error:', response.status, response.statusText); - return new Response( - JSON.stringify({ - error: 'API error', - message: `❌ Invalid response from API: ${response.status} ${response.statusText}`, - }), - { status: response.status, headers: { 'Content-Type': 'application/json' } } - ); - } - - return response; - } catch (error) { - console.error('[Service Worker] API request error ❌:', error); - - // Restituisci una risposta di errore personalizzata - return new Response( - JSON.stringify({ - error: 'Network error', - message: '❌ Unable to fetch from API: ' + error.message, - }), - { status: 503, headers: { 'Content-Type': 'application/json' } } - ); - } - } - - // Funzione per effettuare una richiesta di rete e memorizzare nella cache - async function fetchAndCache(request) { - const cache = await caches.open(DYNAMIC_CACHE); - try { - const response = await fetch(request); - - // Clona e salva la risposta nella cache solo se valida - if (response.ok) { - const responseClone = response.clone(); - cache.put(request, responseClone); - } - - return response; - } catch (error) { - console.error('[SW] Fetch and cache error ❌:', error); - throw error; - } - } - - // Strategia di caching: stale-while-revalidate - async function cacheWithStaleWhileRevalidate(request, event) { - const cache = await caches.open(DYNAMIC_CACHE); - - // Prova a recuperare la risorsa dalla cache - const cachedResponse = await cache.match(request); - if (cachedResponse) { - // Aggiorna in background mentre restituisci la risposta in cache - event.waitUntil( - fetchAndCache(request).catch((error) => { - console.error('[SW] Background fetch and cache error ❌:', error); - }) - ); - return cachedResponse; - } - - // Se non è in cache, fai la richiesta di rete - try { - return await fetchAndCache(request); - } catch (error) { - console.error('[SW] Cache miss and network error ❌:', error); - - // Restituisci una risposta di fallback personalizzata - return new Response( - JSON.stringify({ - error: 'Network error', - message: 'Unable to fetch resource from network or cache.', - }), - { status: 503, headers: { 'Content-Type': 'application/json' } } - ); - } - } - - // Listener per gestire tutte le richieste - /*self.addEventListener('fetch', (event) => { - const { request } = event; - const url = new URL(request.url); - try { - // Ignora richieste non gestibili - if (request.method !== 'GET' || url.protocol !== 'https:') { - return; - } - - // Ignora richieste per file di sviluppo (es. /src/) - if (url.pathname.startsWith('/src/') || url.search.includes('vue&type')) { - return; - } - - // Gestione richieste API - if (url.hostname === API_DOMAIN) { - if (debug) console.log("E' una RICHIESTA API!"); - event.respondWith(handleApiRequest(request)); - return; - } - - // Gestione risorse statiche e altre richieste - if (debug) console.log("E' una RICHIESTA statica..."); - event.respondWith(cacheWithStaleWhileRevalidate(request, event)); - } catch (error) { - console.error('[Service Worker] Fetch error ❌:', error); - } - }); - */ - - // Gestione degli errori non catturati - self.addEventListener('unhandledrejection', (event) => { - console.error('[Service Worker] Unhandled rejection ❌:', event.reason); - }); - - // Gestione degli errori globali - self.addEventListener('error', (event) => { - console.error('[Service Worker] Global error ❌:', event.error); - }); - - // Funzione di utilità per il logging (decommentare se necessario) - // function logFetchDetails(request) { - // console.log('[Service Worker] Fetching:', request.url); - // console.log('Cache mode:', request.cache); - // console.log('Request mode:', request.mode); - // } - - self.addEventListener('sync', (event) => { - console.log('[Service Worker V5] Background syncing', event); - - let mystrparam = event.tag; - let multiparams = mystrparam.split('|'); - if (multiparams && multiparams.length > 3) { - let [cmd, table, method, token, refreshToken, browser_random] = multiparams; - - if (cmd === 'sync-todos') { - console.log('[Service Worker] Syncing', cmd, table, method); - - const headers = new Headers(); - headers.append('content-Type', 'application/json'); - headers.append('Accept', 'application/json'); - headers.append('x-auth', token); - headers.append('x-refrtok', refreshToken); - headers.append('x-browser-random', browser_random); - - event.waitUntil( - readAllData(table).then((alldata) => { - const myrecs = [...alldata]; - let errorfromserver = false; - - if (myrecs) { - let promiseChain = Promise.resolve(); - - for (let rec of myrecs) { - //TODO: Risistemare con calma... per ora non penso venga usato... - let link = cfgenv.serverweb + '/todos'; - if (method !== 'POST') link += '/' + rec._id; - - promiseChain = promiseChain.then(() => - fetch(link, { - method: method, - headers: headers, - cache: 'no-cache', - mode: 'cors', - body: JSON.stringify(rec), - }) - .then(() => { - deleteItemFromData(table, rec._id); - deleteItemFromData('swmsg', mystrparam); - }) - .catch((err) => { - if (err.message === 'Failed to fetch') { - errorfromserver = true; - } - }) - ); - } - - return promiseChain.then(() => { - const mystate = !errorfromserver ? 'online' : 'offline'; - writeData('config', { _id: 2, stateconn: mystate }); - }); - } - }) - ); - } - } - }); - - // Notifications - self.addEventListener('notificationclick', (event) => { - const { notification } = event; - const { action } = event; - - if (action === 'confirm') { - notification.close(); - } else { - event.waitUntil( - self.clients.matchAll().then((clis) => { - const client = clis.find((c) => c.visibilityState === 'visible'); - if (client) { - client.navigate(notification.data.url); - client.focus(); - } else { - self.clients.openWindow(notification.data.url); - } - notification.close(); - }) - ); - } - }); - - self.addEventListener('notificationclose', (event) => { - console.log('Notification was closed', event); - }); - - self.addEventListener('push', (event) => { - console.log('Push Notification received', event); - let data = event.data - ? event.data.json() - : { title: 'New!', content: 'Something new happened!', url: '/' }; - - const options = { - body: data.content, - icon: data.icon ? data.icon : '/images/android-chrome-192x192.png', - badge: data.badge ? data.badge : '/images/badge-96x96.png', - data: { url: data.url }, - tag: data.tag, - }; - - event.waitUntil(self.registration.showNotification(data.title, options)); - - const myid = data.id || '0'; - self.registration.sync.register(myid); - writeData('notifications', { _id: myid, tag: options.tag }); - }); -} else { - console.warn('Workbox could not be loaded.'); } -console.log('***** FINE CUSTOM-SERVICE-WORKER.JS ***** '); +// Generate UUID per sync +function generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0, + v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +let syncStore = {}; + +// Message event handler +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + // Opzionale: rispondi al client + if (event.ports && event.ports[0]) { + event.ports[0].postMessage({ success: true }); + } + } + if (event.data.type === 'sync') { + console.log('addEventListener - message'); + const id = generateUUID(); + syncStore[id] = event.data; + self.registration.sync.register(id); + } + console.log(event.data); +}); + +// Funzione per gestire richieste API +async function handleApiRequest(request) { + try { + const response = await fetch(request); + + // Se la risposta non è valida, restituisci un errore personalizzato + if (!response.ok) { + console.warn('[SW] API Response Error:', response.status, response.statusText); + return new Response( + JSON.stringify({ + error: 'API error', + message: `❌ Invalid response from API: ${response.status} ${response.statusText}`, + }), + { status: response.status, headers: { 'Content-Type': 'application/json' } } + ); + } + + return response; + } catch (error) { + console.error('[Service Worker] API request error ❌:', error); + + // Restituisci una risposta di errore personalizzata + return new Response( + JSON.stringify({ + error: 'Network error', + message: '❌ Unable to fetch from API: ' + error.message, + }), + { status: 503, headers: { 'Content-Type': 'application/json' } } + ); + } +} + +// Funzione per effettuare una richiesta di rete e memorizzare nella cache +async function fetchAndCache(request) { + const cache = await caches.open(DYNAMIC_CACHE); + try { + const response = await fetch(request); + + // Clona e salva la risposta nella cache solo se valida + if (response.ok) { + const responseClone = response.clone(); + cache.put(request, responseClone); + } + + return response; + } catch (error) { + console.error('[SW] Fetch and cache error ❌:', error); + throw error; + } +} + +// Strategia di caching: stale-while-revalidate +async function cacheWithStaleWhileRevalidate(request, event) { + const cache = await caches.open(DYNAMIC_CACHE); + + // Prova a recuperare la risorsa dalla cache + const cachedResponse = await cache.match(request); + if (cachedResponse) { + // Aggiorna in background mentre restituisci la risposta in cache + event.waitUntil( + fetchAndCache(request).catch((error) => { + console.error('[SW] Background fetch and cache error ❌:', error); + }) + ); + return cachedResponse; + } + + // Se non è in cache, fai la richiesta di rete + try { + return await fetchAndCache(request); + } catch (error) { + console.error('[SW] Cache miss and network error ❌:', error); + + // Restituisci una risposta di fallback personalizzata + return new Response( + JSON.stringify({ + error: 'Network error', + message: 'Unable to fetch resource from network or cache.', + }), + { status: 503, headers: { 'Content-Type': 'application/json' } } + ); + } +} + +// Listener per gestire tutte le richieste +/*self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + try { + // Ignora richieste non gestibili + if (request.method !== 'GET' || url.protocol !== 'https:') { + return; + } + + // Ignora richieste per file di sviluppo (es. /src/) + if (url.pathname.startsWith('/src/') || url.search.includes('vue&type')) { + return; + } + + // Gestione richieste API + if (url.hostname === API_DOMAIN) { + if (debug) console.log("E' una RICHIESTA API!"); + event.respondWith(handleApiRequest(request)); + return; + } + + // Gestione risorse statiche e altre richieste + if (debug) console.log("E' una RICHIESTA statica..."); + event.respondWith(cacheWithStaleWhileRevalidate(request, event)); + } catch (error) { + console.error('[Service Worker] Fetch error ❌:', error); + } +}); +*/ + +// Gestione degli errori non catturati +self.addEventListener('unhandledrejection', (event) => { + console.error('[Service Worker] Unhandled rejection ❌:', event.reason); +}); + +// Gestione degli errori globali +self.addEventListener('error', (event) => { + console.error('[Service Worker] Global error ❌:', event.error); +}); + +// Background Sync event +self.addEventListener('sync', (event) => { + console.log('[Service Worker V5] Background syncing', event); + + let mystrparam = event.tag; + let multiparams = mystrparam.split('|'); + if (multiparams && multiparams.length > 3) { + let [cmd, table, method, token, refreshToken, browser_random] = multiparams; + + if (cmd === 'sync-todos') { + console.log('[Service Worker] Syncing', cmd, table, method); + + const headers = new Headers(); + headers.append('content-Type', 'application/json'); + headers.append('Accept', 'application/json'); + headers.append('x-auth', token); + headers.append('x-refrtok', refreshToken); + headers.append('x-browser-random', browser_random); + + event.waitUntil( + readAllData(table).then((alldata) => { + const myrecs = [...alldata]; + let errorfromserver = false; + + if (myrecs) { + let promiseChain = Promise.resolve(); + + for (let rec of myrecs) { + //TODO: Risistemare con calma... per ora non penso venga usato... + let link = cfgenv.serverweb + '/todos'; + if (method !== 'POST') link += '/' + rec._id; + + promiseChain = promiseChain.then(() => + fetch(link, { + method: method, + headers: headers, + cache: 'no-cache', + mode: 'cors', + body: JSON.stringify(rec), + }) + .then(() => { + deleteItemFromData(table, rec._id); + deleteItemFromData('swmsg', mystrparam); + }) + .catch((err) => { + if (err.message === 'Failed to fetch') { + errorfromserver = true; + } + }) + ); + } + + return promiseChain.then(() => { + const mystate = !errorfromserver ? 'online' : 'offline'; + writeData('config', { _id: 2, stateconn: mystate }); + }); + } + }) + ); + } + } + + // Sync per notifiche (se necessario) + if (event.tag === 'sync-notifications') { + event.waitUntil( + Promise.resolve().then(() => { + console.log('[SW] Syncing notifications'); + }) + ); + } +}); + +// ======================================== +// 🔔 PUSH NOTIFICATIONS - BACKWARD COMPATIBLE VERSION +// ======================================== + +// Push event - BACKWARD COMPATIBLE con formato vecchio E nuovo +self.addEventListener('push', (event) => { + console.log('[Service Worker] 🔔 Push notification received:', event); + + // Default data - supporta ENTRAMBI i formati (vecchio e nuovo) + let data = { + title: 'New!', + body: 'Something new happened!', + content: 'Something new happened!', // VECCHIO formato + icon: '/images/android-chrome-192x192.png', // VECCHIO path + badge: '/images/badge-96x96.png', // VECCHIO path + tag: 'default', + url: '/', + requireInteraction: false, + notificationType: null, + rideId: null, + fromUser: null, + actions: [] + }; + + // Parse del payload se presente + if (event.data) { + try { + const payload = event.data.json(); + data = { ...data, ...payload }; + + // BACKWARD COMPATIBILITY: Se c'è 'content' ma non 'body', usa 'content' + if (payload.content && !payload.body) { + data.body = payload.content; + } + // Se c'è 'body' ma non 'content', sincronizza + if (payload.body && !payload.content) { + data.content = payload.body; + } + + console.log('[SW] 📦 Parsed notification payload:', data); + } catch (e) { + console.warn('[SW] ⚠️ Failed to parse push payload as JSON, using text'); + const textData = event.data.text(); + data.body = textData; + data.content = textData; + } + } + + // Configura le actions SOLO se c'è un notificationType (nuovo formato) + if (data.notificationType && (!data.actions || data.actions.length === 0)) { + data.actions = getNotificationActions(data.notificationType, data.rideId); + } + + // Opzioni della notifica - supporta ENTRAMBI i path delle icone + const options = { + body: data.body || data.content, // Fallback a 'content' se 'body' non c'è + icon: data.icon, + badge: data.badge, + tag: data.tag, + requireInteraction: data.requireInteraction || false, + vibrate: data.vibrate || [200, 100, 200], + data: { + dateOfArrival: Date.now(), + url: data.url || '/', + rideId: data.rideId, + notificationType: data.notificationType, + fromUser: data.fromUser, + // BACKWARD COMPATIBILITY: mantieni anche il formato vecchio + legacyFormat: !data.notificationType // true se è formato vecchio + }, + actions: data.actions || [] + }; + + // Mostra la notifica + event.waitUntil( + self.registration.showNotification(data.title, options).then(() => { + console.log('[SW] ✅ Notification displayed successfully'); + + // Salva la notifica in IndexedDB - BACKWARD COMPATIBLE + const myid = data.id || generateUUID(); + self.registration.sync.register(myid); + + // Formato compatibile: se è vecchio formato usa solo _id e tag + const notificationData = options.data.legacyFormat + ? { _id: myid, tag: options.tag } + : { + _id: myid, + tag: options.tag, + type: data.notificationType, + timestamp: Date.now() + }; + + writeData('notifications', notificationData).catch(err => { + console.error('[SW] Failed to save notification to IndexedDB:', err); + }); + }) + ); +}); + +// Funzione helper per generare actions (SOLO per nuovo formato) +function getNotificationActions(notificationType, rideId) { + const actions = []; + + switch (notificationType) { + case 'newRideRequest': + actions.push( + { action: 'view-requests', title: '👀 Visualizza richieste' }, + { action: 'dismiss', title: '❌ Ignora' } + ); + break; + + case 'requestAccepted': + actions.push( + { action: 'view-ride', title: '🚗 Vedi viaggio' }, + { action: 'message', title: '💬 Messaggio' } + ); + break; + + case 'requestRejected': + actions.push( + { action: 'search-rides', title: '🔍 Cerca altri viaggi' } + ); + break; + + case 'newMessage': + actions.push( + { action: 'open-chat', title: '💬 Apri chat' }, + { action: 'dismiss', title: '✓ OK' } + ); + break; + + case 'rideReminder': + actions.push( + { action: 'view-ride', title: '📍 Vedi dettagli' }, + { action: 'dismiss', title: '✓ OK' } + ); + break; + + case 'rideCancelled': + actions.push( + { action: 'search-rides', title: '🔍 Cerca alternativa' } + ); + break; + + case 'feedbackRequest': + actions.push( + { action: 'leave-feedback', title: '⭐ Lascia feedback' }, + { action: 'dismiss', title: 'Dopo' } + ); + break; + + case 'newFeedbackReceived': + actions.push( + { action: 'view-feedback', title: '⭐ Vedi feedback' } + ); + break; + + default: + actions.push( + { action: 'open-app', title: '📱 Apri app' } + ); + } + + return actions; +} + +// Notification click event - BACKWARD COMPATIBLE +self.addEventListener('notificationclick', (event) => { + console.log('[Service Worker] 🖱️ Notification clicked:', event); + console.log('[SW] Action:', event.action); + console.log('[SW] Notification data:', event.notification.data); + + // Chiudi la notifica + event.notification.close(); + + // Estrai i dati dalla notifica + const notificationData = event.notification.data || {}; + const isLegacyFormat = notificationData.legacyFormat || false; + + let finalUrl; + + // BACKWARD COMPATIBILITY: Se è formato vecchio, usa solo notification.data.url + if (isLegacyFormat || !notificationData.notificationType) { + console.log('[SW] 📜 Using legacy notification format'); + finalUrl = notificationData.url || '/'; + } else { + // NUOVO formato: usa il routing intelligente + console.log('[SW] 🆕 Using new notification format with smart routing'); + const rideId = notificationData.rideId; + const notificationType = notificationData.notificationType; + const action = event.action; + + finalUrl = determineUrlFromNotification(action, notificationType, rideId); + } + + console.log('[SW] 🎯 Navigating to:', finalUrl); + + // Se finalUrl è null (action 'dismiss'), non fare nulla + if (!finalUrl) { + console.log('[SW] Action dismissed, no navigation'); + return; + } + + // Apri l'URL appropriato + event.waitUntil( + clients.matchAll({ type: 'window', includeUncontrolled: true }) + .then((clientList) => { + console.log('[SW] Found', clientList.length, 'open windows'); + + // Cerca una finestra già aperta con l'URL desiderato + for (const client of clientList) { + if (client.url.includes(finalUrl.split('?')[0]) && 'focus' in client) { + console.log('[SW] ✅ Focusing existing window'); + return client.focus(); + } + } + + // Cerca una finestra dell'app aperta e naviga lì + for (const client of clientList) { + if (client.url.includes(self.location.origin) && 'navigate' in client) { + console.log('[SW] ✅ Navigating existing window to:', finalUrl); + return client.navigate(finalUrl).then(client => client.focus()); + } + } + + // Altrimenti apri una nuova finestra + if (clients.openWindow) { + console.log('[SW] 🆕 Opening new window'); + return clients.openWindow(finalUrl); + } + }) + .catch((error) => { + console.error('[SW] ❌ Error handling notification click:', error); + }) + ); +}); + +// Funzione per determinare l'URL (SOLO per nuovo formato) +function determineUrlFromNotification(action, notificationType, rideId) { + // Gestisci prima le actions specifiche + if (action) { + switch (action) { + case 'view-requests': + return '/trasporti/miei-viaggi?tab=requests'; + + case 'view-ride': + return rideId ? `/trasporti/viaggio/${rideId}` : '/trasporti/miei-viaggi'; + + case 'message': + case 'open-chat': + return rideId ? `/trasporti/chat?ride=${rideId}` : '/trasporti/chat'; + + case 'search-rides': + return '/trasporti?view=search'; + + case 'leave-feedback': + return rideId ? `/trasporti/feedback/${rideId}` : '/trasporti/feedback'; + + case 'view-feedback': + return '/trasporti/miei-feedback'; + + case 'open-app': + return '/trasporti'; + + case 'dismiss': + return null; // Non fare nulla + + default: + break; + } + } + + // Se non c'è action, usa il tipo di notifica + if (notificationType) { + switch (notificationType) { + case 'newRideRequest': + return '/trasporti/miei-viaggi?tab=requests'; + + case 'requestAccepted': + case 'requestRejected': + case 'rideReminder': + return rideId ? `/trasporti/viaggio/${rideId}` : '/trasporti/miei-viaggi'; + + case 'newMessage': + return '/trasporti/chat'; + + case 'rideCancelled': + return '/trasporti/miei-viaggi?tab=cancelled'; + + case 'feedbackRequest': + return rideId ? `/trasporti/feedback/${rideId}` : '/trasporti/feedback'; + + case 'newFeedbackReceived': + return '/trasporti/miei-feedback'; + + default: + return '/trasporti'; + } + } + + // Fallback + return '/trasporti'; +} + +// Notification close event - tracking opzionale +self.addEventListener('notificationclose', (event) => { + console.log('[Service Worker] 🔕 Notification closed:', event); + + const notificationData = event.notification.data || {}; + + // Solo per nuovo formato, traccia le dismissioni + if (!notificationData.legacyFormat && notificationData.notificationType) { + writeData('notification-dismissals', { + _id: generateUUID(), + type: notificationData.notificationType, + timestamp: Date.now(), + tag: event.notification.tag + }).catch(err => { + console.error('[SW] Failed to track notification dismissal:', err); + }); + } +}); + +console.log('***** 🚀 CUSTOM-SERVICE-WORKER.JS - BACKWARD COMPATIBLE VERSION ***** '); diff --git a/src/components/CMyElem/CMyElem.ts b/src/components/CMyElem/CMyElem.ts index 829577fb..25a30e3c 100755 --- a/src/components/CMyElem/CMyElem.ts +++ b/src/components/CMyElem/CMyElem.ts @@ -34,7 +34,7 @@ import { CMyActivities } from '@/components/CMyActivities'; import { CECommerce } from '@/components/CECommerce'; import { EventPosterGenerator } from '@/components/EventPosterGenerator'; import { RideWidget } from 'app/src/modules/viaggi/components/widgets/RideWidget'; -import { CheckEmail } from '@/components/CheckEmail'; +import { CheckEmail } from '@/components/checkemail'; import { HomeRiso } from '@/components/HomeRiso'; import mycircuits from '@/views/user/mycircuits/mycircuits.vue'; import PageRis from '@/components/pageris/pageris.vue'; diff --git a/src/components/CMyElem/CMyElem.vue b/src/components/CMyElem/CMyElem.vue index 7439a8e5..17e811ba 100755 --- a/src/components/CMyElem/CMyElem.vue +++ b/src/components/CMyElem/CMyElem.vue @@ -144,7 +144,6 @@ v-else-if="myel.type === shared_consts.ELEMTYPE.CREA_VOLANTINO" class="myElemBase" > - ˚
- ˚
{ - const stored = localStorage.getItem(STORAGE_KEY); + const stored = localStorage.getItem(STOR_LAST_EMAIL_SENT); return stored ? parseInt(stored, 10) : 0; }; const setLastSentTime = () => { - localStorage.setItem(STORAGE_KEY, Date.now().toString()); + localStorage.setItem(STOR_LAST_EMAIL_SENT, Date.now().toString()); }; const calculateTimeLeft = (): number => { diff --git a/src/model/GlobalStore.ts b/src/model/GlobalStore.ts index 1a0c344c..466606b1 100755 --- a/src/model/GlobalStore.ts +++ b/src/model/GlobalStore.ts @@ -622,6 +622,7 @@ export interface IListRoutes { inmenu?: boolean solotitle?: boolean infooter?: boolean + badge?: any submenu?: boolean noroute?: boolean onlyAdmin?: boolean diff --git a/src/modules/viaggi/components/ride/CommunityFilters.vue b/src/modules/viaggi/components/ride/CommunityFilters.vue new file mode 100644 index 00000000..4ea0cd07 --- /dev/null +++ b/src/modules/viaggi/components/ride/CommunityFilters.vue @@ -0,0 +1,561 @@ + + + + + + diff --git a/src/modules/viaggi/components/ride/CommunityRideCard.vue b/src/modules/viaggi/components/ride/CommunityRideCard.vue new file mode 100644 index 00000000..688aec08 --- /dev/null +++ b/src/modules/viaggi/components/ride/CommunityRideCard.vue @@ -0,0 +1,655 @@ + + + + + + diff --git a/src/modules/viaggi/components/ride/ContribTypeSelector.ts b/src/modules/viaggi/components/ride/ContribTypeSelector.ts index 341c6dbd..00df29f2 100644 --- a/src/modules/viaggi/components/ride/ContribTypeSelector.ts +++ b/src/modules/viaggi/components/ride/ContribTypeSelector.ts @@ -154,7 +154,7 @@ export default defineComponent({ const requiresPrice = (contribType: ContribType): boolean => { const label = contribType.label.toLowerCase(); - const noPriceTypes = ['dono', 'baratto', 'scambio lavoro']; + const noPriceTypes = ['dono', 'Offerta Libera', 'baratto', 'scambio lavoro']; return !noPriceTypes.includes(label); }; diff --git a/src/modules/viaggi/components/ride/MyRideCard.vue b/src/modules/viaggi/components/ride/MyRideCard.vue index 60573d5b..17ee4689 100644 --- a/src/modules/viaggi/components/ride/MyRideCard.vue +++ b/src/modules/viaggi/components/ride/MyRideCard.vue @@ -74,7 +74,7 @@ icon="notifications_active" size="sm" unelevated - @click.stop="$emit('manage-requests')" + @click.stop="$emit('manage-requests', ride)" />
@@ -87,7 +87,7 @@ icon="star" size="sm" unelevated - @click.stop="$emit('leave-feedback')" + @click.stop="$emit('leave-feedback', ride)" />
@@ -102,7 +102,7 @@ color="primary" label="Modifica" icon="edit" - @click.stop="$emit('edit')" + @click.stop="$emit('edit', ride._id)" /> @@ -162,7 +162,7 @@ export default defineComponent({ const { formatRideDate, getStatusColor, getStatusLabel } = useRides(); const formattedDate = computed(() => { - const date = new Date(props.ride.dateTime); + const date = new Date(props.ride.departureDate); return date.toLocaleDateString('it-IT', { weekday: 'short', day: 'numeric', @@ -171,7 +171,7 @@ export default defineComponent({ }); const formattedTime = computed(() => { - const date = new Date(props.ride.dateTime); + const date = new Date(props.ride.departureDate); return date.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' diff --git a/src/modules/viaggi/components/ride/RecurrenceSelector.ts b/src/modules/viaggi/components/ride/RecurrenceSelector.ts index 7cd78c27..7a721fbb 100644 --- a/src/modules/viaggi/components/ride/RecurrenceSelector.ts +++ b/src/modules/viaggi/components/ride/RecurrenceSelector.ts @@ -8,8 +8,8 @@ export default defineComponent({ props: { modelValue: { type: Object as PropType, - default: () => ({ type: 'once' }) - } + default: () => ({ type: 'once' }), + }, }, emits: ['update:modelValue'], @@ -22,65 +22,95 @@ export default defineComponent({ customDates: [], startDate: '', endDate: '', - excludedDates: [] + excludedDates: [], }); const selectedDates = ref([]); const excludedDates = ref([]); // Opzioni - const recurrenceTypes = RECURRENCE_TYPE_OPTIONS.map(opt => ({ + const recurrenceTypes = RECURRENCE_TYPE_OPTIONS.map((opt) => ({ label: opt.label, value: opt.value, - icon: opt.icon + icon: opt.icon, })); const daysOfWeek = DAYS_OF_WEEK; + const formattedStartDate = computed({ + get: () => { + if (!localRecurrence.startDate) return ''; + // Estrae solo YYYY-MM-DD dalla stringa ISO + return localRecurrence.startDate.substring(0, 10); + }, + set: (val: string) => { + localRecurrence.startDate = val ? `${val}T00:00:00.000Z` : ''; + }, + }); + + const formattedEndDate = computed({ + get: () => { + if (!localRecurrence.endDate) return ''; + // Estrae solo YYYY-MM-DD dalla stringa ISO + return localRecurrence.endDate.substring(0, 10); + }, + set: (val: string) => { + localRecurrence.endDate = val ? `${val}T00:00:00.000Z` : ''; + }, + }); + // Watch per sincronizzare con modelValue - watch(() => props.modelValue, (newVal) => { - if (newVal) { - Object.assign(localRecurrence, newVal); + watch( + () => props.modelValue, + (newVal) => { + if (newVal) { + Object.assign(localRecurrence, newVal); - if (newVal.customDates) { - selectedDates.value = newVal.customDates.map(d => - typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0] - ); - } + if (newVal.customDates) { + selectedDates.value = newVal.customDates.map((d) => + typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0] + ); + } - if (newVal.excludedDates) { - excludedDates.value = newVal.excludedDates.map(d => - typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0] - ); + if (newVal.excludedDates) { + excludedDates.value = newVal.excludedDates.map((d) => + typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0] + ); + } } - } - }, { immediate: true, deep: true }); + }, + { immediate: true, deep: true } + ); // Watch per emettere update - watch([localRecurrence, selectedDates, excludedDates], () => { - const result: Recurrence = { - type: localRecurrence.type - }; + watch( + [localRecurrence, selectedDates, excludedDates], + () => { + const result: Recurrence = { + type: localRecurrence.type, + }; - if (localRecurrence.type !== 'once') { - result.startDate = localRecurrence.startDate; - result.endDate = localRecurrence.endDate; + if (localRecurrence.type !== 'once') { + result.startDate = localRecurrence.startDate; + result.endDate = localRecurrence.endDate; - if (excludedDates.value.length > 0) { - result.excludedDates = excludedDates.value; + if (excludedDates.value.length > 0) { + result.excludedDates = excludedDates.value; + } } - } - if (localRecurrence.type === 'weekly' || localRecurrence.type === 'custom_days') { - result.daysOfWeek = localRecurrence.daysOfWeek; - } + if (localRecurrence.type === 'weekly' || localRecurrence.type === 'custom_days') { + result.daysOfWeek = localRecurrence.daysOfWeek; + } - if (localRecurrence.type === 'custom_dates') { - result.customDates = selectedDates.value; - } + if (localRecurrence.type === 'custom_dates') { + result.customDates = selectedDates.value; + } - emit('update:modelValue', result); - }, { deep: true }); + emit('update:modelValue', result); + }, + { deep: true } + ); // Methods const isDaySelected = (day: number): boolean => { @@ -116,7 +146,7 @@ export default defineComponent({ return date.toLocaleDateString('it-IT', { weekday: 'short', day: 'numeric', - month: 'short' + month: 'short', }); }; @@ -149,7 +179,7 @@ export default defineComponent({ return 'Seleziona i giorni della settimana'; } const weeklyDays = localRecurrence.daysOfWeek - .map(d => daysOfWeek.find(day => day.value === d)?.label) + .map((d) => daysOfWeek.find((day) => day.value === d)?.label) .join(', '); return `Ogni settimana: ${weeklyDays}`; @@ -158,7 +188,7 @@ export default defineComponent({ return 'Seleziona i giorni della settimana'; } const customDays = localRecurrence.daysOfWeek - .map(d => daysOfWeek.find(day => day.value === d)?.label) + .map((d) => daysOfWeek.find((day) => day.value === d)?.label) .join(', '); return `Giorni selezionati: ${customDays}`; @@ -193,7 +223,9 @@ export default defineComponent({ removeExcludedDate, formatDate, dateOptions, - exclusionDateOptions + exclusionDateOptions, + formattedStartDate, + formattedEndDate, }; - } + }, }); diff --git a/src/modules/viaggi/components/ride/RecurrenceSelector.vue b/src/modules/viaggi/components/ride/RecurrenceSelector.vue index cf932d78..3e11389d 100644 --- a/src/modules/viaggi/components/ride/RecurrenceSelector.vue +++ b/src/modules/viaggi/components/ride/RecurrenceSelector.vue @@ -1,7 +1,11 @@