- Aggiornamento Viaggi

This commit is contained in:
Surya Paolo
2025-12-30 11:36:37 +01:00
parent cd123ac363
commit 169f4d6baf
40 changed files with 8129 additions and 1395 deletions

View File

@@ -158,7 +158,10 @@ export default defineConfig((ctx) => {
}, },
framework: { framework: {
config: {}, config: {
notify: { position: 'top' },
loading: { delay: 200 },
},
components: [ components: [
'QLayout', 'QLayout',
'QDrawer', 'QDrawer',

View File

@@ -117,7 +117,7 @@ self.addEventListener('activate', (event) => {
); );
}) })
); );
self.clients.claim(); // SERVE? OPPURE NO ? self.clients.claim();
}); });
const USASYNC = false; const USASYNC = false;
@@ -263,297 +263,572 @@ if (workbox) {
networkTimeoutSeconds: 10, // timeout rapido networkTimeoutSeconds: 10, // timeout rapido
plugins: [ plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }), 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( registerRoute(
({ url }) => url.hostname === API_DOMAIN, ({ url }) => url.hostname === API_DOMAIN,
new NetworkFirst({ new NetworkFirst({
cacheName: `${CACHE_PREFIX}-api-cache-${CACHE_VERSION}`, cacheName: `${CACHE_PREFIX}-api-cache-${CACHE_VERSION}`,
networkTimeoutSeconds: 10, networkTimeoutSeconds: 5,
fetchOptions: { credentials: 'include' },
plugins: [ plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }), 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 ***** ');

View File

@@ -34,7 +34,7 @@ import { CMyActivities } from '@/components/CMyActivities';
import { CECommerce } from '@/components/CECommerce'; import { CECommerce } from '@/components/CECommerce';
import { EventPosterGenerator } from '@/components/EventPosterGenerator'; import { EventPosterGenerator } from '@/components/EventPosterGenerator';
import { RideWidget } from 'app/src/modules/viaggi/components/widgets/RideWidget'; 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 { HomeRiso } from '@/components/HomeRiso';
import mycircuits from '@/views/user/mycircuits/mycircuits.vue'; import mycircuits from '@/views/user/mycircuits/mycircuits.vue';
import PageRis from '@/components/pageris/pageris.vue'; import PageRis from '@/components/pageris/pageris.vue';

View File

@@ -144,7 +144,6 @@
v-else-if="myel.type === shared_consts.ELEMTYPE.CREA_VOLANTINO" v-else-if="myel.type === shared_consts.ELEMTYPE.CREA_VOLANTINO"
class="myElemBase" class="myElemBase"
> >
˚
<div <div
v-if="editOn" v-if="editOn"
class="elemEdit" class="elemEdit"
@@ -158,7 +157,6 @@
v-else-if="myel.type === shared_consts.ELEMTYPE.VIAGGI_WIDGET" v-else-if="myel.type === shared_consts.ELEMTYPE.VIAGGI_WIDGET"
class="myElemBase" class="myElemBase"
> >
˚
<div <div
v-if="editOn" v-if="editOn"
class="elemEdit" class="elemEdit"

View File

@@ -9,7 +9,7 @@ import { Api } from '@api';
const RESEND_COOLDOWN_MINUTES = 1; const RESEND_COOLDOWN_MINUTES = 1;
const RESEND_COOLDOWN_MS = RESEND_COOLDOWN_MINUTES * 60 * 1000; const RESEND_COOLDOWN_MS = RESEND_COOLDOWN_MINUTES * 60 * 1000;
const STORAGE_KEY = 'lastVerificationEmailSent'; const STOR_LAST_EMAIL_SENT = 'lastVerificationEmailSent';
export default defineComponent({ export default defineComponent({
name: 'CheckEmail', name: 'CheckEmail',
@@ -38,12 +38,12 @@ export default defineComponent({
// Methods // Methods
const getLastSentTime = (): number => { const getLastSentTime = (): number => {
const stored = localStorage.getItem(STORAGE_KEY); const stored = localStorage.getItem(STOR_LAST_EMAIL_SENT);
return stored ? parseInt(stored, 10) : 0; return stored ? parseInt(stored, 10) : 0;
}; };
const setLastSentTime = () => { const setLastSentTime = () => {
localStorage.setItem(STORAGE_KEY, Date.now().toString()); localStorage.setItem(STOR_LAST_EMAIL_SENT, Date.now().toString());
}; };
const calculateTimeLeft = (): number => { const calculateTimeLeft = (): number => {

View File

@@ -622,6 +622,7 @@ export interface IListRoutes {
inmenu?: boolean inmenu?: boolean
solotitle?: boolean solotitle?: boolean
infooter?: boolean infooter?: boolean
badge?: any
submenu?: boolean submenu?: boolean
noroute?: boolean noroute?: boolean
onlyAdmin?: boolean onlyAdmin?: boolean

View File

@@ -0,0 +1,561 @@
<!-- components/ride/CommunityFilters.vue -->
<template>
<div class="community-filters">
<!-- Quick Filters Chips -->
<div
v-if="hasActiveFilters"
class="community-filters__active"
>
<q-chip
v-for="filter in activeFiltersList"
:key="filter.key"
removable
color="primary"
text-color="white"
size="sm"
@remove="removeFilter(filter.key)"
>
{{ filter.label }}
</q-chip>
<q-btn
flat
dense
label="Cancella tutti"
color="negative"
size="sm"
@click="$emit('clear')"
/>
</div>
<!-- Filter Form -->
<div class="community-filters__form">
<q-input
v-model="localFilters.departure"
label="Partenza"
outlined
dense
clearable
@update:model-value="debouncedEmit"
>
<template v-slot:prepend>
<q-icon
name="trip_origin"
color="positive"
/>
</template>
</q-input>
<!-- Destination -->
<q-input
v-model="localFilters.destination"
label="Destinazione"
outlined
dense
clearable
@update:model-value="debouncedEmit"
>
<template v-slot:prepend>
<q-icon
name="place"
color="negative"
/>
</template>
</q-input>
<!-- Date From -->
<q-input
v-model="localFilters.dateFrom"
label="Data da"
outlined
dense
clearable
@update:model-value="debouncedEmit"
>
<template v-slot:prepend>
<q-icon name="event" />
</template>
<template v-slot:append>
<q-icon
name="event"
class="cursor-pointer"
>
<q-popup-proxy
cover
transition-show="scale"
transition-hide="scale"
>
<q-date
v-model="localFilters.dateFrom"
mask="YYYY-MM-DD"
:options="dateFromOptions"
@update:model-value="debouncedEmit"
>
<div class="row items-center justify-end">
<q-btn
v-close-popup
label="OK"
color="primary"
flat
/>
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<!-- Date To -->
<q-input
v-model="localFilters.dateTo"
label="Data a"
outlined
dense
clearable
@update:model-value="debouncedEmit"
>
<template v-slot:prepend>
<q-icon name="event" />
</template>
<template v-slot:append>
<q-icon
name="event"
class="cursor-pointer"
>
<q-popup-proxy
cover
transition-show="scale"
transition-hide="scale"
>
<q-date
v-model="localFilters.dateTo"
mask="YYYY-MM-DD"
:options="dateToOptions"
@update:model-value="debouncedEmit"
>
<div class="row items-center justify-end">
<q-btn
v-close-popup
label="OK"
color="primary"
flat
/>
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<!-- Max Price -->
<div class="community-filters__slider-wrapper">
<label class="community-filters__slider-label">
Prezzo massimo: {{ localFilters.maxPrice || 100 }}
</label>
<q-slider
v-model="localFilters.maxPrice"
:min="0"
:max="100"
:step="5"
label
color="positive"
@change="debouncedEmit"
/>
</div>
<q-select
v-model="localFilters.vehicleType"
:options="VEHICLE_TYPES"
label="Tipo veicolo"
outlined
dense
clearable
emit-value
map-options
@update:model-value="debouncedEmit"
>
<template v-slot:prepend>
<q-icon name="directions_car" />
</template>
</q-select>
<!-- Min Seats -->
<q-select
v-model="localFilters.minSeats"
:options="seatsOptions"
label="Posti minimi disponibili"
outlined
dense
clearable
emit-value
map-options
@update:model-value="debouncedEmit"
>
<template v-slot:prepend>
<q-icon name="event_seat" />
</template>
</q-select>
<!-- Sort By -->
<q-select
v-model="localFilters.sortBy"
:options="sortOptions"
label="Ordina per"
outlined
dense
emit-value
map-options
@update:model-value="debouncedEmit"
>
<template v-slot:prepend>
<q-icon name="sort" />
</template>
</q-select>
</div>
<!-- Quick Date Filters -->
<div class="community-filters__quick-dates">
<div class="community-filters__quick-dates-label">Filtri rapidi:</div>
<q-btn
outline
dense
label="Oggi"
color="primary"
size="sm"
@click="setToday"
/>
<q-btn
outline
dense
label="Domani"
color="primary"
size="sm"
@click="setTomorrow"
/>
<q-btn
outline
dense
label="Questa settimana"
color="primary"
size="sm"
@click="setThisWeek"
/>
<q-btn
outline
dense
label="Weekend"
color="primary"
size="sm"
@click="setWeekend"
/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, watch, PropType } from 'vue';
import { date as qdate } from 'quasar';
import type { CommunityFilters } from '../../composables/useCommunityRides';
import { VEHICLE_TYPES } from '../../types';
import { useRecentCities } from '../../composables/useRecentCities';
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
export default defineComponent({
name: 'CommunityFilters',
props: {
filters: {
type: Object as PropType<CommunityFilters>,
required: true,
},
},
emits: ['update', 'clear'],
setup(props, { emit }) {
// Local copy of filters for immediate UI updates
const localFilters = ref<CommunityFilters>({ ...props.filters });
const { addRecentSearch, getRecentSearches } = useRecentCities();
// Sync local filters with prop changes
watch(
() => props.filters,
(newFilters) => {
localFilters.value = { ...newFilters };
},
{ deep: true }
);
// Options
const seatsOptions = [
{ label: '1+ posto', value: 1 },
{ label: '2+ posti', value: 2 },
{ label: '3+ posti', value: 3 },
{ label: '4+ posti', value: 4 },
];
const sortOptions = [
{ label: 'Data (prima i più vicini)', value: 'date' },
{ label: 'Prezzo (crescente)', value: 'price_asc' },
{ label: 'Prezzo (decrescente)', value: 'price_desc' },
{ label: 'Posti disponibili', value: 'seats' },
{ label: 'Valutazione conducente', value: 'rating' },
];
// Computed
const hasActiveFilters = computed(() => {
return !!(
localFilters.value.departure ||
localFilters.value.destination ||
localFilters.value.dateFrom ||
localFilters.value.dateTo ||
localFilters.value.maxPrice ||
localFilters.value.minSeats
);
});
const activeFiltersList = computed(() => {
const filters: Array<{ key: string; label: string }> = [];
if (localFilters.value.departure) {
filters.push({
key: 'departure',
label: `Da: ${localFilters.value.departure}`,
});
}
if (localFilters.value.destination) {
filters.push({
key: 'destination',
label: `A: ${localFilters.value.destination}`,
});
}
if (localFilters.value.dateFrom) {
filters.push({
key: 'dateFrom',
label: `Dal: ${qdate.formatDate(localFilters.value.dateFrom, 'DD/MM/YYYY')}`,
});
}
if (localFilters.value.dateTo) {
filters.push({
key: 'dateTo',
label: `Al: ${qdate.formatDate(localFilters.value.dateTo, 'DD/MM/YYYY')}`,
});
}
if (localFilters.value.maxPrice) {
filters.push({
key: 'maxPrice',
label: `Max: €${localFilters.value.maxPrice}`,
});
}
if (localFilters.value.minSeats) {
filters.push({
key: 'minSeats',
label: `${localFilters.value.minSeats}+ posti`,
});
}
return filters;
});
// Date options
const dateFromOptions = (date: string) => {
// Only allow dates from today onwards
return date >= qdate.formatDate(new Date(), 'YYYY/MM/DD');
};
const dateToOptions = (date: string) => {
// Only allow dates from today onwards
if (date < qdate.formatDate(new Date(), 'YYYY/MM/DD')) {
return false;
}
// If dateFrom is set, only allow dates after dateFrom
if (localFilters.value.dateFrom) {
return date >= localFilters.value.dateFrom;
}
return true;
};
// Methods
const debouncedEmit = () => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(() => {
// Salva città recenti
if (localFilters.value.departure) {
addRecentSearch({ city: localFilters.value.departure });
}
if (localFilters.value.destination) {
addRecentSearch({ city: localFilters.value.destination });
}
emit('update', { ...localFilters.value });
}, 500);
};
const removeFilter = (key: string) => {
(localFilters.value as any)[key] =
key === 'sortBy'
? 'date'
: key === 'maxPrice'
? null
: key === 'minSeats'
? null
: '';
emit('update', { ...localFilters.value });
};
const setToday = () => {
const today = qdate.formatDate(new Date(), 'YYYY-MM-DD');
localFilters.value.dateFrom = today;
localFilters.value.dateTo = today;
emit('update', { ...localFilters.value });
};
const setTomorrow = () => {
const tomorrow = qdate.addToDate(new Date(), { days: 1 });
const tomorrowStr = qdate.formatDate(tomorrow, 'YYYY-MM-DD');
localFilters.value.dateFrom = tomorrowStr;
localFilters.value.dateTo = tomorrowStr;
emit('update', { ...localFilters.value });
};
const setThisWeek = () => {
const today = new Date();
const endOfWeek = qdate.addToDate(today, { days: 7 });
localFilters.value.dateFrom = qdate.formatDate(today, 'YYYY-MM-DD');
localFilters.value.dateTo = qdate.formatDate(endOfWeek, 'YYYY-MM-DD');
emit('update', { ...localFilters.value });
};
const setWeekend = () => {
const today = new Date();
const dayOfWeek = today.getDay();
// Calculate next Saturday (6) and Sunday (0)
let daysUntilSaturday = (6 - dayOfWeek + 7) % 7;
if (daysUntilSaturday === 0 && dayOfWeek !== 6) {
daysUntilSaturday = 7;
}
const saturday = qdate.addToDate(today, { days: daysUntilSaturday });
const sunday = qdate.addToDate(saturday, { days: 1 });
localFilters.value.dateFrom = qdate.formatDate(saturday, 'YYYY-MM-DD');
localFilters.value.dateTo = qdate.formatDate(sunday, 'YYYY-MM-DD');
emit('update', { ...localFilters.value });
};
return {
localFilters,
seatsOptions,
sortOptions,
hasActiveFilters,
activeFiltersList,
dateFromOptions,
dateToOptions,
debouncedEmit,
removeFilter,
setToday,
setTomorrow,
setThisWeek,
setWeekend,
VEHICLE_TYPES,
};
},
});
</script>
<style lang="scss" scoped>
.community-filters {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
// Active Filters
&__active {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
// Form
&__form {
display: grid;
gap: 12px;
margin-bottom: 16px;
@media (min-width: 768px) {
grid-template-columns: repeat(2, 1fr);
}
}
&__slider-wrapper {
padding: 8px 12px;
background: #f8f9fa;
border-radius: 8px;
@media (min-width: 768px) {
grid-column: 1 / -1;
}
}
&__slider-label {
display: block;
font-size: 13px;
color: #555;
margin-bottom: 8px;
font-weight: 500;
}
// Quick Dates
&__quick-dates {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
&__quick-dates-label {
font-size: 13px;
color: #888;
font-weight: 500;
width: 100%;
margin-bottom: 4px;
@media (min-width: 768px) {
width: auto;
margin-bottom: 0;
}
}
}
// Dark Mode
.body--dark {
.community-filters {
background: #1e1e30;
&__active,
&__quick-dates {
border-color: rgba(255, 255, 255, 0.1);
}
&__slider-wrapper {
background: rgba(255, 255, 255, 0.05);
}
}
}
</style>

View File

@@ -0,0 +1,655 @@
<!-- components/ride/CommunityRideCard.vue -->
<template>
<div
class="community-ride-card"
@click="$emit('view', ride._id)"
>
<!-- Header: Driver Info -->
<div class="community-ride-card__header">
<div
class="community-ride-card__driver"
@click.stop="$emit('view-driver', ride.userId._id)"
>
<q-avatar size="48px">
<img
v-if="ride.userId.profile?.img"
:src="ride.userId.profile.img"
:alt="`${ride.userId.name} ${ride.userId.surname}`"
/>
<div
v-else
class="community-ride-card__avatar-placeholder"
>
{{ getInitials(ride.userId) }}
</div>
</q-avatar>
<div class="community-ride-card__driver-info">
<div class="community-ride-card__driver-name">
{{ ride.userId.name }} {{ ride.userId.surname }}
</div>
<div class="community-ride-card__driver-stats">
<q-icon
v-if="ride.userId.stats?.averageRating"
name="star"
color="amber"
size="14px"
/>
<span v-if="ride.userId.stats?.averageRating">
{{ ride.userId.stats.averageRating.toFixed(1) }}
</span>
<span
v-if="ride.userId.stats?.completedRides"
class="community-ride-card__driver-trips"
>
{{ ride.userId.stats.completedRides }}
{{ ride.userId.stats.completedRides === 1 ? 'viaggio' : 'viaggi' }}
</span>
</div>
</div>
</div>
<!-- Request Status Badge -->
<q-badge
v-if="ride.userRequestStatus"
:color="getRequestStatusColor(ride.userRequestStatus)"
:label="getRequestStatusLabel(ride.userRequestStatus)"
class="community-ride-card__status-badge"
/>
</div>
<!-- Route Info -->
<div class="community-ride-card__route">
<div class="community-ride-card__route-point">
<q-icon
name="trip_origin"
color="positive"
size="20px"
/>
<div class="community-ride-card__route-city">{{ ride.departure.city }}</div>
</div>
<div class="community-ride-card__route-line">
<q-icon
name="more_vert"
color="grey-4"
size="16px"
/>
</div>
<div class="community-ride-card__route-point">
<q-icon
name="place"
color="negative"
size="20px"
/>
<div class="community-ride-card__route-city">{{ ride.destination.city }}</div>
</div>
</div>
<!-- Date & Time -->
<div class="community-ride-card__datetime">
<div class="community-ride-card__date">
<q-icon
name="event"
size="18px"
color="primary"
/>
<span>{{ formatDate(displayDate) }}</span>
</div>
<div class="community-ride-card__time">
<q-icon
name="schedule"
size="18px"
color="primary"
/>
<span>{{ formatTime(displayDate) }}</span>
</div>
</div>
<!-- Details Grid -->
<div class="community-ride-card__details">
<!-- Price -->
<div class="community-ride-card__detail">
<q-icon
name="euro"
color="positive"
size="20px"
/>
<div class="community-ride-card__detail-content">
<div class="community-ride-card__detail-value">
{{ ride.price.toFixed(2) }}
</div>
<div class="community-ride-card__detail-label">a persona</div>
</div>
</div>
<!-- Available Seats -->
<div class="community-ride-card__detail">
<q-icon
name="event_seat"
color="info"
size="20px"
/>
<div class="community-ride-card__detail-content">
<div class="community-ride-card__detail-value">
{{ ride.passengers.available }}/{{ ride.passengers.total }}
</div>
<div class="community-ride-card__detail-label">posti liberi</div>
</div>
</div>
<!-- Vehicle (if available) -->
<div
v-if="ride.vehicleId"
class="community-ride-card__detail"
>
<q-icon
name="directions_car"
color="grey-7"
size="20px"
/>
<div class="community-ride-card__detail-content">
<div class="community-ride-card__detail-value">
{{ ride.vehicleId.brand }}
</div>
<div class="community-ride-card__detail-label">{{ ride.vehicleId.model }}</div>
</div>
</div>
</div>
<!-- Preferences -->
<div
v-if="hasPreferences"
class="community-ride-card__preferences"
>
<q-chip
v-if="ride.preferences?.smoking === false"
size="sm"
color="positive"
text-color="white"
icon="smoke_free"
dense
>
No fumo
</q-chip>
<q-chip
v-if="ride.preferences?.pets"
size="sm"
color="info"
text-color="white"
icon="pets"
dense
>
Animali OK
</q-chip>
<q-chip
v-if="ride.preferences?.music"
size="sm"
color="purple"
text-color="white"
icon="music_note"
dense
>
Musica
</q-chip>
<q-chip
v-if="ride.preferences?.chatter"
size="sm"
color="orange"
text-color="white"
icon="chat"
dense
>
{{ getChatterLabel(ride.preferences.chatter) }}
</q-chip>
</div>
<!-- Notes -->
<div
v-if="ride.notes"
class="community-ride-card__notes"
>
<q-icon
name="info_outline"
size="16px"
color="grey-6"
/>
<p>{{ ride.notes }}</p>
</div>
<!-- Actions -->
<div
class="community-ride-card__actions"
@click.stop
>
<q-btn
v-if="!ride.userRequestStatus"
color="primary"
label="Richiedi Posto"
icon="add"
unelevated
rounded
:loading="requesting"
@click="$emit('request', ride._id)"
/>
<q-btn
v-else-if="ride.userRequestStatus === 'pending'"
color="orange"
label="Richiesta Inviata"
icon="schedule"
disable
unelevated
rounded
/>
<q-btn
v-else-if="ride.userRequestStatus === 'accepted'"
color="positive"
label="Accettata"
icon="check_circle"
disable
unelevated
rounded
/>
<q-btn
v-else-if="ride.userRequestStatus === 'rejected'"
color="negative"
label="Rifiutata"
icon="cancel"
disable
unelevated
rounded
/>
<q-btn
flat
round
icon="share"
color="grey-7"
@click="$emit('share', ride)"
>
<q-tooltip>Condividi</q-tooltip>
</q-btn>
<q-btn
flat
round
:icon="isFavorite ? 'favorite' : 'favorite_border'"
:color="isFavorite ? 'red' : 'grey-7'"
:loading="togglingFavorite"
@click="toggleFavorite"
>
<q-tooltip>{{ isFavorite ? 'Rimuovi dai' : 'Aggiungi ai' }} preferiti</q-tooltip>
</q-btn>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref, PropType } from 'vue';
import { date as qdate, useQuasar } from 'quasar';
import { Api } from '@api';
import type { CommunityRide } from '../../composables/useCommunityRides';
export default defineComponent({
name: 'CommunityRideCard',
props: {
ride: {
type: Object as PropType<CommunityRide>,
required: true,
},
requesting: {
type: Boolean,
default: false,
},
isFavorite: {
type: Boolean,
default: false,
},
},
emits: ['view', 'view-driver', 'request', 'share', 'favorite-toggled'],
setup(props, { emit }) {
const $q = useQuasar();
// State
const togglingFavorite = ref(false);
// Computed
const hasPreferences = computed(() => {
return !!(
props.ride.preferences?.smoking !== undefined ||
props.ride.preferences?.pets ||
props.ride.preferences?.music ||
props.ride.preferences?.chatter
);
});
const displayDate = computed(() => {
return props.ride._displayDate || props.ride.departureDate;
});
// Methods
const getInitials = (user: any) => {
if (!user) return '?';
const name = user.name || '';
const surname = user.surname || '';
return `${name.charAt(0)}${surname.charAt(0)}`.toUpperCase();
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
if (qdate.isSameDate(date, today, 'day')) {
return 'Oggi';
} else if (qdate.isSameDate(date, tomorrow, 'day')) {
return 'Domani';
}
return qdate.formatDate(dateStr, 'ddd DD MMM YYYY');
};
const formatTime = (dateStr: string) => {
return qdate.formatDate(dateStr, 'HH:mm');
};
const getRequestStatusColor = (status: string) => {
const colors: Record<string, string> = {
pending: 'orange',
accepted: 'positive',
rejected: 'negative',
};
return colors[status] || 'grey';
};
const getRequestStatusLabel = (status: string) => {
const labels: Record<string, string> = {
pending: 'In attesa',
accepted: 'Accettata',
rejected: 'Rifiutata',
};
return labels[status] || status;
};
const getChatterLabel = (level: string) => {
const labels: Record<string, string> = {
none: 'Silenzioso',
little: 'Poco',
normal: 'Normale',
much: 'Chiacchierone',
};
return labels[level] || level;
};
const toggleFavorite = async () => {
togglingFavorite.value = true;
try {
const response = await Api.SendReq(
`/api/viaggi/rides/${props.ride._id}/favorite`,
'POST'
);
if (response.success) {
emit('favorite-toggled', response.data.isFavorite);
$q.notify({
type: response.data.isFavorite ? 'positive' : 'info',
message: response.data.isFavorite
? 'Aggiunto ai preferiti'
: 'Rimosso dai preferiti',
});
}
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.message || 'Errore nel salvare il preferito',
});
} finally {
togglingFavorite.value = false;
}
};
return {
// State
togglingFavorite,
// Computed
hasPreferences,
// Methods
getInitials,
formatDate,
formatTime,
getRequestStatusColor,
getRequestStatusLabel,
getChatterLabel,
toggleFavorite,
displayDate,
};
},
});
</script>
<style lang="scss" scoped>
.community-ride-card {
background: white;
border-radius: 16px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
cursor: pointer;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
// Header
&__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
&__driver {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
&__avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
border-radius: 50%;
}
&__driver-info {
display: flex;
flex-direction: column;
gap: 4px;
}
&__driver-name {
font-weight: 600;
color: #333;
font-size: 15px;
}
&__driver-stats {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #888;
}
&__driver-trips {
color: #888;
}
&__status-badge {
flex-shrink: 0;
}
// Route
&__route {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 12px;
}
&__route-point {
display: flex;
align-items: center;
gap: 8px;
}
&__route-city {
font-weight: 600;
color: #333;
font-size: 15px;
}
&__route-line {
display: flex;
padding-left: 2px;
}
// DateTime
&__datetime {
display: flex;
gap: 16px;
margin-bottom: 12px;
padding: 10px 12px;
background: #f8f9fa;
border-radius: 8px;
}
&__date,
&__time {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #555;
font-weight: 500;
}
// Details
&__details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 12px;
margin-bottom: 12px;
}
&__detail {
display: flex;
align-items: center;
gap: 8px;
}
&__detail-content {
display: flex;
flex-direction: column;
}
&__detail-value {
font-weight: 700;
color: #333;
font-size: 15px;
line-height: 1.2;
}
&__detail-label {
font-size: 11px;
color: #888;
line-height: 1.2;
}
// Preferences
&__preferences {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
}
// Notes
&__notes {
display: flex;
gap: 8px;
padding: 10px 12px;
background: #fafafa;
border-radius: 8px;
margin-bottom: 12px;
p {
margin: 0;
font-size: 13px;
color: #555;
line-height: 1.5;
}
}
// Actions
&__actions {
display: flex;
align-items: center;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
> :first-child {
flex: 1;
}
}
}
// Dark Mode
.body--dark {
.community-ride-card {
background: #1e1e30;
&__header,
&__actions {
border-color: rgba(255, 255, 255, 0.1);
}
&__driver-name,
&__route-city,
&__detail-value {
color: #fff;
}
&__datetime,
&__notes {
background: rgba(255, 255, 255, 0.05);
}
}
}
</style>

View File

@@ -154,7 +154,7 @@ export default defineComponent({
const requiresPrice = (contribType: ContribType): boolean => { const requiresPrice = (contribType: ContribType): boolean => {
const label = contribType.label.toLowerCase(); const label = contribType.label.toLowerCase();
const noPriceTypes = ['dono', 'baratto', 'scambio lavoro']; const noPriceTypes = ['dono', 'Offerta Libera', 'baratto', 'scambio lavoro'];
return !noPriceTypes.includes(label); return !noPriceTypes.includes(label);
}; };

View File

@@ -74,7 +74,7 @@
icon="notifications_active" icon="notifications_active"
size="sm" size="sm"
unelevated unelevated
@click.stop="$emit('manage-requests')" @click.stop="$emit('manage-requests', ride)"
/> />
</div> </div>
@@ -87,7 +87,7 @@
icon="star" icon="star"
size="sm" size="sm"
unelevated unelevated
@click.stop="$emit('leave-feedback')" @click.stop="$emit('leave-feedback', ride)"
/> />
</div> </div>
</q-card-section> </q-card-section>
@@ -102,7 +102,7 @@
color="primary" color="primary"
label="Modifica" label="Modifica"
icon="edit" icon="edit"
@click.stop="$emit('edit')" @click.stop="$emit('edit', ride._id)"
/> />
<q-btn <q-btn
v-if="canComplete" v-if="canComplete"
@@ -112,7 +112,7 @@
color="positive" color="positive"
label="Completa" label="Completa"
icon="check_circle" icon="check_circle"
@click.stop="$emit('complete')" @click.stop="$emit('complete', ride)"
/> />
<q-space /> <q-space />
<q-btn <q-btn
@@ -123,7 +123,7 @@
color="negative" color="negative"
label="Cancella" label="Cancella"
icon="cancel" icon="cancel"
@click.stop="$emit('cancel')" @click.stop="$emit('cancel', ride)"
/> />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
@@ -162,7 +162,7 @@ export default defineComponent({
const { formatRideDate, getStatusColor, getStatusLabel } = useRides(); const { formatRideDate, getStatusColor, getStatusLabel } = useRides();
const formattedDate = computed(() => { const formattedDate = computed(() => {
const date = new Date(props.ride.dateTime); const date = new Date(props.ride.departureDate);
return date.toLocaleDateString('it-IT', { return date.toLocaleDateString('it-IT', {
weekday: 'short', weekday: 'short',
day: 'numeric', day: 'numeric',
@@ -171,7 +171,7 @@ export default defineComponent({
}); });
const formattedTime = computed(() => { const formattedTime = computed(() => {
const date = new Date(props.ride.dateTime); const date = new Date(props.ride.departureDate);
return date.toLocaleTimeString('it-IT', { return date.toLocaleTimeString('it-IT', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit'

View File

@@ -8,8 +8,8 @@ export default defineComponent({
props: { props: {
modelValue: { modelValue: {
type: Object as PropType<Recurrence>, type: Object as PropType<Recurrence>,
default: () => ({ type: 'once' }) default: () => ({ type: 'once' }),
} },
}, },
emits: ['update:modelValue'], emits: ['update:modelValue'],
@@ -22,65 +22,95 @@ export default defineComponent({
customDates: [], customDates: [],
startDate: '', startDate: '',
endDate: '', endDate: '',
excludedDates: [] excludedDates: [],
}); });
const selectedDates = ref<string[]>([]); const selectedDates = ref<string[]>([]);
const excludedDates = ref<string[]>([]); const excludedDates = ref<string[]>([]);
// Opzioni // Opzioni
const recurrenceTypes = RECURRENCE_TYPE_OPTIONS.map(opt => ({ const recurrenceTypes = RECURRENCE_TYPE_OPTIONS.map((opt) => ({
label: opt.label, label: opt.label,
value: opt.value, value: opt.value,
icon: opt.icon icon: opt.icon,
})); }));
const daysOfWeek = DAYS_OF_WEEK; 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 per sincronizzare con modelValue
watch(() => props.modelValue, (newVal) => { watch(
if (newVal) { () => props.modelValue,
Object.assign(localRecurrence, newVal); (newVal) => {
if (newVal) {
Object.assign(localRecurrence, newVal);
if (newVal.customDates) { if (newVal.customDates) {
selectedDates.value = newVal.customDates.map(d => selectedDates.value = newVal.customDates.map((d) =>
typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0] typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0]
); );
} }
if (newVal.excludedDates) { if (newVal.excludedDates) {
excludedDates.value = newVal.excludedDates.map(d => excludedDates.value = newVal.excludedDates.map((d) =>
typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0] typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0]
); );
}
} }
} },
}, { immediate: true, deep: true }); { immediate: true, deep: true }
);
// Watch per emettere update // Watch per emettere update
watch([localRecurrence, selectedDates, excludedDates], () => { watch(
const result: Recurrence = { [localRecurrence, selectedDates, excludedDates],
type: localRecurrence.type () => {
}; const result: Recurrence = {
type: localRecurrence.type,
};
if (localRecurrence.type !== 'once') { if (localRecurrence.type !== 'once') {
result.startDate = localRecurrence.startDate; result.startDate = localRecurrence.startDate;
result.endDate = localRecurrence.endDate; result.endDate = localRecurrence.endDate;
if (excludedDates.value.length > 0) { if (excludedDates.value.length > 0) {
result.excludedDates = excludedDates.value; result.excludedDates = excludedDates.value;
}
} }
}
if (localRecurrence.type === 'weekly' || localRecurrence.type === 'custom_days') { if (localRecurrence.type === 'weekly' || localRecurrence.type === 'custom_days') {
result.daysOfWeek = localRecurrence.daysOfWeek; result.daysOfWeek = localRecurrence.daysOfWeek;
} }
if (localRecurrence.type === 'custom_dates') { if (localRecurrence.type === 'custom_dates') {
result.customDates = selectedDates.value; result.customDates = selectedDates.value;
} }
emit('update:modelValue', result); emit('update:modelValue', result);
}, { deep: true }); },
{ deep: true }
);
// Methods // Methods
const isDaySelected = (day: number): boolean => { const isDaySelected = (day: number): boolean => {
@@ -116,7 +146,7 @@ export default defineComponent({
return date.toLocaleDateString('it-IT', { return date.toLocaleDateString('it-IT', {
weekday: 'short', weekday: 'short',
day: 'numeric', day: 'numeric',
month: 'short' month: 'short',
}); });
}; };
@@ -149,7 +179,7 @@ export default defineComponent({
return 'Seleziona i giorni della settimana'; return 'Seleziona i giorni della settimana';
} }
const weeklyDays = localRecurrence.daysOfWeek const weeklyDays = localRecurrence.daysOfWeek
.map(d => daysOfWeek.find(day => day.value === d)?.label) .map((d) => daysOfWeek.find((day) => day.value === d)?.label)
.join(', '); .join(', ');
return `Ogni settimana: ${weeklyDays}`; return `Ogni settimana: ${weeklyDays}`;
@@ -158,7 +188,7 @@ export default defineComponent({
return 'Seleziona i giorni della settimana'; return 'Seleziona i giorni della settimana';
} }
const customDays = localRecurrence.daysOfWeek const customDays = localRecurrence.daysOfWeek
.map(d => daysOfWeek.find(day => day.value === d)?.label) .map((d) => daysOfWeek.find((day) => day.value === d)?.label)
.join(', '); .join(', ');
return `Giorni selezionati: ${customDays}`; return `Giorni selezionati: ${customDays}`;
@@ -193,7 +223,9 @@ export default defineComponent({
removeExcludedDate, removeExcludedDate,
formatDate, formatDate,
dateOptions, dateOptions,
exclusionDateOptions exclusionDateOptions,
formattedStartDate,
formattedEndDate,
}; };
} },
}); });

View File

@@ -1,7 +1,11 @@
<template> <template>
<div class="recurrence-selector"> <div class="recurrence-selector">
<div class="recurrence-selector__header"> <div class="recurrence-selector__header">
<q-icon name="repeat" size="20px" color="primary" /> <q-icon
name="repeat"
size="20px"
color="primary"
/>
<span class="recurrence-selector__title">Ripetizione Percorso</span> <span class="recurrence-selector__title">Ripetizione Percorso</span>
</div> </div>
@@ -20,7 +24,10 @@
</div> </div>
<!-- Opzioni specifiche per tipo --> <!-- Opzioni specifiche per tipo -->
<transition name="slide-fade" mode="out-in"> <transition
name="slide-fade"
mode="out-in"
>
<!-- Weekly: Giorni della settimana --> <!-- Weekly: Giorni della settimana -->
<div <div
v-if="localRecurrence.type === 'weekly'" v-if="localRecurrence.type === 'weekly'"
@@ -86,7 +93,10 @@
/> />
<!-- Chips date selezionate --> <!-- Chips date selezionate -->
<div v-if="selectedDates.length > 0" class="recurrence-selector__selected-dates"> <div
v-if="selectedDates.length > 0"
class="recurrence-selector__selected-dates"
>
<q-chip <q-chip
v-for="(date, index) in selectedDates" v-for="(date, index) in selectedDates"
:key="date" :key="date"
@@ -114,30 +124,42 @@
<div class="row q-gutter-md"> <div class="row q-gutter-md">
<q-input <q-input
v-model="localRecurrence.startDate" v-model="formattedStartDate"
type="date" type="date"
label="Data inizio" label="Data inizio"
outlined outlined
dense dense
class="col" class="col"
:rules="[val => !!val || 'Data inizio richiesta']" :rules="[(val) => !!val || 'Data inizio richiesta']"
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="event" color="positive" /> <q-icon
name="event"
color="positive"
/>
</template> </template>
</q-input> </q-input>
<q-input <q-input
v-model="localRecurrence.endDate" v-model="formattedEndDate"
type="date" type="date"
label="Data fine" label="Data fine"
outlined outlined
dense dense
class="col" class="col"
:rules="[val => !!val || 'Data fine richiesta', val => !localRecurrence.startDate || val >= localRecurrence.startDate || 'Data fine deve essere dopo data inizio']" :rules="[
(val) => !!val || 'Data fine richiesta',
(val) =>
!localRecurrence.startDate ||
val >= localRecurrence.startDate ||
'Data fine deve essere dopo data inizio',
]"
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="event" color="negative" /> <q-icon
name="event"
color="negative"
/>
</template> </template>
</q-input> </q-input>
</div> </div>
@@ -161,7 +183,10 @@
color="negative" color="negative"
/> />
<div v-if="excludedDates.length > 0" class="q-mt-sm"> <div
v-if="excludedDates.length > 0"
class="q-mt-sm"
>
<q-chip <q-chip
v-for="(date, index) in excludedDates" v-for="(date, index) in excludedDates"
:key="date" :key="date"
@@ -183,8 +208,14 @@
</transition> </transition>
<!-- Riepilogo --> <!-- Riepilogo -->
<div v-if="summaryText" class="recurrence-selector__summary"> <div
<q-icon name="info" color="info" /> v-if="summaryText"
class="recurrence-selector__summary"
>
<q-icon
name="info"
color="info"
/>
<span>{{ summaryText }}</span> <span>{{ summaryText }}</span>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,920 @@
<!-- components/ride/RideCalendar.vue -->
<template>
<div class="ride-calendar">
<!-- Calendar Header -->
<div class="ride-calendar__header">
<q-btn
flat
round
icon="chevron_left"
color="primary"
@click="previousMonth"
/>
<div class="ride-calendar__month-year">
<h2>{{ currentMonthName }} {{ currentYear }}</h2>
<p v-if="stats">
{{ stats.totalRides }} {{ stats.totalRides === 1 ? 'viaggio' : 'viaggi' }}
</p>
</div>
<q-btn
flat
round
icon="chevron_right"
color="primary"
@click="nextMonth"
/>
</div>
<!-- Quick Navigation -->
<div class="ride-calendar__quick-nav">
<q-btn
flat
dense
label="Oggi"
color="primary"
size="sm"
@click="goToToday"
/>
<q-btn
flat
dense
label="Questo Mese"
color="primary"
size="sm"
@click="goToCurrentMonth"
/>
</div>
<!-- Loading -->
<div
v-if="loading"
class="ride-calendar__loading"
>
<q-spinner-dots
size="50px"
color="primary"
/>
<p>Caricamento calendario...</p>
</div>
<!-- Calendar Grid -->
<div
v-else
class="ride-calendar__grid"
>
<!-- Weekday Headers -->
<div class="ride-calendar__weekdays">
<div
v-for="day in weekdays"
:key="day"
class="ride-calendar__weekday"
>
{{ day }}
</div>
</div>
<!-- Calendar Days -->
<div class="ride-calendar__days">
<!-- Empty cells for days before month starts -->
<div
v-for="n in startDayOffset"
:key="`empty-${n}`"
class="ride-calendar__day ride-calendar__day--empty"
/>
<!-- Actual days -->
<div
v-for="day in daysInMonth"
:key="day"
:class="getDayClasses(day)"
@click="selectDay(day)"
>
<div class="ride-calendar__day-number">{{ day }}</div>
<div
v-if="getDayData(day)"
class="ride-calendar__day-info"
>
<div class="ride-calendar__day-rides">
{{ getDayData(day).count }}
</div>
<div class="ride-calendar__day-seats">
<q-icon
name="event_seat"
size="10px"
/>
{{ getDayData(day).totalSeats }}
</div>
</div>
</div>
</div>
</div>
<!-- Legend -->
<div class="ride-calendar__legend">
<div class="ride-calendar__legend-item">
<div class="ride-calendar__legend-box ride-calendar__legend-box--today" />
<span>Oggi</span>
</div>
<div class="ride-calendar__legend-item">
<div class="ride-calendar__legend-box ride-calendar__legend-box--selected" />
<span>Selezionato</span>
</div>
<div class="ride-calendar__legend-item">
<div class="ride-calendar__legend-box ride-calendar__legend-box--has-rides" />
<span>Con viaggi</span>
</div>
<div class="ride-calendar__legend-item">
<div class="ride-calendar__legend-box ride-calendar__legend-box--past" />
<span>Passato</span>
</div>
</div>
<!-- Selected Day Details Drawer -->
<q-dialog
v-model="showDayDetails"
position="bottom"
:maximized="$q.screen.lt.sm"
>
<q-card class="ride-calendar__day-details">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">
{{ selectedDateFormatted }}
</div>
<q-space />
<q-btn
icon="close"
flat
round
dense
v-close-popup
/>
</q-card-section>
<q-card-section v-if="selectedDayRides && selectedDayRides.length > 0">
<div class="ride-calendar__day-summary">
<div class="ride-calendar__summary-item">
<q-icon
name="directions_car"
color="primary"
size="24px"
/>
<div>
<div class="ride-calendar__summary-value">
{{ selectedDayRides.length }}
</div>
<div class="ride-calendar__summary-label">
{{ selectedDayRides.length === 1 ? 'Viaggio' : 'Viaggi' }}
</div>
</div>
</div>
<div class="ride-calendar__summary-item">
<q-icon
name="event_seat"
color="positive"
size="24px"
/>
<div>
<div class="ride-calendar__summary-value">
{{ selectedDayTotalSeats }}
</div>
<div class="ride-calendar__summary-label">Posti disponibili</div>
</div>
</div>
</div>
<!-- Rides List -->
<div class="ride-calendar__rides-list">
<div
v-for="ride in selectedDayRides"
:key="ride._id"
class="ride-calendar__ride-item"
@click="viewRide(ride._id)"
>
<div class="ride-calendar__ride-time">
{{ ride.time }}
<q-icon
v-if="ride._isRecurrence"
name="repeat"
size="12px"
color="primary"
class="q-ml-xs"
>
<q-tooltip>Viaggio ricorrente</q-tooltip>
</q-icon>
</div>
<div class="ride-calendar__ride-info">
<div class="ride-calendar__ride-route">
<span>{{ ride.departure }}</span>
<q-icon
name="arrow_forward"
size="14px"
/>
<span>{{ ride.destination }}</span>
</div>
<div class="ride-calendar__ride-meta">
<q-avatar size="20px">
<img
v-if="ride.driver?.profile?.img"
:src="ride.driver.profile.img"
/>
<div
v-else
class="ride-calendar__avatar-mini"
>
{{ getDriverInitials(ride.driver) }}
</div>
</q-avatar>
<span>{{ ride.driver?.name }} {{ ride.driver?.surname }}</span>
</div>
</div>
<div class="ride-calendar__ride-details">
<q-chip
size="sm"
color="positive"
text-color="white"
dense
>
{{ ride.price }}
</q-chip>
<q-chip
size="sm"
color="info"
text-color="white"
dense
>
{{ ride.availableSeats }} posti
</q-chip>
</div>
<q-icon
name="chevron_right"
color="grey-5"
/>
</div>
</div>
<q-btn
color="primary"
label="Vedi tutti in lista"
icon="list"
unelevated
rounded
class="full-width q-mt-md"
@click="viewAllDayRides"
/>
</q-card-section>
<q-card-section v-else>
<div class="text-center q-pa-lg">
<q-icon
name="event_busy"
size="48px"
color="grey-4"
/>
<p class="text-grey-6 q-mt-md">Nessun viaggio disponibile per questa data</p>
</div>
</q-card-section>
</q-card>
</q-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, watch, PropType } from 'vue';
import { useRouter } from 'vue-router';
import { useQuasar, date as qdate } from 'quasar';
import type { CalendarData } from '../../composables/useCommunityRides';
export default defineComponent({
name: 'RideCalendar',
props: {
calendarData: {
type: Object as PropType<CalendarData | null>,
default: null,
},
loading: {
type: Boolean,
default: false,
},
},
emits: ['month-change', 'day-select', 'view-ride'],
setup(props, { emit }) {
const router = useRouter();
const $q = useQuasar();
// State
const currentMonth = ref(new Date().getMonth());
const currentYear = ref(new Date().getFullYear());
const selectedDay = ref<number | null>(null);
const showDayDetails = ref(false);
const weekdays = ['Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom'];
// Computed
const currentMonthName = computed(() => {
const monthNames = [
'Gennaio',
'Febbraio',
'Marzo',
'Aprile',
'Maggio',
'Giugno',
'Luglio',
'Agosto',
'Settembre',
'Ottobre',
'Novembre',
'Dicembre',
];
return monthNames[currentMonth.value];
});
const daysInMonth = computed(() => {
return new Date(currentYear.value, currentMonth.value + 1, 0).getDate();
});
const startDayOffset = computed(() => {
const firstDay = new Date(currentYear.value, currentMonth.value, 1).getDay();
// Convert Sunday (0) to 7, and shift so Monday is 0
return firstDay === 0 ? 6 : firstDay - 1;
});
const stats = computed(() => {
if (!props.calendarData) return null;
return {
totalRides: props.calendarData.totalRides,
datesWithRides: props.calendarData.datesWithRides,
};
});
const selectedDateFormatted = computed(() => {
if (!selectedDay.value) return '';
const date = new Date(currentYear.value, currentMonth.value, selectedDay.value);
return qdate.formatDate(date, 'dddd DD MMMM YYYY');
});
const selectedDayRides = computed(() => {
if (!selectedDay.value || !props.calendarData) return [];
const dateKey = getDateKey(selectedDay.value);
return props.calendarData.ridesByDate[dateKey]?.rides || [];
});
const selectedDayTotalSeats = computed(() => {
if (!selectedDay.value || !props.calendarData) return 0;
const dateKey = getDateKey(selectedDay.value);
return props.calendarData.ridesByDate[dateKey]?.totalSeats || 0;
});
// Methods
const getDateKey = (day: number): string => {
const date = new Date(currentYear.value, currentMonth.value, day);
return qdate.formatDate(date, 'YYYY-MM-DD');
};
const getDayData = (day: number) => {
if (!props.calendarData) return null;
const dateKey = getDateKey(day);
return props.calendarData.ridesByDate[dateKey] || null;
};
const isToday = (day: number): boolean => {
const today = new Date();
return (
day === today.getDate() &&
currentMonth.value === today.getMonth() &&
currentYear.value === today.getFullYear()
);
};
const isPast = (day: number): boolean => {
const date = new Date(currentYear.value, currentMonth.value, day);
const today = new Date();
today.setHours(0, 0, 0, 0);
return date < today;
};
const getDayClasses = (day: number) => {
return {
'ride-calendar__day': true,
'ride-calendar__day--today': isToday(day),
'ride-calendar__day--selected': selectedDay.value === day,
'ride-calendar__day--has-rides': !!getDayData(day),
'ride-calendar__day--past': isPast(day),
};
};
const selectDay = (day: number) => {
if (isPast(day)) return;
selectedDay.value = day;
showDayDetails.value = true;
const dateKey = getDateKey(day);
emit('day-select', dateKey);
};
const previousMonth = () => {
if (currentMonth.value === 0) {
currentMonth.value = 11;
currentYear.value--;
} else {
currentMonth.value--;
}
};
const nextMonth = () => {
if (currentMonth.value === 11) {
currentMonth.value = 0;
currentYear.value++;
} else {
currentMonth.value++;
}
};
const goToToday = () => {
const today = new Date();
currentMonth.value = today.getMonth();
currentYear.value = today.getFullYear();
selectedDay.value = today.getDate();
showDayDetails.value = true;
};
const goToCurrentMonth = () => {
const today = new Date();
currentMonth.value = today.getMonth();
currentYear.value = today.getFullYear();
selectedDay.value = null;
};
const viewRide = (rideId: string) => {
showDayDetails.value = false;
emit('view-ride', rideId);
};
const viewAllDayRides = () => {
showDayDetails.value = false;
const dateKey = getDateKey(selectedDay.value!);
emit('day-select', dateKey);
};
const getDriverInitials = (driver: any) => {
if (!driver) return '?';
const name = driver.name || '';
const surname = driver.surname || '';
return `${name.charAt(0)}${surname.charAt(0)}`.toUpperCase();
};
// Watch for month/year changes
watch([currentMonth, currentYear], () => {
emit('month-change', currentMonth.value, currentYear.value);
});
const getRecurrenceDatesForRide = (
ride: any,
startRange: Date,
endRange: Date
): Date[] => {
const { recurrence, departureDate } = ride;
if (!recurrence || recurrence.type === 'once') {
const date = new Date(departureDate);
if (date >= startRange && date <= endRange) {
return [date];
}
return [];
}
const dates: Date[] = [];
const excludedSet = new Set(
(recurrence.excludedDates || []).map(
(d: any) => new Date(d).toISOString().split('T')[0]
)
);
if (recurrence.type === 'weekly' || recurrence.type === 'custom_days') {
if (!recurrence.daysOfWeek?.length) return [new Date(departureDate)];
let current = new Date(startRange);
while (current <= endRange) {
if (recurrence.daysOfWeek.includes(current.getDay())) {
const dateStr = current.toISOString().split('T')[0];
if (!excludedSet.has(dateStr)) {
dates.push(new Date(current));
}
}
current.setDate(current.getDate() + 1);
}
} else if (recurrence.type === 'custom_dates') {
(recurrence.customDates || []).forEach((date: any) => {
const d = new Date(date);
if (d >= startRange && d <= endRange) {
const dateStr = d.toISOString().split('T')[0];
if (!excludedSet.has(dateStr)) {
dates.push(d);
}
}
});
}
return dates;
};
return {
// State
currentMonth,
currentYear,
selectedDay,
showDayDetails,
weekdays,
// Computed
currentMonthName,
daysInMonth,
startDayOffset,
stats,
selectedDateFormatted,
selectedDayRides,
selectedDayTotalSeats,
// Methods
getDayData,
getDayClasses,
selectDay,
previousMonth,
nextMonth,
goToToday,
goToCurrentMonth,
viewRide,
viewAllDayRides,
getDriverInitials,
};
},
});
</script>
<style lang="scss" scoped>
.ride-calendar {
background: white;
border-radius: 16px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
// Header
&__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
&__month-year {
text-align: center;
h2 {
font-size: 20px;
font-weight: 600;
margin: 0;
color: #333;
}
p {
margin: 4px 0 0;
font-size: 13px;
color: #888;
}
}
// Quick Nav
&__quick-nav {
display: flex;
gap: 8px;
justify-content: center;
margin-bottom: 16px;
}
// Loading
&__loading {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 20px;
p {
margin-top: 12px;
color: #888;
}
}
// Grid
&__grid {
margin-bottom: 16px;
}
&__weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
margin-bottom: 8px;
}
&__weekday {
text-align: center;
font-size: 12px;
font-weight: 600;
color: #666;
padding: 8px 4px;
}
&__days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
&__day {
aspect-ratio: 1;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.2s;
background: white;
&:hover:not(&--past):not(&--empty) {
background: #f5f5f5;
border-color: #667eea;
}
&--empty {
border: none;
cursor: default;
}
&--today {
border: 2px solid #667eea;
font-weight: 700;
}
&--selected {
background: #667eea;
border-color: #667eea;
.ride-calendar__day-number {
color: white;
}
.ride-calendar__day-info {
color: white;
}
}
&--has-rides {
background: #f0f4ff;
border-color: #667eea;
}
&--past {
opacity: 0.4;
cursor: not-allowed;
&:hover {
background: white;
border-color: #e0e0e0;
}
}
}
&__day-number {
font-size: 14px;
font-weight: 600;
color: #333;
}
&__day-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
font-size: 10px;
color: #667eea;
}
&__day-rides {
font-weight: 700;
}
&__day-seats {
display: flex;
align-items: center;
gap: 2px;
}
// Legend
&__legend {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
&__legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #666;
}
&__legend-box {
width: 16px;
height: 16px;
border-radius: 4px;
&--today {
border: 2px solid #667eea;
background: white;
}
&--selected {
background: #667eea;
}
&--has-rides {
background: #f0f4ff;
border: 1px solid #667eea;
}
&--past {
background: #f5f5f5;
opacity: 0.4;
}
}
// Day Details
&__day-details {
max-width: 600px;
margin: 0 auto;
}
&__day-summary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 20px;
}
&__summary-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 12px;
}
&__summary-value {
font-size: 24px;
font-weight: 700;
color: #333;
line-height: 1;
}
&__summary-label {
font-size: 12px;
color: #888;
}
&__rides-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 400px;
overflow-y: auto;
}
&__ride-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #e9ecef;
transform: translateX(4px);
}
}
&__ride-time {
font-weight: 700;
color: #667eea;
min-width: 50px;
}
&__ride-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
&__ride-route {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
font-size: 14px;
color: #333;
}
&__ride-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #888;
}
&__avatar-mini {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
font-size: 10px;
border-radius: 50%;
}
&__ride-details {
display: flex;
gap: 4px;
}
}
// Dark Mode
.body--dark {
.ride-calendar {
background: #1e1e30;
&__month-year h2 {
color: #fff;
}
&__day {
background: #2a2a40;
border-color: rgba(255, 255, 255, 0.1);
&:hover:not(&--past):not(&--empty) {
background: rgba(255, 255, 255, 0.1);
}
&--has-rides {
background: rgba(102, 126, 234, 0.2);
}
&--selected {
background: #667eea;
}
}
&__day-number {
color: #fff;
}
&__legend {
border-color: rgba(255, 255, 255, 0.1);
}
&__summary-item,
&__ride-item {
background: rgba(255, 255, 255, 0.05);
&:hover {
background: rgba(255, 255, 255, 0.1);
}
}
&__summary-value,
&__ride-route {
color: #fff;
}
}
}
</style>

View File

@@ -87,17 +87,17 @@ export default defineComponent({
// Date computed // Date computed
const formattedDate = computed(() => { const formattedDate = computed(() => {
return formatRideDate(props.ride.dateTime); return formatRideDate(props.ride.departureDate);
}); });
const formattedDepartureTime = computed(() => { const formattedDepartureTime = 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' }); return date.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
}); });
const estimatedArrival = computed(() => { const estimatedArrival = computed(() => {
if (!props.ride.estimatedDuration) return null; if (!props.ride.estimatedDuration) return null;
const departure = new Date(props.ride.dateTime); const departure = new Date(props.ride.departureDate);
const arrival = new Date(departure.getTime() + props.ride.estimatedDuration * 60000); const arrival = new Date(departure.getTime() + props.ride.estimatedDuration * 60000);
return arrival.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); return arrival.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
}); });

View File

@@ -247,7 +247,7 @@ export default defineComponent({
} }
loading.value = true; loading.value = true;
console.log('🚗 Calculating route...'); // console.log('🚗 Calculating route...');
try { try {
const waypointCoords: Coordinates[] = props.waypoints const waypointCoords: Coordinates[] = props.waypoints

View File

@@ -1,5 +1,5 @@
import { ref, reactive, computed, watch, defineComponent, PropType } from 'vue'; import { ref, reactive, computed, watch, defineComponent, PropType } from 'vue';
import type { Vehicle, VehicleType, VehicleFeature } from '../../types'; import type { Vehicle } from '../../types';
import { VEHICLE_TYPES, VEHICLE_COLORS, VEHICLE_FEATURES_OPTIONS } from '../../types'; import { VEHICLE_TYPES, VEHICLE_COLORS, VEHICLE_FEATURES_OPTIONS } from '../../types';
export default defineComponent({ export default defineComponent({
@@ -26,7 +26,7 @@ export default defineComponent({
// State // State
const showNewVehicleForm = ref(false); const showNewVehicleForm = ref(false);
const saveVehicleToProfile = ref(false); const saveVehicleToProfile = ref(false);
const selectedFeatures = ref<VehicleFeature[]>([]); const selectedFeatures = ref<string[]>([]);
const localVehicle = reactive<Vehicle>({ const localVehicle = reactive<Vehicle>({
type: 'auto', type: 'auto',
@@ -41,7 +41,6 @@ export default defineComponent({
}); });
// Options // Options
const vehicleTypes = VEHICLE_TYPES;
const vehicleFeatures = VEHICLE_FEATURES_OPTIONS; const vehicleFeatures = VEHICLE_FEATURES_OPTIONS;
const colorOptions = VEHICLE_COLORS.map((c) => ({ const colorOptions = VEHICLE_COLORS.map((c) => ({
label: c.name, label: c.name,
@@ -99,15 +98,8 @@ export default defineComponent({
showNewVehicleForm.value = false; showNewVehicleForm.value = false;
}; };
const getVehicleTypeIcon = (type?: VehicleType): string => { const getVehicleTypeIcon = (type?: string): string => {
const icons: Record<VehicleType, string> = { return VEHICLE_TYPES.find((vt: any) => vt.value === type)?.icon || ' ';
auto: '🚗',
moto: '🏍️',
furgone: '🚐',
minibus: '🚌',
altro: '🚙',
};
return icons[type || 'auto'] || '🚗';
}; };
const getColorHex = (colorName?: string): string => { const getColorHex = (colorName?: string): string => {
@@ -127,7 +119,7 @@ export default defineComponent({
localVehicle, localVehicle,
// Options // Options
vehicleTypes, VEHICLE_TYPES,
vehicleFeatures, vehicleFeatures,
colorOptions, colorOptions,

View File

@@ -76,7 +76,7 @@
<div class="vehicle-selector__label">Tipo veicolo:</div> <div class="vehicle-selector__label">Tipo veicolo:</div>
<div class="vehicle-selector__type-buttons"> <div class="vehicle-selector__type-buttons">
<q-btn <q-btn
v-for="type in vehicleTypes" v-for="type in VEHICLE_TYPES"
:key="type.value" :key="type.value"
:color="localVehicle.type === type.value ? 'primary' : 'grey-4'" :color="localVehicle.type === type.value ? 'primary' : 'grey-4'"
:text-color="localVehicle.type === type.value ? 'white' : 'dark'" :text-color="localVehicle.type === type.value ? 'white' : 'dark'"

View File

@@ -1,8 +1,7 @@
// RideWidget.ts // RideWidget.ts
import { defineComponent, ref, computed, onMounted, watch } from 'vue'; import { defineComponent, ref, computed, onMounted, watch, onUnmounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Api } from '@api'; import { Api } from '@api';
import type { Ride, ContribType } from '../../../types/viaggi.types';
interface WidgetStats { interface WidgetStats {
offers: number; offers: number;
@@ -10,6 +9,27 @@ interface WidgetStats {
matches: number; matches: number;
} }
interface ContribType {
label: string;
value?: number;
}
interface Ride {
_id: string;
type: 'offer' | 'request';
departure: { city: string; address?: string; lat?: number; lng?: number };
destination: { city: string; address?: string; lat?: number; lng?: number };
departureDate: string | Date;
status: string;
availableSeats?: number;
contribution?: {
types: ContribType[];
euroPrice?: number;
risPrice?: number;
};
userId?: any;
}
interface WidgetData { interface WidgetData {
stats: WidgetStats; stats: WidgetStats;
recentRides: Ride[]; recentRides: Ride[];
@@ -72,9 +92,10 @@ export default defineComponent({
loading.value = true; loading.value = true;
try { try {
const response = await Api.SendReqWithData('/api/viaggi/widget/data', 'GET', {}); // ✅ Chiamata corretta con Api.SendReq
const response = await Api.SendReqWithData('/api/viaggi/widget/data', 'GET');
if (response.success) { if (response.success && response.data) {
const data: WidgetData = response.data; const data: WidgetData = response.data;
stats.value = data.stats || { offers: 0, requests: 0, matches: 0 }; stats.value = data.stats || { offers: 0, requests: 0, matches: 0 };
@@ -84,10 +105,25 @@ export default defineComponent({
unreadMessages.value = data.unreadMessages || 0; unreadMessages.value = data.unreadMessages || 0;
emit('loaded', data); emit('loaded', data);
} else {
console.warn('⚠️ Risposta vuota o non valida dal widget API');
// Imposta valori di default
stats.value = { offers: 0, requests: 0, matches: 0 };
recentRides.value = [];
myActiveRides.value = [];
pendingRequests.value = 0;
unreadMessages.value = 0;
} }
} catch (error) { } catch (error: any) {
console.error('Errore caricamento widget trasporti:', error); console.error('Errore caricamento widget trasporti:', error);
emit('error', error); emit('error', error);
// Imposta valori di default in caso di errore
stats.value = { offers: 0, requests: 0, matches: 0 };
recentRides.value = [];
myActiveRides.value = [];
pendingRequests.value = 0;
unreadMessages.value = 0;
} finally { } finally {
loading.value = false; loading.value = false;
} }
@@ -95,13 +131,16 @@ export default defineComponent({
const loadStats = async () => { const loadStats = async () => {
try { try {
const response = await Api.SendReqWithData('/api/viaggi/stats/summary', 'GET'); // Carica solo le statistiche rapide (più leggero)
const response = await Api.SendReqWithData('/api/viaggi/widget/stats', 'GET');
if (response.success) { if (response.success && response.data) {
stats.value = response.data; // Aggiorna solo alcuni dati
pendingRequests.value = response.data.pendingRequests || 0;
unreadMessages.value = response.data.unreadMessages || 0;
} }
} catch (error) { } catch (error) {
console.error('Errore caricamento stats:', error); console.error('Errore caricamento stats:', error);
} }
}; };
@@ -141,7 +180,7 @@ export default defineComponent({
'Baratto': '🤝', 'Baratto': '🤝',
'Scambio Lavoro': '💪', 'Scambio Lavoro': '💪',
'Monete Alternative': '🪙', 'Monete Alternative': '🪙',
'RIS': '🍚', 'RIS': '🌟',
'Euro': '💶', 'Euro': '💶',
'Bitcoin': '₿', 'Bitcoin': '₿',
'Banca del Tempo': '⏳' 'Banca del Tempo': '⏳'
@@ -180,10 +219,11 @@ export default defineComponent({
// Navigation // Navigation
const goToCreate = (type: 'offer' | 'request') => { const goToCreate = (type: 'offer' | 'request') => {
router.push({ if (type === 'offer') {
path: '/viaggi/crea', router.push('/viaggi/offri');
query: { type } } else {
}); router.push('/viaggi/richiedi');
}
}; };
const goToList = () => { const goToList = () => {
@@ -195,7 +235,7 @@ export default defineComponent({
}; };
const goToMyRides = () => { const goToMyRides = () => {
router.push('/viaggi/rides/my'); router.push('/viaggi/miei-viaggi');
}; };
const goToSearch = () => { const goToSearch = () => {
@@ -238,14 +278,22 @@ export default defineComponent({
// Lifecycle // Lifecycle
onMounted(() => { onMounted(() => {
loadStats(); // Carica dati iniziali
if (props.defaultExpanded) { if (props.defaultExpanded) {
loadWidgetData(); loadWidgetData();
} else {
loadStats();
} }
// Avvia auto-refresh
startAutoRefresh(); startAutoRefresh();
}); });
// Cleanup onUnmounted(() => {
stopAutoRefresh();
});
// Watch per cambio interval
watch(() => props.refreshInterval, () => { watch(() => props.refreshInterval, () => {
stopAutoRefresh(); stopAutoRefresh();
startAutoRefresh(); startAutoRefresh();

View File

@@ -0,0 +1,366 @@
// composables/useCommunityRides.ts
import { ref, computed, watch } from 'vue';
import { Api } from '@api';
export interface CommunityStats {
total: number;
thisWeek: number;
next7Days: number;
totalSeatsAvailable: number;
byDayOfWeek: Array<{
date: string;
dayName: string;
count: number;
rides: string[];
}>;
popularRoutes: Array<{
departure: string;
destination: string;
count: number;
avgPrice: string | null;
availableSeats: number;
}>;
popularDestinations: Array<{
destination: Location;
count: number;
}>;
}
export interface CommunityRide {
_id: string;
userId: {
_id: string;
name: string;
surname: string;
username?: string;
profile?: { img?: string };
stats?: {
averageRating?: number;
totalRides?: number;
completedRides?: number;
};
};
vehicleId?: {
_id: string;
brand: string;
model: string;
plate: string;
type?: string;
};
departure: string;
destination: string;
departureDate: string;
_displayDate: string;
price: number;
passengers: {
total: number;
available: number;
};
preferences?: {
smoking?: boolean;
pets?: boolean;
music?: boolean;
chatter?: string;
};
notes?: string;
status: string;
userRequestStatus?: 'pending' | 'accepted' | 'rejected' | null;
}
export interface CommunityFilters {
departure: string;
destination: string;
dateFrom: string;
dateTo: string;
maxPrice: number | null;
minSeats: number | null;
sortBy: 'date' | 'price_asc' | 'price_desc' | 'seats' | 'rating';
vehicleType?: string | null;
}
export interface CalendarDate {
date: string;
count: number;
totalSeats: number;
rides: Array<{
_id: string;
departure: string;
destination: string;
time: string;
price: number;
availableSeats: number;
driver: any;
}>;
}
export interface CalendarData {
month: number;
year: number;
ridesByDate: Record<string, CalendarDate>;
totalRides: number;
datesWithRides: number;
}
const STOR_TRASP_COMMRIDE = 'trasp_commride_filter';
const loadFiltersFromStorage = (): Partial<CommunityFilters> => {
try {
const stored = localStorage.getItem(STOR_TRASP_COMMRIDE);
if (stored) return JSON.parse(stored);
} catch (error) {
console.error('Errore caricamento filtri:', error);
}
return {};
};
const filters = ref<CommunityFilters>({
departure: '',
destination: '',
dateFrom: '',
dateTo: '',
maxPrice: null,
minSeats: null,
sortBy: 'date',
...loadFiltersFromStorage(),
});
// Watch A LIVELLO DI MODULO (FUORI da useCommunityRides)
watch(
filters,
(newFilters) => {
try {
localStorage.setItem(STOR_TRASP_COMMRIDE, JSON.stringify(newFilters));
} catch (error) {
console.error('Errore salvataggio filtri:', error);
}
},
{ deep: true }
);
export function useCommunityRides() {
// State
const loading = ref(false);
const stats = ref<CommunityStats | null>(null);
const rides = ref<CommunityRide[]>([]);
const calendarData = ref<CalendarData | null>(null);
const pagination = ref({
page: 1,
limit: 20,
total: 0,
pages: 0,
hasMore: false,
});
// Computed
const hasFilters = computed(() => {
return !!(
filters.value.departure ||
filters.value.destination ||
filters.value.dateFrom ||
filters.value.dateTo ||
filters.value.maxPrice ||
filters.value.minSeats
);
});
const filteredRidesCount = computed(() => rides.value.length);
// Methods
const fetchStatsComm = async () => {
loading.value = true;
try {
const response = await Api.SendReqWithData('/api/viaggi/rides/statscomm', 'GET');
if (response.success) {
stats.value = response.data;
}
return response;
} catch (error: any) {
console.error('Error fetching community stats:', error);
throw error;
} finally {
loading.value = false;
}
};
const fetchCommunityRides = async (page = 1, resetList = true) => {
loading.value = true;
try {
// Build query params
const params = new URLSearchParams({
page: page.toString(),
limit: pagination.value.limit.toString(),
});
if (filters.value.departure) {
params.append('departure', filters.value.departure);
}
if (filters.value.destination) {
params.append('destination', filters.value.destination);
}
if (filters.value.dateFrom) {
params.append('dateFrom', filters.value.dateFrom);
}
if (filters.value.dateTo) {
params.append('dateTo', filters.value.dateTo);
}
if (filters.value.maxPrice) {
params.append('maxPrice', filters.value.maxPrice.toString());
}
if (filters.value.minSeats) {
params.append('minSeats', filters.value.minSeats.toString());
}
if (filters.value.sortBy) {
params.append('sortBy', filters.value.sortBy);
}
if (filters.value.vehicleType) {
params.append('vehicleType', filters.value.vehicleType);
}
const response = await Api.SendReqWithData(
`/api/viaggi/rides/community?${params.toString()}`,
'GET'
);
if (response.success) {
if (resetList) {
rides.value = response.data.rides;
} else {
rides.value.push(...response.data.rides);
}
pagination.value = response.data.pagination;
}
return response;
} catch (error: any) {
console.error('Error fetching community rides:', error);
throw error;
} finally {
loading.value = false;
}
};
const fetchCalendarData = async (month?: number, year?: number) => {
loading.value = true;
try {
const params = new URLSearchParams();
if (month !== undefined) params.append('month', month.toString());
if (year !== undefined) params.append('year', year.toString());
const response = await Api.SendReqWithData(
`/api/viaggi/rides/calendar?${params.toString()}`,
'GET'
);
if (response.success) {
calendarData.value = response.data;
}
return response;
} catch (error: any) {
console.error('Error fetching calendar data:', error);
throw error;
} finally {
loading.value = false;
}
};
const loadMore = async () => {
if (!pagination.value.hasMore || loading.value) return;
await fetchCommunityRides(pagination.value.page + 1, false);
};
const applyFilters = async (newFilters: Partial<CommunityFilters>) => {
filters.value = { ...filters.value, ...newFilters };
pagination.value.page = 1;
await fetchCommunityRides(1, true);
};
const clearFilters = async () => {
filters.value = {
departure: '',
destination: '',
dateFrom: '',
dateTo: '',
maxPrice: null,
minSeats: null,
sortBy: 'date',
};
pagination.value.page = 1;
await fetchCommunityRides(1, true);
};
const requestRide = async (rideId: string, message?: string) => {
try {
const response = await Api.SendReqWithData(`/api/viaggi/ride/request`, 'POST', {
rideId,
message,
});
if (response.success) {
// Update the ride's request status
const rideIndex = rides.value.findIndex((r) => r._id === rideId);
if (rideIndex !== -1) {
rides.value[rideIndex].userRequestStatus = 'pending';
}
}
return response;
} catch (error: any) {
console.error('Error requesting ride:', error);
throw error;
}
};
const cancelRideRequest = async (requestId: string, rideId: string) => {
try {
const response = await Api.SendReqWithData(
`/api/viaggi/ride/${requestId}/request`,
'DELETE'
);
if (response.success) {
// Update the ride's request status
const rideIndex = rides.value.findIndex((r) => r._id === rideId);
if (rideIndex !== -1) {
rides.value[rideIndex].userRequestStatus = null;
}
}
return response;
} catch (error: any) {
console.error('Error cancelling request:', error);
throw error;
}
};
return {
// State
loading,
stats,
rides,
calendarData,
pagination,
filters,
// Computed
hasFilters,
filteredRidesCount,
// Methods
fetchStatsComm,
fetchCommunityRides,
fetchCalendarData,
loadMore,
applyFilters,
clearFilters,
requestRide,
cancelRideRequest,
};
}

View File

@@ -162,7 +162,7 @@ export function useContribTypes() {
const type = findById(contribTypeId); const type = findById(contribTypeId);
if (!type) return false; if (!type) return false;
const noPriceTypes = ['dono', 'baratto', 'scambio lavoro']; const noPriceTypes = ['dono', 'Offerta Libera', 'baratto', 'scambio lavoro'];
return !noPriceTypes.includes(type.label.toLowerCase()); return !noPriceTypes.includes(type.label.toLowerCase());
}; };

View File

@@ -0,0 +1,284 @@
// composables/useNotifications.ts
import { ref } from 'vue';
import { Api } from '@api';
export interface NotificationChannel {
enabled: boolean;
newRideRequest: boolean;
requestAccepted: boolean;
requestRejected: boolean;
rideReminder24h: boolean;
rideReminder2h: boolean;
rideCancelled: boolean;
newMessage: boolean;
}
export interface EmailNotifications extends NotificationChannel {
newCommunityRide: boolean;
weeklyDigest: boolean;
}
export interface TelegramNotifications extends NotificationChannel {
chatId: string | null;
username: string | null;
}
export interface PushNotifications extends NotificationChannel {
subscription: any;
newCommunityRide: boolean;
}
export interface NotificationPreferences {
email: EmailNotifications;
telegram: TelegramNotifications;
push: PushNotifications;
}
export function useNotifications() {
// State
const loading = ref(false);
const preferences = ref<NotificationPreferences | null>(null);
const testingSending = ref<string | null>(null);
// Methods
const fetchPreferences = async () => {
loading.value = true;
try {
const response = await Api.SendReqWithData(
'/api/viaggi/notifications/preferences',
'GET'
);
if (response.success) {
preferences.value = response.data;
}
return response;
} catch (error: any) {
console.error('Error fetching notification preferences:', error);
throw error;
} finally {
loading.value = false;
}
};
const updatePreferences = async (updates: Partial<NotificationPreferences>) => {
loading.value = true;
try {
const response = await Api.SendReqWithData(
'/api/viaggi/notifications/preferences',
'PUT',
updates
);
if (response.success) {
preferences.value = response.data;
}
return response;
} catch (error: any) {
console.error('Error updating notification preferences:', error);
throw error;
} finally {
loading.value = false;
}
};
const connectTelegram = async (chatId: string, username?: string) => {
loading.value = true;
try {
const response = await Api.SendReqWithData(
'/api/viaggi/notifications/telegram/connect',
'POST',
{ chatId, username }
);
if (response.success) {
// Update local preferences
if (preferences.value) {
preferences.value.telegram.chatId = chatId;
preferences.value.telegram.username = username || null;
preferences.value.telegram.enabled = true;
}
}
return response;
} catch (error: any) {
console.error('Error connecting Telegram:', error);
throw error;
} finally {
loading.value = false;
}
};
const disconnectTelegram = async () => {
loading.value = true;
try {
const response = await Api.SendReqWithData(
'/api/viaggi/notifications/telegram/disconnect',
'DELETE'
);
if (response.success) {
// Update local preferences
if (preferences.value) {
preferences.value.telegram.chatId = null;
preferences.value.telegram.username = null;
preferences.value.telegram.enabled = false;
}
}
return response;
} catch (error: any) {
console.error('Error disconnecting Telegram:', error);
throw error;
} finally {
loading.value = false;
}
};
const subscribePush = async (subscription: any) => {
loading.value = true;
try {
const response = await Api.SendReqWithData(
'/api/viaggi/notifications/push/subscribe',
'POST',
{ subscription }
);
if (response.success) {
// Update local preferences
if (preferences.value) {
preferences.value.push.subscription = subscription;
preferences.value.push.enabled = true;
}
}
return response;
} catch (error: any) {
console.error('Error subscribing to push notifications:', error);
throw error;
} finally {
loading.value = false;
}
};
const unsubscribePush = async () => {
loading.value = true;
try {
const response = await Api.SendReqWithData(
'/api/viaggi/notifications/push/unsubscribe',
'DELETE'
);
if (response.success) {
// Update local preferences
if (preferences.value) {
preferences.value.push.subscription = null;
preferences.value.push.enabled = false;
}
}
return response;
} catch (error: any) {
console.error('Error unsubscribing from push notifications:', error);
throw error;
} finally {
loading.value = false;
}
};
const sendTestNotification = async (type: 'email' | 'telegram' | 'push') => {
testingSending.value = type;
try {
const response = await Api.SendReqWithData(
'/api/viaggi/notifications/test',
'POST',
{ type }
);
return response;
} catch (error: any) {
console.error('Error sending test notification:', error);
throw error;
} finally {
testingSending.value = null;
}
};
// Web Push Helper
const requestPushPermission = async (): Promise<PushSubscription | null> => {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
throw new Error('Push notifications non supportate dal browser');
}
// Request permission
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
throw new Error('Permesso per le notifiche negato');
}
// Register service worker
const registration = await navigator.serviceWorker.register('/service-worker.js');
// Subscribe to push
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
import.meta.env.VITE_VAPID_PUBLIC_KEY
)
});
return subscription;
};
// Telegram Deep Link Generator
const getTelegramBotLink = (startParam?: string): string => {
const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME;
const baseUrl = `https://t.me/${botUsername}`;
return startParam ? `${baseUrl}?start=${startParam}` : baseUrl;
};
return {
// State
loading,
preferences,
testingSending,
// Methods
fetchPreferences,
updatePreferences,
connectTelegram,
disconnectTelegram,
subscribePush,
unsubscribePush,
sendTestNotification,
requestPushPermission,
getTelegramBotLink
};
}
// Utility function for VAPID key conversion
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}

View File

@@ -11,7 +11,7 @@ interface RecentCity {
type: 'search' | 'trip'; type: 'search' | 'trip';
} }
const STORAGE_KEY = 'trasporti_recent_cities'; const STOR_TRASP_CITIE = 'trasp_recent_cities';
const MAX_RECENT = 10; const MAX_RECENT = 10;
export function useRecentCities() { export function useRecentCities() {
@@ -22,7 +22,7 @@ export function useRecentCities() {
*/ */
const loadRecent = (): void => { const loadRecent = (): void => {
try { try {
const stored = localStorage.getItem(STORAGE_KEY); const stored = localStorage.getItem(STOR_TRASP_CITIE);
if (stored) { if (stored) {
recentCities.value = JSON.parse(stored); recentCities.value = JSON.parse(stored);
} }
@@ -37,7 +37,7 @@ export function useRecentCities() {
*/ */
const saveRecent = (): void => { const saveRecent = (): void => {
try { try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(recentCities.value)); localStorage.setItem(STOR_TRASP_CITIE, JSON.stringify(recentCities.value));
} catch (error) { } catch (error) {
console.error('Error saving recent cities:', error); console.error('Error saving recent cities:', error);
} }
@@ -137,7 +137,7 @@ export function useRecentCities() {
*/ */
const clearRecent = (): void => { const clearRecent = (): void => {
recentCities.value = []; recentCities.value = [];
localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STOR_TRASP_CITIE);
}; };
/** /**

View File

@@ -0,0 +1,56 @@
// composables/useRideNotifications.ts
import { ref } from 'vue';
import { useQuasar } from 'quasar';
import { CommunityFilters } from './useCommunityRides';
import { Api } from 'app/src/store/Api';
export function useRideNotifications() {
const $q = useQuasar();
const lastCheckTime = ref<string | null>(null);
const checkNewRides = async (filters: CommunityFilters) => {
// Get last check time from localStorage
const lastCheck = localStorage.getItem('last_rides_check');
if (!lastCheck) {
// First time, just save current time
localStorage.setItem('last_rides_check', new Date().toISOString());
return;
}
try {
// Fetch rides created after lastCheck
const response = await Api.SendReqWithData(
`/api/viaggi/rides/community?createdAfter=${lastCheck}`,
'GET'
);
if (response.success && response.data.rides.length > 0) {
$q.notify({
type: 'info',
message: `${response.data.rides.length} nuovi viaggi disponibili!`,
icon: 'notifications',
actions: [
{
label: 'Vedi',
color: 'white',
handler: () => {
// Navigate to list view
}
}
],
timeout: 0
});
}
// Update last check time
localStorage.setItem('last_rides_check', new Date().toISOString());
} catch (error) {
console.error('Error checking new rides:', error);
}
};
return {
checkNewRides
};
}

View File

@@ -14,6 +14,8 @@ import type {
Location, Location,
} from '../types'; } from '../types';
const STOR_TRASP_CITIE = 'trasp_ride_filters';
// ============================================================ // ============================================================
// STATE // STATE
// ============================================================ // ============================================================
@@ -41,13 +43,45 @@ const pagination = reactive({
pages: 0, pages: 0,
}); });
const filters = reactive<RideSearchFilters>({ // Carica filtri da localStorage
type: undefined, const loadFiltersFromStorage = (): RideSearchFilters => {
from: '', try {
to: '', const stored = localStorage.getItem(STOR_TRASP_CITIE);
date: '', if (stored) {
seats: 1, return JSON.parse(stored);
}); }
} catch (error) {
console.error('Errore caricamento filtri da localStorage:', error);
}
// Default
return {
type: undefined,
from: '',
to: '',
date: '',
seats: 1,
};
};
const filters = reactive<RideSearchFilters>(loadFiltersFromStorage());
// ------------------------------------------------------------
// WATCHERS
// ------------------------------------------------------------
// Salva filtri in localStorage quando cambiano
watch(
filters,
(newFilters) => {
try {
localStorage.setItem(STOR_TRASP_CITIE, JSON.stringify(newFilters));
} catch (error) {
console.error('Errore salvataggio filtri in localStorage:', error);
}
},
{ deep: true }
);
// ============================================================ // ============================================================
// COMPOSABLE // COMPOSABLE
@@ -140,7 +174,7 @@ export function useRides() {
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nel recupero dei viaggi'; error.value = err.data?.message || err.message || 'Errore nel recupero dei viaggi';
rides.value = []; // ✅ Reset to empty array on error rides.value = []; // ✅ Reset to empty array on error
throw err; throw err;
} finally { } finally {
@@ -182,7 +216,7 @@ export function useRides() {
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nella ricerca'; error.value = err.data?.message || err.message || 'Errore nella ricerca';
rides.value = []; // ✅ Reset to empty array on error rides.value = []; // ✅ Reset to empty array on error
throw err; throw err;
} finally { } finally {
@@ -211,7 +245,7 @@ export function useRides() {
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nel recupero del viaggio'; error.value = err.data?.message || err.message || 'Errore nel recupero del viaggio';
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -240,7 +274,8 @@ export function useRides() {
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nella creazione del viaggio'; error.value =
err.data?.message || err.message || 'Errore nella creazione del viaggio';
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -274,7 +309,8 @@ export function useRides() {
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||"Errore nell'aggiornamento del viaggio"; error.value =
err.data?.message || err.message || "Errore nell'aggiornamento del viaggio";
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -289,9 +325,13 @@ export function useRides() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = (await Api.SendReqWithData(`/api/viaggi/rides/${rideId}`, 'DELETE', { const response = (await Api.SendReqWithData(
reason, `/api/viaggi/rides/${rideId}`,
})) as ApiResponse<void>; 'DELETE',
{
reason,
}
)) as ApiResponse<void>;
if (response.success) { if (response.success) {
// Rimuovi dalla lista // Rimuovi dalla lista
@@ -303,7 +343,8 @@ export function useRides() {
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nella cancellazione del viaggio'; error.value =
err.data?.message || err.message || 'Errore nella cancellazione del viaggio';
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -335,7 +376,8 @@ export function useRides() {
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nel completamento del viaggio'; error.value =
err.data?.message || err.message || 'Errore nel completamento del viaggio';
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -375,7 +417,8 @@ export function useRides() {
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nel recupero dei tuoi viaggi'; error.value =
err.data?.message || err.message || 'Errore nel recupero dei tuoi viaggi';
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -417,6 +460,9 @@ export function useRides() {
} }
}; };
/**
* Reset filtri
*/
/** /**
* Reset filtri * Reset filtri
*/ */
@@ -427,6 +473,13 @@ export function useRides() {
filters.date = ''; filters.date = '';
filters.seats = 1; filters.seats = 1;
filters.passingThrough = ''; filters.passingThrough = '';
// Rimuovi da localStorage
try {
localStorage.removeItem(STOR_TRASP_CITIE);
} catch (error) {
console.error('Errore rimozione filtri da localStorage:', error);
}
}; };
/** /**
@@ -523,7 +576,7 @@ export function useRides() {
* Verifica se il viaggio è nel passato * Verifica se il viaggio è nel passato
*/ */
const isPastRide = (ride: Ride) => { const isPastRide = (ride: Ride) => {
return new Date(ride.dateTime) < new Date(); return new Date(ride.departureDate) < new Date();
}; };
/** /**
@@ -538,6 +591,20 @@ export function useRides() {
return true; return true;
}; };
// Aggiungi il metodo
const fetchCancelledRides = async (page = 1, limit = 20) => {
try {
const response = await Api.SendReqWithData(
`/api/viaggi/rides/cancelled?page=${page}&limit=${limit}`,
'GET'
);
return response;
} catch (error: any) {
console.error('Error fetching cancelled rides:', error);
throw error;
}
};
// ------------------------------------------------------------ // ------------------------------------------------------------
// RETURN // RETURN
// ------------------------------------------------------------ // ------------------------------------------------------------
@@ -582,5 +649,6 @@ export function useRides() {
getStatusLabel, getStatusLabel,
isPastRide, isPastRide,
canBook, canBook,
fetchCancelledRides,
}; };
} }

View File

@@ -0,0 +1,50 @@
// composables/useSavedFilters.ts
import { ref } from 'vue';
interface SavedFilter {
id: string;
name: string;
filters: CommunityFilters;
createdAt: string;
}
export function useSavedFilters() {
const savedFilters = ref<SavedFilter[]>([]);
const loadFromStorage = () => {
const stored = localStorage.getItem('saved_filters');
if (stored) {
savedFilters.value = JSON.parse(stored);
}
};
const saveFilter = (name: string, filters: CommunityFilters) => {
const newFilter: SavedFilter = {
id: Date.now().toString(),
name,
filters,
createdAt: new Date().toISOString()
};
savedFilters.value.push(newFilter);
localStorage.setItem('saved_filters', JSON.stringify(savedFilters.value));
};
const deleteFilter = (id: string) => {
savedFilters.value = savedFilters.value.filter(f => f.id !== id);
localStorage.setItem('saved_filters', JSON.stringify(savedFilters.value));
};
const applyFilter = (id: string) => {
const filter = savedFilters.value.find(f => f.id === id);
return filter?.filters || null;
};
return {
savedFilters,
loadFromStorage,
saveFilter,
deleteFilter,
applyFilter
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ import { useDriverProfile } from '../composables/useDriverProfile';
import { useChat } from '../composables/useChat'; import { useChat } from '../composables/useChat';
import RideCard from '../components/ride/RideCard.vue'; import RideCard from '../components/ride/RideCard.vue';
import FeedbackList from '../components/feedback/FeedbackList.vue'; import FeedbackList from '../components/feedback/FeedbackList.vue';
import type { DriverPublicProfile } from '../types'; import { VEHICLE_TYPES } from '../types/viaggi.types';
import { useUserStore } from 'app/src/store'; import { useUserStore } from 'app/src/store';
export default defineComponent({ export default defineComponent({
@@ -108,14 +108,8 @@ export default defineComponent({
}; };
const getVehicleIcon = (type?: string): string => { const getVehicleIcon = (type?: string): string => {
const icons: Record<string, string> = { const vehicleType = VEHICLE_TYPES.find((vt) => vt.value === type);
auto: '🚗', return vehicleType ? vehicleType.icon : ' ';
moto: '🏍️',
furgone: '🚐',
minibus: '🚌',
altro: '🚙'
};
return icons[type || 'auto'] || '🚗';
}; };
const formatBadgeName = (name: string): string => { const formatBadgeName = (name: string): string => {

View File

@@ -20,12 +20,13 @@ export default defineComponent({
const router = useRouter(); const router = useRouter();
const $q = useQuasar(); const $q = useQuasar();
const userStore = useUserStore() const userStore = useUserStore();
const { const {
myRides, myRides,
loading, loading,
fetchMyRides, fetchMyRides,
fetchCancelledRides,
deleteRide, deleteRide,
completeRideApi, completeRideApi,
} = useRides(); } = useRides();
@@ -50,6 +51,15 @@ export default defineComponent({
const selectedRide = ref<Ride | null>(null); const selectedRide = ref<Ride | null>(null);
const selectedRideRequests = ref<RideRequest[]>([]); const selectedRideRequests = ref<RideRequest[]>([]);
const currentUserId = ref<string>(userStore.my._id); const currentUserId = ref<string>(userStore.my._id);
const cancelledRides = ref<Ride[]>([]);
const loadingCancelled = ref(false);
const cancelledPagination = ref({
page: 1,
hasMore: false
});
// Badge for new community rides
const newCommunityRidesCount = ref(0);
// Filters // Filters
const roleFilters = [ const roleFilters = [
@@ -61,6 +71,7 @@ export default defineComponent({
// Computed // Computed
const upcomingCount = computed(() => myRides.upcoming.length); const upcomingCount = computed(() => myRides.upcoming.length);
const pendingRequestsCount = computed(() => requestCounts.value.pending); const pendingRequestsCount = computed(() => requestCounts.value.pending);
const cancelledCount = computed(() => cancelledRides.value.length);
const filteredUpcoming = computed(() => { const filteredUpcoming = computed(() => {
if (roleFilter.value === 'all') return myRides.upcoming; if (roleFilter.value === 'all') return myRides.upcoming;
@@ -78,6 +89,14 @@ export default defineComponent({
return myRides.past.filter(r => !isDriver(r)); return myRides.past.filter(r => !isDriver(r));
}); });
const filteredCancelled = computed(() => {
if (roleFilter.value === 'all') return cancelledRides.value;
if (roleFilter.value === 'driver') {
return cancelledRides.value.filter(r => isDriver(r));
}
return cancelledRides.value.filter(r => !isDriver(r));
});
// Methods // Methods
const isDriver = (ride: Ride): boolean => { const isDriver = (ride: Ride): boolean => {
const userId = typeof ride.userId === 'string' ? ride.userId : ride.userId._id; const userId = typeof ride.userId === 'string' ? ride.userId : ride.userId._id;
@@ -92,12 +111,15 @@ export default defineComponent({
}; };
const canLeaveFeedback = (ride: Ride): boolean => { const canLeaveFeedback = (ride: Ride): boolean => {
// TODO: Implement logic to check if user can leave feedback
return ride.status === 'completed'; return ride.status === 'completed';
}; };
const goToCreate = () => { const goToCreate = () => {
router.push({ path: '/viaggi/richiedi' }); router.push({ path: '/viaggi/crea' });
};
const goToCommunity = () => {
router.push({ path: '/viaggi/community?view=report' });
}; };
const goToRide = (rideId: string) => { const goToRide = (rideId: string) => {
@@ -127,6 +149,10 @@ export default defineComponent({
await deleteRide(ride._id, reason); await deleteRide(ride._id, reason);
$q.notify({ type: 'positive', message: 'Viaggio cancellato' }); $q.notify({ type: 'positive', message: 'Viaggio cancellato' });
await fetchMyRides(); await fetchMyRides();
// Reload cancelled rides if on that tab
if (activeTab.value === 'cancelled') {
await loadCancelledRides();
}
} catch (error: any) { } catch (error: any) {
$q.notify({ type: 'negative', message: error.data?.message || error.message }); $q.notify({ type: 'negative', message: error.data?.message || error.message });
} }
@@ -159,7 +185,7 @@ export default defineComponent({
}; };
const openFeedbackDialog = (ride: Ride) => { const openFeedbackDialog = (ride: Ride) => {
router.push(`/viaggi/feedback/viaggio/${ride._id}`); router.push(`/viaggi/feedback/viaggi/${ride._id}`);
}; };
const acceptRequest = async (request: RideRequest) => { const acceptRequest = async (request: RideRequest) => {
@@ -227,6 +253,38 @@ export default defineComponent({
return user.username || 'Utente'; return user.username || 'Utente';
}; };
const loadCancelledRides = async (page = 1) => {
loadingCancelled.value = true;
try {
const response = await fetchCancelledRides(page);
if (response.success) {
if (page === 1) {
cancelledRides.value = response.data.rides;
} else {
cancelledRides.value.push(...response.data.rides);
}
cancelledPagination.value = {
page: response.data.pagination.page,
hasMore: response.data.pagination.hasMore
};
}
} catch (error: any) {
$q.notify({
type: 'negative',
message: 'Errore nel caricamento dei viaggi cancellati'
});
} finally {
loadingCancelled.value = false;
}
};
const loadMoreCancelled = async () => {
if (!cancelledPagination.value.hasMore || loadingCancelled.value) return;
await loadCancelledRides(cancelledPagination.value.page + 1);
};
// Watch tab changes // Watch tab changes
watch(activeTab, async (tab) => { watch(activeTab, async (tab) => {
if (tab === 'requests') { if (tab === 'requests') {
@@ -235,6 +293,8 @@ export default defineComponent({
} else { } else {
await fetchSentRequests(); await fetchSentRequests();
} }
} else if (tab === 'cancelled' && cancelledRides.value.length === 0) {
await loadCancelledRides();
} }
}); });
@@ -250,6 +310,9 @@ export default defineComponent({
onMounted(async () => { onMounted(async () => {
await fetchMyRides(); await fetchMyRides();
await fetchReceivedRequests(); await fetchReceivedRequests();
// TODO: Fetch new community rides count for badge
// This could be done with a separate API call or WebSocket
}); });
return { return {
@@ -261,10 +324,14 @@ export default defineComponent({
selectedRideRequests, selectedRideRequests,
loading, loading,
loadingRequests, loadingRequests,
loadingCancelled,
myRides, myRides,
receivedRequests, receivedRequests,
sentRequests, sentRequests,
currentUserId, currentUserId,
cancelledRides,
cancelledPagination,
newCommunityRidesCount,
// Filters // Filters
roleFilters, roleFilters,
@@ -272,14 +339,17 @@ export default defineComponent({
// Computed // Computed
upcomingCount, upcomingCount,
pendingRequestsCount, pendingRequestsCount,
cancelledCount,
filteredUpcoming, filteredUpcoming,
filteredPast, filteredPast,
filteredCancelled,
// Methods // Methods
isDriver, isDriver,
getPendingRequests, getPendingRequests,
canLeaveFeedback, canLeaveFeedback,
goToCreate, goToCreate,
goToCommunity,
goToRide, goToRide,
goToProfile, goToProfile,
editRide, editRide,
@@ -289,7 +359,8 @@ export default defineComponent({
openFeedbackDialog, openFeedbackDialog,
acceptRequest, acceptRequest,
rejectRequest, rejectRequest,
cancelRequest cancelRequest,
loadMoreCancelled
}; };
} }
}); });

View File

@@ -1,221 +1,279 @@
<!-- MyRidesPage.vue - TEMPLATE UPDATE -->
<template> <template>
<q-page class="my-rides-page"> <q-page class="my-rides-page">
<div class="my-rides-page__container"> <!-- Header -->
<!-- Header --> <div class="my-rides-page__header">
<div class="my-rides-page__header"> <h1>I Miei Viaggi</h1>
<h1 class="my-rides-page__title">I Miei Viaggi</h1> <div class="my-rides-page__header-actions">
<q-btn <q-btn
color="primary" flat
round
icon="explore"
color="white"
@click="goToCommunity"
>
<q-badge v-if="newCommunityRidesCount > 0" color="red" floating>
{{ newCommunityRidesCount }}
</q-badge>
<q-tooltip>Esplora viaggi community</q-tooltip>
</q-btn>
<q-btn
round
icon="add" icon="add"
label="Nuovo Viaggio" color="white"
rounded text-color="primary"
unelevated
@click="goToCreate" @click="goToCreate"
/> />
</div> </div>
</div>
<!-- Tabs --> <!-- Tabs -->
<q-tabs <q-tabs
v-model="activeTab" v-model="activeTab"
class="my-rides-page__tabs" class="my-rides-page__tabs"
active-color="primary" active-color="primary"
indicator-color="primary" indicator-color="primary"
align="left" align="justify"
narrow-indicator dense
no-caps >
> <q-tab name="upcoming" icon="upcoming" label="Prossimi">
<q-tab name="upcoming" icon="event"> <q-badge v-if="upcomingCount > 0" color="primary" floating>
<span class="q-ml-sm">In arrivo</span> {{ upcomingCount }}
<q-badge v-if="upcomingCount > 0" color="primary" floating> </q-badge>
{{ upcomingCount }} </q-tab>
</q-badge> <q-tab name="requests" icon="notifications" label="Richieste">
</q-tab> <q-badge v-if="pendingRequestsCount > 0" color="orange" floating>
<q-tab name="past" icon="history"> {{ pendingRequestsCount }}
<span class="q-ml-sm">Passati</span> </q-badge>
</q-tab> </q-tab>
<q-tab name="requests" icon="inbox"> <q-tab name="past" icon="history" label="Passati" />
<span class="q-ml-sm">Richieste</span> <q-tab name="cancelled" icon="cancel" label="Cancellati">
<q-badge v-if="pendingRequestsCount > 0" color="negative" floating> <q-badge v-if="cancelledCount > 0" color="grey" floating>
{{ pendingRequestsCount }} {{ cancelledCount }}
</q-badge> </q-badge>
</q-tab> </q-tab>
</q-tabs> </q-tabs>
<!-- Filter Pills --> <!-- Role Filter (shown for upcoming, past, cancelled) -->
<div class="my-rides-page__filters"> <div
<q-chip v-if="activeTab !== 'requests'"
v-for="filter in roleFilters" class="my-rides-page__filter"
:key="filter.value" >
:selected="roleFilter === filter.value" <q-select
:color="roleFilter === filter.value ? 'primary' : undefined" v-model="roleFilter"
:text-color="roleFilter === filter.value ? 'white' : undefined" :options="roleFilters"
clickable option-value="value"
@click="roleFilter = filter.value" option-label="label"
> emit-value
{{ filter.label }} map-options
</q-chip> outlined
</div> dense
label="Filtra per ruolo"
/>
</div>
<!-- Tab Panels --> <!-- Tab Panels -->
<q-tab-panels v-model="activeTab" animated class="my-rides-page__panels"> <q-tab-panels v-model="activeTab" animated class="my-rides-page__panels">
<!-- Upcoming Rides -->
<q-tab-panel name="upcoming" class="q-pa-none">
<div v-if="loading" class="my-rides-page__loading">
<q-skeleton v-for="i in 3" :key="i" type="rect" height="180px" class="q-mb-md" />
</div>
<div v-else-if="filteredUpcoming.length === 0" class="my-rides-page__empty"> <!-- UPCOMING TAB -->
<div class="my-rides-page__empty-icon">📅</div> <q-tab-panel name="upcoming" class="my-rides-page__panel">
<h3>Nessun viaggio in programma</h3> <div v-if="loading" class="my-rides-page__loading">
<p>I tuoi prossimi viaggi appariranno qui</p> <q-spinner-dots size="50px" color="primary" />
<q-btn color="primary" label="Crea un viaggio" @click="goToCreate" /> </div>
</div>
<div v-else class="my-rides-page__list"> <div v-else-if="filteredUpcoming.length === 0" class="my-rides-page__empty">
<TransitionGroup name="list"> <q-icon name="event_busy" size="80px" color="grey-4" />
<MyRideCard <h3>Nessun viaggio programmato</h3>
v-for="ride in filteredUpcoming" <p>Inizia a condividere i tuoi viaggi o cerca un passaggio</p>
:key="ride._id" <div class="my-rides-page__empty-actions">
:ride="ride" <q-btn
:is-driver="isDriver(ride)" color="primary"
:pending-requests="getPendingRequests(ride._id)" icon="add"
@click="goToRide(ride._id)" label="Crea viaggio"
@manage-requests="openRequestsDialog(ride)" rounded
@cancel="cancelRide(ride)" unelevated
@complete="completeRide(ride)" @click="goToCreate"
@edit="editRide(ride._id)" />
/> <q-btn
</TransitionGroup> color="secondary"
</div> icon="explore"
</q-tab-panel> label="Esplora community"
rounded
<!-- Past Rides --> outline
<q-tab-panel name="past" class="q-pa-none"> @click="goToCommunity"
<div v-if="loading" class="my-rides-page__loading">
<q-skeleton v-for="i in 3" :key="i" type="rect" height="180px" class="q-mb-md" />
</div>
<div v-else-if="filteredPast.length === 0" class="my-rides-page__empty">
<div class="my-rides-page__empty-icon">🛣</div>
<h3>Nessun viaggio passato</h3>
<p>I viaggi completati appariranno qui</p>
</div>
<div v-else class="my-rides-page__list">
<MyRideCard
v-for="ride in filteredPast"
:key="ride._id"
:ride="ride"
:is-driver="isDriver(ride)"
:show-feedback-prompt="canLeaveFeedback(ride)"
@click="goToRide(ride._id)"
@leave-feedback="openFeedbackDialog(ride)"
/> />
</div> </div>
</q-tab-panel> </div>
<!-- Requests --> <div v-else class="my-rides-page__list">
<q-tab-panel name="requests" class="q-pa-none"> <MyRideCard
<q-tabs v-for="ride in filteredUpcoming"
v-model="requestsSubTab" :key="ride._id"
class="my-rides-page__sub-tabs" :ride="ride"
active-color="primary" :is-driver="isDriver(ride)"
indicator-color="primary" :pending-requests="getPendingRequests(ride._id)"
align="left" @view="goToRide"
dense @edit="editRide"
> @cancel="cancelRide"
<q-tab name="received" label="Ricevute" /> @complete="completeRide"
<q-tab name="sent" label="Inviate" /> @view-requests="openRequestsDialog"
</q-tabs> />
</div>
</q-tab-panel>
<!-- Received Requests --> <!-- REQUESTS TAB -->
<div v-if="requestsSubTab === 'received'"> <q-tab-panel name="requests" class="my-rides-page__panel">
<div v-if="loadingRequests" class="my-rides-page__loading"> <q-tabs
<q-skeleton v-for="i in 3" :key="i" type="QItem" class="q-mb-sm" /> v-model="requestsSubTab"
class="my-rides-page__sub-tabs"
active-color="primary"
indicator-color="primary"
align="justify"
>
<q-tab name="received" label="Ricevute" />
<q-tab name="sent" label="Inviate" />
</q-tabs>
<div v-if="loadingRequests" class="my-rides-page__loading">
<q-spinner-dots size="50px" color="primary" />
</div>
<q-tab-panels v-else v-model="requestsSubTab" animated>
<q-tab-panel name="received">
<div v-if="receivedRequests.length === 0" class="my-rides-page__empty">
<q-icon name="inbox" size="80px" color="grey-4" />
<h3>Nessuna richiesta ricevuta</h3>
</div> </div>
<div v-else class="my-rides-page__list">
<div v-else-if="receivedRequests.length === 0" class="my-rides-page__empty my-rides-page__empty--small">
<q-icon name="inbox" size="48px" color="grey-4" />
<span>Nessuna richiesta ricevuta</span>
</div>
<q-list v-else class="my-rides-page__requests-list">
<RequestCard <RequestCard
v-for="request in receivedRequests" v-for="request in receivedRequests"
:key="request._id" :key="request._id"
:request="request" :request="request"
mode="received" type="received"
@accept="acceptRequest(request)" @accept="acceptRequest"
@reject="rejectRequest(request)" @reject="rejectRequest"
@view-ride="goToRide(request.rideId._id || request.rideId)" @view-profile="goToProfile"
@view-user="goToProfile(request.passengerId._id || request.passengerId)"
/> />
</q-list>
</div>
<!-- Sent Requests -->
<div v-if="requestsSubTab === 'sent'">
<div v-if="loadingRequests" class="my-rides-page__loading">
<q-skeleton v-for="i in 3" :key="i" type="QItem" class="q-mb-sm" />
</div> </div>
</q-tab-panel>
<div v-else-if="sentRequests.length === 0" class="my-rides-page__empty my-rides-page__empty--small"> <q-tab-panel name="sent">
<q-icon name="send" size="48px" color="grey-4" /> <div v-if="sentRequests.length === 0" class="my-rides-page__empty">
<span>Nessuna richiesta inviata</span> <q-icon name="send" size="80px" color="grey-4" />
<h3>Nessuna richiesta inviata</h3>
</div> </div>
<div v-else class="my-rides-page__list">
<q-list v-else class="my-rides-page__requests-list">
<RequestCard <RequestCard
v-for="request in sentRequests" v-for="request in sentRequests"
:key="request._id" :key="request._id"
:request="request" :request="request"
mode="sent" type="sent"
@cancel="cancelRequest(request)" @cancel="cancelRequest"
@view-ride="goToRide(request.rideId._id || request.rideId)" @view-profile="goToProfile"
@view-user="goToProfile(request.driverId._id || request.driverId)"
/> />
</q-list> </div>
</q-tab-panel>
</q-tab-panels>
</q-tab-panel>
<!-- PAST TAB -->
<q-tab-panel name="past" class="my-rides-page__panel">
<div v-if="loading" class="my-rides-page__loading">
<q-spinner-dots size="50px" color="primary" />
</div>
<div v-else-if="filteredPast.length === 0" class="my-rides-page__empty">
<q-icon name="history" size="80px" color="grey-4" />
<h3>Nessun viaggio passato</h3>
</div>
<div v-else class="my-rides-page__list">
<MyRideCard
v-for="ride in filteredPast"
:key="ride._id"
:ride="ride"
:is-driver="isDriver(ride)"
:can-leave-feedback="canLeaveFeedback(ride)"
@view="goToRide"
@feedback="openFeedbackDialog"
/>
</div>
</q-tab-panel>
<!-- CANCELLED TAB -->
<q-tab-panel name="cancelled" class="my-rides-page__panel">
<div v-if="loadingCancelled" class="my-rides-page__loading">
<q-spinner-dots size="50px" color="primary" />
</div>
<div v-else-if="filteredCancelled.length === 0" class="my-rides-page__empty">
<q-icon name="check_circle" size="80px" color="positive" />
<h3>Nessun viaggio cancellato</h3>
<p>Ottimo! Non hai viaggi cancellati</p>
</div>
<div v-else>
<div class="my-rides-page__list">
<MyRideCard
v-for="ride in filteredCancelled"
:key="ride._id"
:ride="ride"
:is-driver="isDriver(ride)"
:is-cancelled="true"
@view="goToRide"
/>
</div> </div>
</q-tab-panel>
</q-tab-panels> <!-- Load More for Cancelled -->
</div> <div
v-if="cancelledPagination.hasMore"
class="my-rides-page__load-more"
>
<q-btn
flat
color="primary"
:loading="loadingCancelled"
@click="loadMoreCancelled"
>
Carica altri
</q-btn>
</div>
</div>
</q-tab-panel>
</q-tab-panels>
<!-- Requests Dialog --> <!-- Requests Dialog -->
<q-dialog v-model="showRequestsDialog" position="bottom" full-width> <q-dialog v-model="showRequestsDialog">
<q-card class="my-rides-page__requests-dialog"> <q-card style="min-width: 350px; max-width: 500px">
<q-card-section class="row items-center"> <q-card-section>
<div class="text-h6">Richieste per questo viaggio</div> <div class="text-h6">Richieste per questo viaggio</div>
<q-space />
<q-btn flat round icon="close" v-close-popup />
</q-card-section> </q-card-section>
<q-separator /> <q-card-section class="q-pt-none">
<div v-if="selectedRideRequests.length === 0">
<q-card-section class="q-pa-none" style="max-height: 60vh; overflow-y: auto"> <p class="text-center text-grey-6">Nessuna richiesta</p>
<q-list v-if="selectedRideRequests.length > 0"> </div>
<div v-else>
<RequestCard <RequestCard
v-for="request in selectedRideRequests" v-for="request in selectedRideRequests"
:key="request._id" :key="request._id"
:request="request" :request="request"
mode="received" type="received"
@accept="acceptRequest(request); showRequestsDialog = false" compact
@reject="rejectRequest(request)" @accept="acceptRequest"
@view-user="goToProfile(request.passengerId._id || request.passengerId)" @reject="rejectRequest"
@view-profile="goToProfile"
/> />
</q-list>
<div v-else class="text-center q-pa-lg text-grey">
Nessuna richiesta pendente
</div> </div>
</q-card-section> </q-card-section>
<q-card-actions align="right">
<q-btn flat label="Chiudi" color="primary" v-close-popup />
</q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
<!-- FAB Mobile -->
<q-page-sticky position="bottom-right" :offset="[18, 18]" class="lt-md">
<q-btn fab color="primary" icon="add" @click="goToCreate" />
</q-page-sticky>
</q-page> </q-page>
</template> </template>
<script lang="ts" src="./MyRidesPage.ts" /> <script lang="ts" src="./MyRidesPage.ts"></script>
<style lang="scss" src="./MyRidesPage.scss" /> <style lang="scss" scoped src="./MyRidesPage.scss"></style>

View File

@@ -0,0 +1,973 @@
<!-- NotificationsPage.vue -->
<template>
<q-page class="notifications-page">
<!-- Header -->
<div class="notifications-page__header">
<q-btn
flat
round
icon="arrow_back"
color="white"
@click="goBack"
/>
<div>
<h1>Notifiche</h1>
<p>Gestisci come ricevere le notifiche</p>
</div>
</div>
<!-- Loading -->
<div v-if="loading && !preferences" class="notifications-page__loading">
<q-spinner-dots size="50px" color="primary" />
<p>Caricamento...</p>
</div>
<!-- Content -->
<div v-else class="notifications-page__content">
<!-- EMAIL NOTIFICATIONS -->
<div class="notifications-page__section">
<div class="notifications-page__section-header">
<div class="notifications-page__section-icon">
<q-icon name="email" size="32px" color="primary" />
</div>
<div class="notifications-page__section-info">
<h2>Email</h2>
<p>{{ userEmail }}</p>
</div>
<q-toggle
v-model="emailEnabled"
color="primary"
@update:model-value="toggleEmail"
/>
</div>
<q-slide-transition>
<div v-if="emailEnabled" class="notifications-page__options">
<div
v-for="option in emailOptions"
:key="option.key"
class="notifications-page__option"
>
<div class="notifications-page__option-info">
<q-icon :name="option.icon" size="20px" :color="option.color" />
<div>
<div class="notifications-page__option-label">{{ option.label }}</div>
<div class="notifications-page__option-desc">{{ option.description }}</div>
</div>
</div>
<q-toggle
v-model="preferences.email[option.key]"
color="primary"
:disable="!emailEnabled"
@update:model-value="savePreferences"
/>
</div>
<div class="notifications-page__test">
<q-btn
outline
color="primary"
label="Invia email di test"
icon="send"
:loading="testingSending === 'email'"
@click="testEmail"
/>
</div>
</div>
</q-slide-transition>
</div>
<!-- TELEGRAM NOTIFICATIONS -->
<div class="notifications-page__section">
<div class="notifications-page__section-header">
<div class="notifications-page__section-icon">
<q-icon name="send" size="32px" color="info" />
</div>
<div class="notifications-page__section-info">
<h2>Telegram</h2>
<p v-if="telegramConnected">@{{ preferences.telegram.username || 'Connesso' }}</p>
<p v-else class="text-grey-6">Non connesso</p>
</div>
<q-toggle
v-model="telegramEnabled"
color="info"
:disable="!telegramConnected"
@update:model-value="toggleTelegram"
/>
</div>
<!-- Telegram Connection -->
<div v-if="!telegramConnected" class="notifications-page__connect">
<div class="notifications-page__connect-info">
<q-icon name="info" color="info" size="24px" />
<div>
<h3>Come connettere Telegram</h3>
<ol>
<li>Apri Telegram e cerca il bot <strong>@TrasportiSolidaliBot</strong></li>
<li>Clicca su "Avvia" o invia <code>/start</code></li>
<li>Il bot ti invierà un codice di connessione</li>
<li>Inserisci il codice qui sotto</li>
</ol>
</div>
</div>
<div class="notifications-page__connect-form">
<q-input
v-model="telegramCode"
label="Codice di connessione"
outlined
placeholder="es: 123456"
:rules="[val => !!val || 'Codice richiesto']"
>
<template v-slot:prepend>
<q-icon name="key" />
</template>
</q-input>
<div class="notifications-page__connect-actions">
<q-btn
color="info"
label="Apri Telegram"
icon="open_in_new"
outline
:href="telegramBotLink"
target="_blank"
/>
<q-btn
color="info"
label="Connetti"
icon="link"
unelevated
:loading="loading"
:disable="!telegramCode"
@click="connectTelegramBot"
/>
</div>
</div>
</div>
<!-- Telegram Options -->
<q-slide-transition>
<div v-if="telegramConnected && telegramEnabled" class="notifications-page__options">
<div
v-for="option in telegramOptions"
:key="option.key"
class="notifications-page__option"
>
<div class="notifications-page__option-info">
<q-icon :name="option.icon" size="20px" :color="option.color" />
<div>
<div class="notifications-page__option-label">{{ option.label }}</div>
<div class="notifications-page__option-desc">{{ option.description }}</div>
</div>
</div>
<q-toggle
v-model="preferences.telegram[option.key]"
color="info"
:disable="!telegramEnabled"
@update:model-value="savePreferences"
/>
</div>
<div class="notifications-page__test">
<q-btn
outline
color="info"
label="Invia messaggio di test"
icon="send"
:loading="testingSending === 'telegram'"
@click="testTelegram"
/>
<q-btn
flat
color="negative"
label="Disconnetti"
icon="link_off"
@click="disconnectTelegramBot"
/>
</div>
</div>
</q-slide-transition>
</div>
<!-- PUSH NOTIFICATIONS -->
<div class="notifications-page__section">
<div class="notifications-page__section-header">
<div class="notifications-page__section-icon">
<q-icon name="notifications" size="32px" color="orange" />
</div>
<div class="notifications-page__section-info">
<h2>Notifiche Push</h2>
<p v-if="pushSupported && pushEnabled">Attive</p>
<p v-else-if="!pushSupported" class="text-grey-6">Non supportate</p>
<p v-else class="text-grey-6">Non attive</p>
</div>
<q-toggle
v-model="pushEnabled"
color="orange"
:disable="!pushSupported"
@update:model-value="togglePush"
/>
</div>
<!-- Push Not Supported -->
<div v-if="!pushSupported" class="notifications-page__not-supported">
<q-icon name="warning" color="warning" size="48px" />
<p>Le notifiche push non sono supportate dal tuo browser o dispositivo.</p>
<p class="text-caption">Prova con Chrome, Firefox, Edge o Safari (iOS 16.4+)</p>
</div>
<!-- Push Options -->
<q-slide-transition>
<div v-if="pushSupported && pushEnabled" class="notifications-page__options">
<div
v-for="option in pushOptions"
:key="option.key"
class="notifications-page__option"
>
<div class="notifications-page__option-info">
<q-icon :name="option.icon" size="20px" :color="option.color" />
<div>
<div class="notifications-page__option-label">{{ option.label }}</div>
<div class="notifications-page__option-desc">{{ option.description }}</div>
</div>
</div>
<q-toggle
v-model="preferences.push[option.key]"
color="orange"
:disable="!pushEnabled"
@update:model-value="savePreferences"
/>
</div>
<div class="notifications-page__test">
<q-btn
outline
color="orange"
label="Invia notifica di test"
icon="notifications"
:loading="testingSending === 'push'"
@click="testPush"
/>
</div>
</div>
</q-slide-transition>
</div>
<!-- NOTIFICATION TYPES INFO -->
<div class="notifications-page__info">
<h3>Tipi di Notifiche</h3>
<div class="notifications-page__info-grid">
<div class="notifications-page__info-item">
<q-icon name="drive_eta" color="primary" size="24px" />
<div>
<strong>Nuova richiesta viaggio</strong>
<p>Quando qualcuno richiede un posto nel tuo viaggio</p>
</div>
</div>
<div class="notifications-page__info-item">
<q-icon name="check_circle" color="positive" size="24px" />
<div>
<strong>Richiesta accettata</strong>
<p>Quando un conducente accetta la tua richiesta</p>
</div>
</div>
<div class="notifications-page__info-item">
<q-icon name="cancel" color="negative" size="24px" />
<div>
<strong>Richiesta rifiutata</strong>
<p>Quando un conducente rifiuta la tua richiesta</p>
</div>
</div>
<div class="notifications-page__info-item">
<q-icon name="schedule" color="orange" size="24px" />
<div>
<strong>Promemoria viaggio</strong>
<p>24h e 2h prima della partenza</p>
</div>
</div>
<div class="notifications-page__info-item">
<q-icon name="event_busy" color="warning" size="24px" />
<div>
<strong>Viaggio cancellato</strong>
<p>Quando un viaggio prenotato viene cancellato</p>
</div>
</div>
<div class="notifications-page__info-item">
<q-icon name="chat" color="info" size="24px" />
<div>
<strong>Nuovo messaggio</strong>
<p>Quando ricevi un messaggio nella chat</p>
</div>
</div>
</div>
</div>
</div>
</q-page>
</template>
<script lang="ts">
import { defineComponent, ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useNotifications } from '../composables/useNotifications';
import { useUserStore } from 'app/src/store';
export default defineComponent({
name: 'NotificationsPage',
setup() {
const router = useRouter();
const $q = useQuasar();
const userStore = useUserStore();
const {
loading,
preferences,
testingSending,
fetchPreferences,
updatePreferences,
connectTelegram,
disconnectTelegram,
subscribePush,
unsubscribePush,
sendTestNotification,
requestPushPermission,
getTelegramBotLink
} = useNotifications();
// State
const telegramCode = ref('');
const emailEnabled = ref(true);
const telegramEnabled = ref(false);
const pushEnabled = ref(false);
// Computed
const userEmail = computed(() => userStore.my.email || 'Non disponibile');
const telegramConnected = computed(() => {
return !!(preferences.value?.telegram.chatId);
});
const pushSupported = computed(() => {
return 'serviceWorker' in navigator && 'PushManager' in window;
});
const telegramBotLink = computed(() => {
return getTelegramBotLink(userStore.my._id);
});
// Notification Options
const emailOptions = [
{
key: 'newRideRequest',
label: 'Nuova richiesta viaggio',
description: 'Quando qualcuno richiede un posto',
icon: 'drive_eta',
color: 'primary'
},
{
key: 'requestAccepted',
label: 'Richiesta accettata',
description: 'Quando la tua richiesta viene accettata',
icon: 'check_circle',
color: 'positive'
},
{
key: 'requestRejected',
label: 'Richiesta rifiutata',
description: 'Quando la tua richiesta viene rifiutata',
icon: 'cancel',
color: 'negative'
},
{
key: 'rideReminder24h',
label: 'Promemoria 24h prima',
description: 'Un giorno prima della partenza',
icon: 'schedule',
color: 'orange'
},
{
key: 'rideReminder2h',
label: 'Promemoria 2h prima',
description: 'Due ore prima della partenza',
icon: 'alarm',
color: 'orange'
},
{
key: 'rideCancelled',
label: 'Viaggio cancellato',
description: 'Quando un viaggio viene cancellato',
icon: 'event_busy',
color: 'warning'
},
{
key: 'newMessage',
label: 'Nuovo messaggio',
description: 'Quando ricevi un messaggio',
icon: 'chat',
color: 'info'
},
{
key: 'newCommunityRide',
label: 'Nuovi viaggi disponibili',
description: 'Quando ci sono nuovi viaggi nella community',
icon: 'explore',
color: 'purple'
},
{
key: 'weeklyDigest',
label: 'Riepilogo settimanale',
description: 'Riassunto dei tuoi viaggi della settimana',
icon: 'summarize',
color: 'indigo'
}
];
const telegramOptions = emailOptions.filter(opt =>
opt.key !== 'newCommunityRide' && opt.key !== 'weeklyDigest'
);
const pushOptions = emailOptions.filter(opt =>
opt.key !== 'weeklyDigest'
);
// Methods
const goBack = () => {
router.back();
};
const syncToggleStates = () => {
if (preferences.value) {
emailEnabled.value = preferences.value.email.enabled;
telegramEnabled.value = preferences.value.telegram.enabled;
pushEnabled.value = preferences.value.push.enabled;
}
};
const toggleEmail = async (value: boolean) => {
try {
await updatePreferences({
email: { ...preferences.value!.email, enabled: value }
});
$q.notify({
type: value ? 'positive' : 'info',
message: value ? 'Notifiche email attivate' : 'Notifiche email disattivate'
});
} catch (error: any) {
emailEnabled.value = !value;
$q.notify({
type: 'negative',
message: 'Errore nell\'aggiornamento'
});
}
};
const toggleTelegram = async (value: boolean) => {
if (!telegramConnected.value) {
telegramEnabled.value = false;
$q.notify({
type: 'warning',
message: 'Connetti prima il tuo account Telegram'
});
return;
}
try {
await updatePreferences({
telegram: { ...preferences.value!.telegram, enabled: value }
});
$q.notify({
type: value ? 'positive' : 'info',
message: value ? 'Notifiche Telegram attivate' : 'Notifiche Telegram disattivate'
});
} catch (error: any) {
telegramEnabled.value = !value;
$q.notify({
type: 'negative',
message: 'Errore nell\'aggiornamento'
});
}
};
const togglePush = async (value: boolean) => {
if (value) {
// Enable push
try {
const subscription = await requestPushPermission();
if (subscription) {
await subscribePush(subscription.toJSON());
$q.notify({
type: 'positive',
message: 'Notifiche push attivate con successo!'
});
}
} catch (error: any) {
pushEnabled.value = false;
$q.notify({
type: 'negative',
message: error.message || 'Errore nell\'attivazione delle notifiche push'
});
}
} else {
// Disable push
try {
await unsubscribePush();
$q.notify({
type: 'info',
message: 'Notifiche push disattivate'
});
} catch (error: any) {
pushEnabled.value = true;
$q.notify({
type: 'negative',
message: 'Errore nella disattivazione'
});
}
}
};
const connectTelegramBot = async () => {
if (!telegramCode.value) {
$q.notify({
type: 'warning',
message: 'Inserisci il codice di connessione'
});
return;
}
try {
// Parse code format: userId-chatId or just chatId
const parts = telegramCode.value.split('-');
const chatId = parts.length > 1 ? parts[1] : telegramCode.value;
await connectTelegram(chatId);
$q.notify({
type: 'positive',
message: 'Telegram connesso con successo!',
caption: 'Riceverai un messaggio di conferma'
});
telegramCode.value = '';
telegramEnabled.value = true;
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.message || 'Errore nella connessione Telegram'
});
}
};
const disconnectTelegramBot = async () => {
$q.dialog({
title: 'Disconnetti Telegram',
message: 'Sei sicuro di voler disconnettere il tuo account Telegram?',
cancel: true
}).onOk(async () => {
try {
await disconnectTelegram();
$q.notify({
type: 'info',
message: 'Telegram disconnesso'
});
telegramEnabled.value = false;
} catch (error: any) {
$q.notify({
type: 'negative',
message: 'Errore nella disconnessione'
});
}
});
};
const savePreferences = async () => {
try {
await updatePreferences(preferences.value!);
} catch (error: any) {
$q.notify({
type: 'negative',
message: 'Errore nel salvataggio'
});
}
};
const testEmail = async () => {
try {
await sendTestNotification('email');
$q.notify({
type: 'positive',
message: 'Email di test inviata!',
caption: 'Controlla la tua casella di posta'
});
} catch (error: any) {
$q.notify({
type: 'negative',
message: 'Errore nell\'invio dell\'email di test'
});
}
};
const testTelegram = async () => {
try {
await sendTestNotification('telegram');
$q.notify({
type: 'positive',
message: 'Messaggio di test inviato!',
caption: 'Controlla Telegram'
});
} catch (error: any) {
$q.notify({
type: 'negative',
message: 'Errore nell\'invio del messaggio di test'
});
}
};
const testPush = async () => {
try {
await sendTestNotification('push');
$q.notify({
type: 'positive',
message: 'Notifica push inviata!',
caption: 'Dovresti vederla a breve'
});
} catch (error: any) {
$q.notify({
type: 'negative',
message: 'Errore nell\'invio della notifica push'
});
}
};
// Lifecycle
onMounted(async () => {
try {
await fetchPreferences();
syncToggleStates();
} catch (error: any) {
$q.notify({
type: 'negative',
message: 'Errore nel caricamento delle preferenze'
});
}
});
return {
// State
loading,
preferences,
testingSending,
telegramCode,
emailEnabled,
telegramEnabled,
pushEnabled,
// Computed
userEmail,
telegramConnected,
pushSupported,
telegramBotLink,
// Options
emailOptions,
telegramOptions,
pushOptions,
// Methods
goBack,
toggleEmail,
toggleTelegram,
togglePush,
connectTelegramBot,
disconnectTelegramBot,
savePreferences,
testEmail,
testTelegram,
testPush
};
}
});
</script>
<style lang="scss" scoped>
.notifications-page {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
min-height: 100vh;
padding-bottom: 80px;
// Header
&__header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16px 20px;
display: flex;
align-items: center;
gap: 12px;
h1 {
font-size: 20px;
font-weight: 600;
margin: 0;
}
p {
margin: 4px 0 0;
opacity: 0.85;
font-size: 14px;
}
}
// Loading
&__loading {
display: flex;
flex-direction: column;
align-items: center;
padding: 60px 20px;
p {
margin-top: 16px;
color: #888;
}
}
// Content
&__content {
padding: 16px;
}
// Section
&__section {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
&__section-header {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
&__section-icon {
flex-shrink: 0;
}
&__section-info {
flex: 1;
h2 {
font-size: 18px;
font-weight: 600;
margin: 0;
color: #333;
}
p {
margin: 4px 0 0;
font-size: 13px;
color: #888;
}
}
// Options
&__options {
padding-top: 16px;
}
&__option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
}
&__option-info {
display: flex;
align-items: flex-start;
gap: 12px;
flex: 1;
}
&__option-label {
font-weight: 600;
color: #333;
font-size: 14px;
}
&__option-desc {
font-size: 12px;
color: #888;
margin-top: 2px;
}
// Test Button
&__test {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
// Telegram Connect
&__connect {
padding-top: 16px;
}
&__connect-info {
display: flex;
gap: 12px;
padding: 16px;
background: #e3f2fd;
border-radius: 12px;
margin-bottom: 16px;
h3 {
font-size: 15px;
font-weight: 600;
margin: 0 0 8px;
color: #1976d2;
}
ol {
margin: 0;
padding-left: 20px;
color: #555;
font-size: 13px;
li {
margin-bottom: 4px;
}
}
code {
background: rgba(0, 0, 0, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
}
&__connect-form {
display: flex;
flex-direction: column;
gap: 12px;
}
&__connect-actions {
display: flex;
gap: 8px;
}
// Not Supported
&__not-supported {
padding: 24px;
text-align: center;
color: #888;
p {
margin: 12px 0 0;
font-size: 14px;
}
}
// Info Section
&__info {
background: white;
border-radius: 16px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
h3 {
font-size: 16px;
font-weight: 600;
margin: 0 0 16px;
color: #333;
}
}
&__info-grid {
display: flex;
flex-direction: column;
gap: 12px;
}
&__info-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
strong {
display: block;
margin-bottom: 4px;
color: #333;
font-size: 14px;
}
p {
margin: 0;
font-size: 12px;
color: #666;
line-height: 1.4;
}
}
}
// Dark Mode
.body--dark {
.notifications-page {
background: linear-gradient(135deg, #1a1a2e 0%, #16162a 100%);
&__section,
&__info {
background: #1e1e30;
}
&__section-header,
&__option,
&__test {
border-color: rgba(255, 255, 255, 0.1);
}
&__section-info h2,
&__option-label,
&__info h3,
&__info-item strong {
color: #fff;
}
&__connect-info {
background: rgba(25, 118, 210, 0.1);
}
&__info-item {
background: rgba(255, 255, 255, 0.05);
}
}
}
</style>

View File

@@ -219,7 +219,7 @@
<!-- Ride Info --> <!-- Ride Info -->
<div <div
class="requests-page__ride" class="requests-page__ride"
@click="viewRide(request.rideId)" @click="viewRide(request.rideId?._id || request.rideId)"
> >
<div class="requests-page__ride-type"> <div class="requests-page__ride-type">
<q-icon <q-icon
@@ -375,7 +375,7 @@
icon="info" icon="info"
label="Dettagli" label="Dettagli"
size="sm" size="sm"
@click="viewRide(request.rideId._id)" @click="viewRide(request.rideId?._id || request.rideId)"
/> />
</div> </div>
</div> </div>
@@ -453,6 +453,7 @@ import { defineComponent, ref, computed, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar, date as qdate } from 'quasar'; import { useQuasar, date as qdate } from 'quasar';
import { Api } from '@api'; import { Api } from '@api';
import { Ride } from '../types';
interface RequestStats { interface RequestStats {
pending: number; pending: number;
@@ -462,7 +463,7 @@ interface RequestStats {
interface RideRequest { interface RideRequest {
_id: string; _id: string;
rideId: string; rideId: string | Ride;
fromUserId: string; fromUserId: string;
toUserId: string; toUserId: string;
fromUser?: any; fromUser?: any;

View File

@@ -66,7 +66,7 @@ export default defineComponent({
waypoints: [], waypoints: [],
date: '', date: '',
time: '', time: '',
dateTime: '', departureDate: '',
flexibleTime: false, flexibleTime: false,
flexibleMinutes: 30, flexibleMinutes: 30,
recurrence: { type: 'once' }, recurrence: { type: 'once' },
@@ -145,7 +145,7 @@ export default defineComponent({
// Preview ride per il riepilogo // Preview ride per il riepilogo
const previewRide = computed((): Partial<Ride> => { const previewRide = computed((): Partial<Ride> => {
const dateTime = const departureDate =
formData.date && formData.time formData.date && formData.time
? new Date(`${formData.date}T${formData.time}`) ? new Date(`${formData.date}T${formData.time}`)
: new Date(); : new Date();
@@ -159,7 +159,7 @@ export default defineComponent({
coordinates: { lat: 0, lng: 0 }, coordinates: { lat: 0, lng: 0 },
}, },
waypoints: formData.waypoints || [], waypoints: formData.waypoints || [],
dateTime: dateTime.toISOString(), departureDate: departureDate.toISOString(),
passengers: formData.passengers, passengers: formData.passengers,
seatsNeeded: formData.seatsNeeded, seatsNeeded: formData.seatsNeeded,
vehicle: formData.vehicle, vehicle: formData.vehicle,
@@ -231,14 +231,14 @@ export default defineComponent({
try { try {
// Combina data e ora // Combina data e ora
const dateTime = new Date(`${formData.date}T${formData.time}`); const departureDate = new Date(`${formData.date}T${formData.time}`);
const rideData: Partial<RideFormData> = { const rideData: Partial<RideFormData> = {
type: formData.type, type: formData.type,
departure: formData.departure, departure: formData.departure,
destination: formData.destination, destination: formData.destination,
waypoints: formData.waypoints, waypoints: formData.waypoints,
dateTime: dateTime.toISOString(), departureDate: departureDate.toISOString(),
flexibleTime: formData.flexibleTime, flexibleTime: formData.flexibleTime,
flexibleMinutes: formData.flexibleMinutes, flexibleMinutes: formData.flexibleMinutes,
recurrence: formData.recurrence, recurrence: formData.recurrence,
@@ -299,7 +299,7 @@ export default defineComponent({
formData.destination = ride.destination; formData.destination = ride.destination;
formData.waypoints = ride.waypoints || []; formData.waypoints = ride.waypoints || [];
const dt = new Date(ride.dateTime); const dt = new Date(ride.departureDate);
formData.date = dt.toISOString().split('T')[0]; formData.date = dt.toISOString().split('T')[0];
formData.time = dt.toTimeString().slice(0, 5); formData.time = dt.toTimeString().slice(0, 5);

View File

@@ -107,13 +107,13 @@ export default defineComponent({
const departureTime = computed(() => { const departureTime = computed(() => {
if (!ride.value) return ''; if (!ride.value) return '';
const date = new Date(ride.value.dateTime); const date = new Date(ride.value.departureDate);
return date.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); return date.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
}); });
const arrivalTime = computed(() => { const arrivalTime = computed(() => {
if (!ride.value || !ride.value.estimatedDuration) return ''; if (!ride.value || !ride.value.estimatedDuration) return '';
const departure = new Date(ride.value.dateTime); const departure = new Date(ride.value.departureDate);
const arrival = new Date( const arrival = new Date(
departure.getTime() + ride.value.estimatedDuration * 60000 departure.getTime() + ride.value.estimatedDuration * 60000
); );
@@ -122,7 +122,7 @@ export default defineComponent({
const formattedDate = computed(() => { const formattedDate = computed(() => {
if (!ride.value) return ''; if (!ride.value) return '';
return formatRideDate(ride.value.dateTime); return formatRideDate(ride.value.departureDate);
}); });
const formattedDuration = computed(() => { const formattedDuration = computed(() => {

View File

@@ -107,11 +107,11 @@ export default defineComponent({
switch (sortBy.value) { switch (sortBy.value) {
case 'date_asc': case 'date_asc':
return sorted.sort( return sorted.sort(
(a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime() (a, b) => new Date(a.departureDate).getTime() - new Date(b.departureDate).getTime()
); );
case 'date_desc': case 'date_desc':
return sorted.sort( return sorted.sort(
(a, b) => new Date(b.dateTime).getTime() - new Date(a.dateTime).getTime() (a, b) => new Date(b.departureDate).getTime() - new Date(a.departureDate).getTime()
); );
case 'price_asc': case 'price_asc':
return sorted.sort((a, b) => { return sorted.sort((a, b) => {

File diff suppressed because it is too large Load Diff

View File

@@ -99,7 +99,7 @@
<!-- Vehicle Type --> <!-- Vehicle Type -->
<div class="vehicle-edit-page__type-selector"> <div class="vehicle-edit-page__type-selector">
<div <div
v-for="vType in vehicleTypes" v-for="vType in VEHICLE_TYPES"
:key="vType.value" :key="vType.value"
class="vehicle-edit-page__type-option" class="vehicle-edit-page__type-option"
:class="{ :class="{
@@ -239,7 +239,7 @@
<label>Tipo di alimentazione *</label> <label>Tipo di alimentazione *</label>
<div class="vehicle-edit-page__fuel-options"> <div class="vehicle-edit-page__fuel-options">
<div <div
v-for="fuel in fuelTypes" v-for="fuel in FUELTYPES"
:key="fuel.value" :key="fuel.value"
class="vehicle-edit-page__fuel-option" class="vehicle-edit-page__fuel-option"
:class="{ :class="{
@@ -266,7 +266,7 @@
<div class="vehicle-edit-page__features"> <div class="vehicle-edit-page__features">
<q-checkbox <q-checkbox
v-for="feature in availableFeatures" v-for="feature in VEHICLE_FEATURES_OPTIONS"
:key="feature.value" :key="feature.value"
v-model="form.features" v-model="form.features"
:val="feature.value" :val="feature.value"
@@ -361,6 +361,7 @@ import { defineComponent, ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { Api } from '@api'; import { Api } from '@api';
import { VEHICLE_TYPES } from '../types/viaggi.types';
interface VehicleForm { interface VehicleForm {
type: string; type: string;
@@ -408,23 +409,6 @@ export default defineComponent({
isDefault: false, isDefault: false,
}); });
// Options
const vehicleTypes = [
{ value: 'car', label: 'Auto', icon: 'directions_car' },
{ value: 'suv', label: 'SUV', icon: 'local_shipping' },
{ value: 'van', label: 'Van', icon: 'airport_shuttle' },
{ value: 'motorcycle', label: 'Moto', icon: 'two_wheeler' },
];
const fuelTypes = [
{ value: 'petrol', label: 'Benzina', icon: 'local_gas_station' },
{ value: 'diesel', label: 'Diesel', icon: 'local_gas_station' },
{ value: 'hybrid', label: 'Ibrido', icon: 'eco' },
{ value: 'electric', label: 'Elettrico', icon: 'bolt' },
{ value: 'lpg', label: 'GPL', icon: 'propane_tank' },
{ value: 'methane', label: 'Metano', icon: 'propane' },
];
const colorOptions = [ const colorOptions = [
{ value: 'bianco', label: 'Bianco', hex: '#ffffff' }, { value: 'bianco', label: 'Bianco', hex: '#ffffff' },
{ value: 'nero', label: 'Nero', hex: '#1a1a1a' }, { value: 'nero', label: 'Nero', hex: '#1a1a1a' },
@@ -439,19 +423,6 @@ export default defineComponent({
{ value: 'beige', label: 'Beige', hex: '#d7ccc8' }, { value: 'beige', label: 'Beige', hex: '#d7ccc8' },
]; ];
const availableFeatures = [
{ value: 'aria_condizionata', label: 'Aria condizionata', icon: 'ac_unit' },
{ value: 'bluetooth', label: 'Bluetooth', icon: 'bluetooth' },
{ value: 'usb', label: 'Presa USB', icon: 'usb' },
{ value: 'navigatore', label: 'Navigatore', icon: 'navigation' },
{ value: 'seggiolino', label: 'Seggiolino bimbi', icon: 'child_friendly' },
{ value: 'portabagagli', label: 'Ampio bagagliaio', icon: 'luggage' },
{ value: 'animali', label: 'Animali ammessi', icon: 'pets' },
{ value: 'wifi', label: 'WiFi', icon: 'wifi' },
{ value: 'fumatori', label: 'Si può fumare', icon: 'smoking_rooms' },
{ value: 'no_fumatori', label: 'Vietato fumare', icon: 'smoke_free' },
];
// Methods // Methods
const confirmCancel = () => { const confirmCancel = () => {
$q.dialog({ $q.dialog({
@@ -689,15 +660,13 @@ export default defineComponent({
isEdit, isEdit,
currentYear, currentYear,
form, form,
vehicleTypes,
fuelTypes,
colorOptions, colorOptions,
availableFeatures,
confirmCancel, confirmCancel,
addPhoto, addPhoto,
onPhotosSelected, onPhotosSelected,
removePhoto, removePhoto,
onSubmit, onSubmit,
VEHICLE_TYPES,
}; };
}, },
}); });

View File

@@ -14,7 +14,7 @@ export interface Location {
region?: string; region?: string;
country?: string; country?: string;
postalCode?: string; postalCode?: string;
coordinates: Coordinates; coordinates?: Coordinates;
} }
export interface Waypoint { export interface Waypoint {
@@ -29,19 +29,9 @@ export interface Waypoint {
// 🚗 VEICOLI // 🚗 VEICOLI
// ============================================================ // ============================================================
export type VehicleType = 'auto' | 'moto' | 'furgone' | 'minibus' | 'altro';
export type VehicleFeature =
| 'aria_condizionata'
| 'wifi'
| 'presa_usb'
| 'bluetooth'
| 'bagagliaio_grande'
| 'seggiolino_bimbi';
export interface Vehicle { export interface Vehicle {
_id?: string; _id?: string;
type?: VehicleType; type?: string;
brand?: string; brand?: string;
model?: string; model?: string;
color?: string; color?: string;
@@ -49,7 +39,7 @@ export interface Vehicle {
year?: number; year?: number;
seats?: number; seats?: number;
licensePlate?: string; licensePlate?: string;
features?: VehicleFeature[]; features?: string[];
isDefault?: boolean; isDefault?: boolean;
isVerified?: boolean; isVerified?: boolean;
photos?: string[]; photos?: string[];
@@ -77,7 +67,12 @@ export interface Recurrence {
export type SmokingPreference = 'yes' | 'no' | 'outside_only'; export type SmokingPreference = 'yes' | 'no' | 'outside_only';
export type PetsPreference = 'no' | 'small' | 'medium' | 'large' | 'all'; export type PetsPreference = 'no' | 'small' | 'medium' | 'large' | 'all';
export type LuggageSize = 'none' | 'small' | 'medium' | 'large'; export type LuggageSize = 'none' | 'small' | 'medium' | 'large';
export type MusicPreference = 'no_music' | 'quiet' | 'moderate' | 'loud' | 'passenger_choice'; export type MusicPreference =
| 'no_music'
| 'quiet'
| 'moderate'
| 'loud'
| 'passenger_choice';
export type ConversationPreference = 'quiet' | 'moderate' | 'chatty'; export type ConversationPreference = 'quiet' | 'moderate' | 'chatty';
export interface RidePreferences { export interface RidePreferences {
@@ -221,7 +216,14 @@ export interface FavoriteLocation {
// ============================================================ // ============================================================
export type RideType = 'offer' | 'request'; export type RideType = 'offer' | 'request';
export type RideStatus = 'draft' | 'active' | 'full' | 'in_progress' | 'completed' | 'cancelled' | 'expired'; export type RideStatus =
| 'draft'
| 'active'
| 'full'
| 'in_progress'
| 'completed'
| 'cancelled'
| 'expired';
export interface RidePassengers { export interface RidePassengers {
available: number; available: number;
@@ -244,7 +246,7 @@ export interface Ride {
departure: Location; departure: Location;
destination: Location; destination: Location;
waypoints?: Waypoint[]; waypoints?: Waypoint[];
dateTime: Date | string; departureDate: Date | string;
flexibleTime?: boolean; flexibleTime?: boolean;
flexibleMinutes?: number; flexibleMinutes?: number;
recurrence?: Recurrence; recurrence?: Recurrence;
@@ -275,7 +277,13 @@ export interface Ride {
// 📩 RIDE REQUEST (Richiesta Passaggio) // 📩 RIDE REQUEST (Richiesta Passaggio)
// ============================================================ // ============================================================
export type RideRequestStatus = 'pending' | 'accepted' | 'rejected' | 'cancelled' | 'expired' | 'completed'; export type RideRequestStatus =
| 'pending'
| 'accepted'
| 'rejected'
| 'cancelled'
| 'expired'
| 'completed';
export interface RideRequestContribution { export interface RideRequestContribution {
agreed: boolean; agreed: boolean;
@@ -336,7 +344,16 @@ export interface RideRequest {
// ============================================================ // ============================================================
export type ChatType = 'direct' | 'ride' | 'group'; export type ChatType = 'direct' | 'ride' | 'group';
export type MessageType = 'text' | 'ride_share' | 'location' | 'image' | 'voice' | 'system' | 'ride_request' | 'ride_accepted' | 'ride_rejected'; export type MessageType =
| 'text'
| 'ride_share'
| 'location'
| 'image'
| 'voice'
| 'system'
| 'ride_request'
| 'ride_accepted'
| 'ride_rejected';
export interface LastMessage { export interface LastMessage {
text: string; text: string;
@@ -616,7 +633,7 @@ export interface RideFormData {
departure: Location; departure: Location;
destination: Location; destination: Location;
waypoints: Waypoint[]; waypoints: Waypoint[];
dateTime: string; departureDate: string;
flexibleTime: boolean; flexibleTime: boolean;
flexibleMinutes: number; flexibleMinutes: number;
recurrence: Recurrence; recurrence: Recurrence;
@@ -785,15 +802,15 @@ export const RIDE_TYPE_OPTIONS: RideTypeOption[] = [
label: 'Offro Passaggio', label: 'Offro Passaggio',
icon: '🟢', icon: '🟢',
color: 'positive', color: 'positive',
description: 'Ho posti disponibili nella mia auto' description: 'Ho posti disponibili nella mia auto',
}, },
{ {
value: 'request', value: 'request',
label: 'Cerco Passaggio', label: 'Cerco Passaggio',
icon: '🔴', icon: '🔴',
color: 'negative', color: 'negative',
description: 'Sto cercando qualcuno che mi dia un passaggio' description: 'Sto cercando qualcuno che mi dia un passaggio',
} },
]; ];
export const DAYS_OF_WEEK: DayOfWeekOption[] = [ export const DAYS_OF_WEEK: DayOfWeekOption[] = [
@@ -803,13 +820,22 @@ export const DAYS_OF_WEEK: DayOfWeekOption[] = [
{ value: 3, label: 'Mercoledì', shortLabel: 'Mer' }, { value: 3, label: 'Mercoledì', shortLabel: 'Mer' },
{ value: 4, label: 'Giovedì', shortLabel: 'Gio' }, { value: 4, label: 'Giovedì', shortLabel: 'Gio' },
{ value: 5, label: 'Venerdì', shortLabel: 'Ven' }, { value: 5, label: 'Venerdì', shortLabel: 'Ven' },
{ value: 6, label: 'Sabato', shortLabel: 'Sab' } { value: 6, label: 'Sabato', shortLabel: 'Sab' },
];
export const FUELTYPES = [
{ value: 'petrol', label: 'Benzina', icon: 'local_gas_station' },
{ value: 'diesel', label: 'Diesel', icon: 'local_gas_station' },
{ value: 'hybrid', label: 'Ibrido', icon: 'eco' },
{ value: 'electric', label: 'Elettrico', icon: 'bolt' },
{ value: 'lpg', label: 'GPL', icon: 'propane_tank' },
{ value: 'methane', label: 'Metano', icon: 'propane' },
]; ];
export const SMOKING_OPTIONS: PreferenceOption<SmokingPreference>[] = [ export const SMOKING_OPTIONS: PreferenceOption<SmokingPreference>[] = [
{ value: 'no', label: 'Non fumatori', icon: '🚭' }, { value: 'no', label: 'Non fumatori', icon: '🚭' },
{ value: 'yes', label: 'Fumatori OK', icon: '🚬' }, { value: 'yes', label: 'Fumatori OK', icon: '🚬' },
{ value: 'outside_only', label: 'Solo fuori', icon: '🪟' } { value: 'outside_only', label: 'Solo fuori', icon: '🪟' },
]; ];
export const PETS_OPTIONS: PreferenceOption<PetsPreference>[] = [ export const PETS_OPTIONS: PreferenceOption<PetsPreference>[] = [
@@ -817,14 +843,14 @@ export const PETS_OPTIONS: PreferenceOption<PetsPreference>[] = [
{ value: 'small', label: 'Taglia piccola', icon: '🐕' }, { value: 'small', label: 'Taglia piccola', icon: '🐕' },
{ value: 'medium', label: 'Taglia media', icon: '🐕‍🦺' }, { value: 'medium', label: 'Taglia media', icon: '🐕‍🦺' },
{ value: 'large', label: 'Taglia grande', icon: '🦮' }, { value: 'large', label: 'Taglia grande', icon: '🦮' },
{ value: 'all', label: 'Tutti OK', icon: '🐾' } { value: 'all', label: 'Tutti OK', icon: '🐾' },
]; ];
export const LUGGAGE_OPTIONS: PreferenceOption<LuggageSize>[] = [ export const LUGGAGE_OPTIONS: PreferenceOption<LuggageSize>[] = [
{ value: 'none', label: 'Nessun bagaglio', icon: '🎒' }, { value: 'none', label: 'Nessun bagaglio', icon: '🎒' },
{ value: 'small', label: 'Piccolo', icon: '👜' }, { value: 'small', label: 'Piccolo', icon: '👜' },
{ value: 'medium', label: 'Medio', icon: '🧳' }, { value: 'medium', label: 'Medio', icon: '🧳' },
{ value: 'large', label: 'Grande', icon: '🛄' } { value: 'large', label: 'Grande', icon: '🛄' },
]; ];
export const MUSIC_OPTIONS: PreferenceOption<MusicPreference>[] = [ export const MUSIC_OPTIONS: PreferenceOption<MusicPreference>[] = [
@@ -832,21 +858,21 @@ export const MUSIC_OPTIONS: PreferenceOption<MusicPreference>[] = [
{ value: 'quiet', label: 'Sottofondo', icon: '🔈' }, { value: 'quiet', label: 'Sottofondo', icon: '🔈' },
{ value: 'moderate', label: 'Normale', icon: '🔉' }, { value: 'moderate', label: 'Normale', icon: '🔉' },
{ value: 'loud', label: 'Alta', icon: '🔊' }, { value: 'loud', label: 'Alta', icon: '🔊' },
{ value: 'passenger_choice', label: 'Scelgono i passeggeri', icon: '🎵' } { value: 'passenger_choice', label: 'Scelgono i passeggeri', icon: '🎵' },
]; ];
export const CONVERSATION_OPTIONS: PreferenceOption<ConversationPreference>[] = [ export const CONVERSATION_OPTIONS: PreferenceOption<ConversationPreference>[] = [
{ value: 'quiet', label: 'Preferisco silenzio', icon: '🤫' }, { value: 'quiet', label: 'Preferisco silenzio', icon: '🤫' },
{ value: 'moderate', label: 'Chiacchierata leggera', icon: '💬' }, { value: 'moderate', label: 'Chiacchierata leggera', icon: '💬' },
{ value: 'chatty', label: 'Amo conversare', icon: '🗣️' } { value: 'chatty', label: 'Amo conversare', icon: '🗣️' },
]; ];
export const VEHICLE_TYPES: PreferenceOption<VehicleType>[] = [ export const VEHICLE_TYPES: PreferenceOption<string>[] = [
{ value: 'auto', label: 'Auto', icon: '🚗' }, { value: 'auto', label: 'Auto', icon: '🚗' },
{ value: 'moto', label: 'Moto', icon: '🏍️' }, { value: 'moto', label: 'Moto', icon: '🏍️' },
{ value: 'furgone', label: 'Furgone', icon: '🚐' }, { value: 'furgone', label: 'Furgone', icon: '🚐' },
{ value: 'minibus', label: 'Minibus', icon: '🚌' }, { value: 'minibus', label: 'Minibus', icon: '🚌' },
{ value: 'altro', label: 'Altro', icon: '🚙' } { value: 'altro', label: 'Altro', icon: '🚙' },
]; ];
export const VEHICLE_COLORS: VehicleColorOption[] = [ export const VEHICLE_COLORS: VehicleColorOption[] = [
@@ -861,7 +887,7 @@ export const VEHICLE_COLORS: VehicleColorOption[] = [
{ name: 'Arancione', hex: '#FFA500' }, { name: 'Arancione', hex: '#FFA500' },
{ name: 'Marrone', hex: '#8B4513' }, { name: 'Marrone', hex: '#8B4513' },
{ name: 'Beige', hex: '#F5F5DC' }, { name: 'Beige', hex: '#F5F5DC' },
{ name: 'Bordeaux', hex: '#800020' } { name: 'Bordeaux', hex: '#800020' },
]; ];
export const FEEDBACK_TAGS_OPTIONS: FeedbackTagOption[] = [ export const FEEDBACK_TAGS_OPTIONS: FeedbackTagOption[] = [
@@ -869,7 +895,12 @@ export const FEEDBACK_TAGS_OPTIONS: FeedbackTagOption[] = [
{ value: 'gentile', label: 'Gentile', icon: '😊', isPositive: true }, { value: 'gentile', label: 'Gentile', icon: '😊', isPositive: true },
{ value: 'auto_pulita', label: 'Auto pulita', icon: '✨', isPositive: true }, { value: 'auto_pulita', label: 'Auto pulita', icon: '✨', isPositive: true },
{ value: 'guida_sicura', label: 'Guida sicura', icon: '🛡️', isPositive: true }, { value: 'guida_sicura', label: 'Guida sicura', icon: '🛡️', isPositive: true },
{ value: 'buona_conversazione', label: 'Buona conversazione', icon: '💬', isPositive: true }, {
value: 'buona_conversazione',
label: 'Buona conversazione',
icon: '💬',
isPositive: true,
},
{ value: 'silenzioso', label: 'Rispetta il silenzio', icon: '🤫', isPositive: true }, { value: 'silenzioso', label: 'Rispetta il silenzio', icon: '🤫', isPositive: true },
{ value: 'flessibile', label: 'Flessibile', icon: '🤸', isPositive: true }, { value: 'flessibile', label: 'Flessibile', icon: '🤸', isPositive: true },
{ value: 'rispettoso', label: 'Rispettoso', icon: '🙏', isPositive: true }, { value: 'rispettoso', label: 'Rispettoso', icon: '🙏', isPositive: true },
@@ -879,23 +910,26 @@ export const FEEDBACK_TAGS_OPTIONS: FeedbackTagOption[] = [
{ value: 'scortese', label: 'Scortese', icon: '😤', isPositive: false }, { value: 'scortese', label: 'Scortese', icon: '😤', isPositive: false },
{ value: 'guida_pericolosa', label: 'Guida pericolosa', icon: '⚠️', isPositive: false }, { value: 'guida_pericolosa', label: 'Guida pericolosa', icon: '⚠️', isPositive: false },
{ value: 'auto_sporca', label: 'Auto sporca', icon: '🗑️', isPositive: false }, { value: 'auto_sporca', label: 'Auto sporca', icon: '🗑️', isPositive: false },
{ value: 'non_rispettoso', label: 'Non rispettoso', icon: '👎', isPositive: false } { value: 'non_rispettoso', label: 'Non rispettoso', icon: '👎', isPositive: false },
]; ];
export const RECURRENCE_TYPE_OPTIONS: PreferenceOption<RecurrenceType>[] = [ export const RECURRENCE_TYPE_OPTIONS: PreferenceOption<RecurrenceType>[] = [
{ value: 'once', label: 'Una volta sola', icon: '1⃣' }, { value: 'once', label: 'Una volta sola', icon: '1⃣' },
{ value: 'weekly', label: 'Ogni settimana', icon: '🔄' }, { value: 'weekly', label: 'Ogni settimana', icon: '🔄' },
{ value: 'custom_days', label: 'Giorni specifici', icon: '📅' }, { value: 'custom_days', label: 'Giorni specifici', icon: '📅' },
{ value: 'custom_dates', label: 'Date personalizzate', icon: '🗓️' } { value: 'custom_dates', label: 'Date personalizzate', icon: '🗓️' },
]; ];
export const VEHICLE_FEATURES_OPTIONS: PreferenceOption<VehicleFeature>[] = [ export const VEHICLE_FEATURES_OPTIONS: PreferenceOption<string>[] = [
{ value: 'aria_condizionata', label: 'Aria condizionata', icon: '❄️' }, { value: 'aria_condizionata', label: 'Aria condizionata', icon: '❄️' },
{ value: 'wifi', label: 'WiFi', icon: '📶' }, { value: 'wifi', label: 'WiFi', icon: '📶' },
{ value: 'presa_usb', label: 'Presa USB', icon: '🔌' }, { value: 'presa_usb', label: 'Presa USB', icon: '🔌' },
{ value: 'bluetooth', label: 'Bluetooth', icon: '🔵' }, { value: 'bluetooth', label: 'Bluetooth', icon: '🔵' },
{ value: 'bagagliaio_grande', label: 'Bagagliaio grande', icon: '🧳' }, { value: 'bagagliaio_grande', label: 'Bagagliaio grande', icon: '🧳' },
{ value: 'seggiolino_bimbi', label: 'Seggiolino bimbi', icon: '👶' } { value: 'seggiolino_bimbi', label: 'Seggiolino bimbi', icon: '👶' },
{ value: 'animali', label: 'Animali ammessi', icon: 'pets' },
{ value: 'fumatori', label: 'Si può fumare', icon: 'smoking_rooms' },
{ value: 'no_fumatori', label: 'Vietato fumare', icon: 'smoke_free' },
]; ];
// ============================================================ // ============================================================

View File

@@ -3,7 +3,7 @@
// ============================================================ // ============================================================
// File: /myprojplanet_vite/src/modules/viaggi/routes/routes_trasporti.ts // File: /myprojplanet_vite/src/modules/viaggi/routes/routes_trasporti.ts
import { ISites, IListRoutes } from '@/model'; // Adatta al tuo path import { ISites, IListRoutes } from '@/model';
/** /**
* Genera le routes per il modulo Trasporti * Genera le routes per il modulo Trasporti
@@ -17,7 +17,7 @@ export function getroutesViaggi(site: ISites) {
const routes_trasporti: IListRoutes[] = [ const routes_trasporti: IListRoutes[] = [
// ---------------------------------------------------------- // ----------------------------------------------------------
// 🏠 HOME & DASHBOARD. (📋 LISTA & RICERCA) // 🏠 HOMEPAGE - Community Viaggi (Pagina Principale)
// ---------------------------------------------------------- // ----------------------------------------------------------
{ {
active: true, active: true,
@@ -26,37 +26,24 @@ export function getroutesViaggi(site: ISites) {
materialIcon: 'commute', materialIcon: 'commute',
faIcon: 'fas fa-car-side', faIcon: 'fas fa-car-side',
name: 'mypages.TrasportiHome', name: 'mypages.TrasportiHome',
component: () => import('@/modules/viaggi/pages/RidesListPage.vue'), component: () => import('@/modules/viaggi/pages/CommunityRidesPage.vue'),
inmenu: false, // È già nel menu principale
submenu: false,
level_parent: 0,
level_child: 0,
meta: {
requiresAuth: false,
title: 'Viaggi Disponibili',
description: 'Esplora i viaggi disponibili nella community'
}
},
{
active: true,
order: 301,
path: '/viaggi/dashboard',
materialIcon: 'dashboard',
faIcon: 'fas fa-tachometer-alt',
name: 'mypages.TrasportiDashboard',
component: () => import('@/modules/viaggi/pages/MyRidesPage.vue'),
inmenu: true, inmenu: true,
submenu: true, submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 1,
meta: { meta: {
requiresAuth: true, requiresAuth: false,
title: 'Dashboard Viaggi' title: 'Viaggi Community',
description: 'Esplora i viaggi disponibili nella community'
} }
}, },
// ----------------------------------------------------------
// 🔍 CERCA & MAPPA
// ----------------------------------------------------------
{ {
active: true, active: true,
order: 311, order: 310,
path: '/viaggi/cerca', path: '/viaggi/cerca',
materialIcon: 'search', materialIcon: 'search',
faIcon: 'fas fa-search', faIcon: 'fas fa-search',
@@ -65,25 +52,24 @@ export function getroutesViaggi(site: ISites) {
inmenu: true, inmenu: true,
submenu: true, submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 2,
meta: { meta: {
requiresAuth: false, requiresAuth: false,
title: 'Cerca Passaggio' title: 'Cerca Viaggio'
} }
}, },
{ {
active: true, active: true,
order: 312, order: 311,
path: '/viaggi/mappa', path: '/viaggi/mappa',
materialIcon: 'map', materialIcon: 'map',
faIcon: 'fas fa-map-marked-alt', faIcon: 'fas fa-map-marked-alt',
name: 'mypages.TrasportiMappa', name: 'mypages.TrasportiMappa',
component: () => import('@/modules/viaggi/pages/RideSearchPage.vue'), component: () => import('@/modules/viaggi/pages/RideSearchPage.vue'),
// props: { defaultView: 'map' }, inmenu: false, // Integrata come tab nella ricerca
inmenu: true, submenu: false,
submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 0,
meta: { meta: {
requiresAuth: false, requiresAuth: false,
title: 'Mappa Viaggi' title: 'Mappa Viaggi'
@@ -91,38 +77,20 @@ export function getroutesViaggi(site: ISites) {
}, },
// ---------------------------------------------------------- // ----------------------------------------------------------
// CREA VIAGGIO // CREA/OFFRI/RICHIEDI VIAGGIO
// ---------------------------------------------------------- // ----------------------------------------------------------
{ {
active: true, active: true,
order: 320, order: 320,
path: '/viaggi/crea',
materialIcon: 'add_circle',
faIcon: 'fas fa-plus-circle',
name: 'mypages.TrasportiCrea',
component: () => import('@/modules/viaggi/pages/RideCreatePage.vue'),
inmenu: true,
submenu: true,
level_parent: 0,
level_child: 0.5,
meta: {
requiresAuth: true,
title: 'Crea Viaggio'
}
},
{
active: true,
order: 321,
path: '/viaggi/offri', path: '/viaggi/offri',
materialIcon: 'directions_car', materialIcon: 'directions_car',
faIcon: 'fas fa-car', faIcon: 'fas fa-car',
name: 'mypages.TrasportiOffri', name: 'mypages.TrasportiOffri',
component: () => import('@/modules/viaggi/pages/RideCreatePage.vue'), component: () => import('@/modules/viaggi/pages/RideCreatePage.vue'),
// props: { defaultType: 'offer' },
inmenu: true, inmenu: true,
submenu: true, submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 3,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Offri Passaggio' title: 'Offri Passaggio'
@@ -130,22 +98,38 @@ export function getroutesViaggi(site: ISites) {
}, },
{ {
active: true, active: true,
order: 322, order: 321,
path: '/viaggi/richiedi', path: '/viaggi/richiedi',
materialIcon: 'hail', materialIcon: 'hail',
faIcon: 'fas fa-hand-paper', faIcon: 'fas fa-hand-paper',
name: 'mypages.TrasportiRichiedi', name: 'mypages.TrasportiRichiedi',
component: () => import('@/modules/viaggi/pages/RideCreatePage.vue'), component: () => import('@/modules/viaggi/pages/RideCreatePage.vue'),
// props: { defaultType: 'request' }, inmenu: false, // Disponibile tramite FAB/quick action
inmenu: true, submenu: false,
submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Richiedi Passaggio' title: 'Richiedi Passaggio'
} }
}, },
{
active: true,
order: 322,
path: '/viaggi/crea',
materialIcon: 'add_circle',
faIcon: 'fas fa-plus-circle',
name: 'mypages.TrasportiCrea',
component: () => import('@/modules/viaggi/pages/RideCreatePage.vue'),
inmenu: false, // Pagina generica, si usano offri/richiedi
submenu: false,
level_parent: 0,
level_child: 0,
meta: {
requiresAuth: true,
title: 'Crea Viaggio'
}
},
// ---------------------------------------------------------- // ----------------------------------------------------------
// 📄 DETTAGLIO & MODIFICA VIAGGIO // 📄 DETTAGLIO & MODIFICA VIAGGIO
@@ -161,7 +145,7 @@ export function getroutesViaggi(site: ISites) {
inmenu: false, inmenu: false,
submenu: false, submenu: false,
level_parent: 0, level_parent: 0,
level_child: 1, level_child: 0,
meta: { meta: {
requiresAuth: false, requiresAuth: false,
title: 'Dettaglio Viaggio' title: 'Dettaglio Viaggio'
@@ -175,11 +159,10 @@ export function getroutesViaggi(site: ISites) {
faIcon: 'fas fa-edit', faIcon: 'fas fa-edit',
name: 'mypages.TrasportiModifica', name: 'mypages.TrasportiModifica',
component: () => import('@/modules/viaggi/pages/RideCreatePage.vue'), component: () => import('@/modules/viaggi/pages/RideCreatePage.vue'),
// props: { editMode: true },
inmenu: false, inmenu: false,
submenu: false, submenu: false,
level_parent: 0, level_parent: 0,
level_child: 1, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Modifica Viaggio' title: 'Modifica Viaggio'
@@ -187,12 +170,12 @@ export function getroutesViaggi(site: ISites) {
}, },
// ---------------------------------------------------------- // ----------------------------------------------------------
// 👤 I MIEI VIAGGI // 📋 I MIEI VIAGGI (Dashboard unificata con tabs)
// ---------------------------------------------------------- // ----------------------------------------------------------
{ {
active: true, active: true,
order: 340, order: 340,
path: '/viaggi/rides/my', path: '/viaggi/miei-viaggi',
materialIcon: 'folder_shared', materialIcon: 'folder_shared',
faIcon: 'fas fa-folder-open', faIcon: 'fas fa-folder-open',
name: 'mypages.TrasportiMieiViaggi', name: 'mypages.TrasportiMieiViaggi',
@@ -200,25 +183,60 @@ export function getroutesViaggi(site: ISites) {
inmenu: true, inmenu: true,
submenu: true, submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 4,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'I Miei Viaggi' title: 'I Miei Viaggi'
} }
}, },
// Alias/redirect per retrocompatibilità
{ {
active: true, active: true,
order: 341, order: 341,
path: '/viaggi/dashboard',
materialIcon: 'dashboard',
faIcon: 'fas fa-tachometer-alt',
name: 'mypages.TrasportiDashboard',
component: () => import('@/modules/viaggi/pages/MyRidesPage.vue'),
inmenu: false,
submenu: false,
level_parent: 0,
level_child: 0,
meta: {
requiresAuth: true,
title: 'Dashboard Viaggi'
}
},
{
active: true,
order: 342,
path: '/viaggi/rides/my',
materialIcon: 'folder_shared',
faIcon: 'fas fa-folder-open',
name: 'mypages.TrasportiMyRides',
component: () => import('@/modules/viaggi/pages/MyRidesPage.vue'),
inmenu: false,
submenu: false,
level_parent: 0,
level_child: 0,
meta: {
requiresAuth: true,
title: 'I Miei Viaggi'
}
},
// Sottopagine per navigazione diretta (non in menu)
{
active: true,
order: 343,
path: '/viaggi/rides/my/conducente', path: '/viaggi/rides/my/conducente',
materialIcon: 'drive_eta', materialIcon: 'drive_eta',
faIcon: 'fas fa-car-side', faIcon: 'fas fa-car-side',
name: 'mypages.TrasportiMieiViaggiDriver', name: 'mypages.TrasportiMieiViaggiDriver',
component: () => import('@/modules/viaggi/pages/MyRidesPage.vue'), component: () => import('@/modules/viaggi/pages/MyRidesPage.vue'),
// props: { defaultTab: 'driver' },
inmenu: false, inmenu: false,
submenu: false, submenu: false,
level_parent: 0, level_parent: 0,
level_child: 1, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Viaggi come Conducente' title: 'Viaggi come Conducente'
@@ -226,17 +244,16 @@ export function getroutesViaggi(site: ISites) {
}, },
{ {
active: true, active: true,
order: 342, order: 344,
path: '/viaggi/miei-viaggi/passeggero', path: '/viaggi/miei-viaggi/passeggero',
materialIcon: 'airline_seat_recline_normal', materialIcon: 'airline_seat_recline_normal',
faIcon: 'fas fa-user', faIcon: 'fas fa-user',
name: 'mypages.TrasportiMieiViaggiPassenger', name: 'mypages.TrasportiMieiViaggiPassenger',
component: () => import('@/modules/viaggi/pages/MyRidesPage.vue'), component: () => import('@/modules/viaggi/pages/MyRidesPage.vue'),
// props: { defaultTab: 'passenger' },
inmenu: false, inmenu: false,
submenu: false, submenu: false,
level_parent: 0, level_parent: 0,
level_child: 1, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Viaggi come Passeggero' title: 'Viaggi come Passeggero'
@@ -244,17 +261,16 @@ export function getroutesViaggi(site: ISites) {
}, },
{ {
active: true, active: true,
order: 343, order: 345,
path: '/viaggi/storico', path: '/viaggi/storico',
materialIcon: 'history', materialIcon: 'history',
faIcon: 'fas fa-history', faIcon: 'fas fa-history',
name: 'mypages.TrasportiStorico', name: 'mypages.TrasportiStorico',
component: () => import('@/modules/viaggi/pages/MyRidesPage.vue'), component: () => import('@/modules/viaggi/pages/MyRidesPage.vue'),
// props: { defaultTab: 'history' }, inmenu: false, // Integrato come tab in "I Miei Viaggi"
inmenu: true, submenu: false,
submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Storico Viaggi' title: 'Storico Viaggi'
@@ -275,24 +291,25 @@ export function getroutesViaggi(site: ISites) {
inmenu: true, inmenu: true,
submenu: true, submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 5,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Messaggi' title: 'Chat',
// badge per messaggi non letti
} }
}, },
{ {
active: true, active: true,
order: 351, order: 351,
path: '/viaggi/chat/:id', path: '/viaggi/chat/:chatId',
materialIcon: 'chat_bubble', materialIcon: 'forum',
faIcon: 'fas fa-comment', faIcon: 'fas fa-comment-dots',
name: 'mypages.TrasportiChatDetail', name: 'mypages.TrasportiChat',
component: () => import('@/modules/viaggi/pages/ChatPage.vue'), component: () => import('@/modules/viaggi/pages/ChatPage.vue'),
inmenu: false, inmenu: false,
submenu: false, submenu: false,
level_parent: 0, level_parent: 0,
level_child: 1, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Conversazione' title: 'Conversazione'
@@ -300,41 +317,40 @@ export function getroutesViaggi(site: ISites) {
}, },
// ---------------------------------------------------------- // ----------------------------------------------------------
// 👨‍✈️ PROFILO CONDUCENTE // 👤 PROFILO
// ---------------------------------------------------------- // ----------------------------------------------------------
{ {
active: true, active: true,
order: 360, order: 360,
path: '/viaggi/profilo/:userId', path: '/viaggi/profilo',
materialIcon: 'person', materialIcon: 'person',
faIcon: 'fas fa-user', faIcon: 'fas fa-user',
name: 'mypages.TrasportiProfiloDriver', name: 'mypages.TrasportiProfiloDriver',
component: () => import('@/modules/viaggi/pages/DriverProfilePage.vue'), component: () => import('@/modules/viaggi/pages/DriverProfilePage.vue'),
inmenu: false, inmenu: true,
submenu: false, submenu: true,
level_parent: 0, level_parent: 0,
level_child: 1, level_child: 6,
meta: { meta: {
requiresAuth: false, requiresAuth: true,
title: 'Profilo Conducente' title: 'Il Mio Profilo'
} }
}, },
{ {
active: true, active: true,
order: 361, order: 361,
path: '/viaggi/mio-profilo', path: '/viaggi/profilo/:userId',
materialIcon: 'account_circle', materialIcon: 'account_circle',
faIcon: 'fas fa-user-circle', faIcon: 'fas fa-user-circle',
name: 'mypages.TrasportiMioProfilo', name: 'mypages.TrasportiProfiloUtente',
component: () => import('@/modules/viaggi/pages/DriverProfilePage.vue'), component: () => import('@/modules/viaggi/pages/DriverProfilePage.vue'),
// props: { isOwnProfile: true }, inmenu: false,
inmenu: true, submenu: false,
submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: false,
title: 'Il Mio Profilo' title: 'Profilo Utente'
} }
}, },
@@ -352,7 +368,7 @@ export function getroutesViaggi(site: ISites) {
inmenu: true, inmenu: true,
submenu: true, submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 7,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'I Miei Veicoli' title: 'I Miei Veicoli'
@@ -369,7 +385,7 @@ export function getroutesViaggi(site: ISites) {
inmenu: false, inmenu: false,
submenu: false, submenu: false,
level_parent: 0, level_parent: 0,
level_child: 1, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Aggiungi Veicolo' title: 'Aggiungi Veicolo'
@@ -383,11 +399,10 @@ export function getroutesViaggi(site: ISites) {
faIcon: 'fas fa-edit', faIcon: 'fas fa-edit',
name: 'mypages.TrasportiVeicoloModifica', name: 'mypages.TrasportiVeicoloModifica',
component: () => import('@/modules/viaggi/pages/Vehicleeditpage.vue'), component: () => import('@/modules/viaggi/pages/Vehicleeditpage.vue'),
// props: { editMode: true },
inmenu: false, inmenu: false,
submenu: false, submenu: false,
level_parent: 0, level_parent: 0,
level_child: 1, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Modifica Veicolo' title: 'Modifica Veicolo'
@@ -400,15 +415,32 @@ export function getroutesViaggi(site: ISites) {
{ {
active: true, active: true,
order: 380, order: 380,
path: '/viaggi/feedback/viaggio/:rideId', path: '/viaggi/miei-feedback',
materialIcon: 'rate_review', materialIcon: 'reviews',
faIcon: 'fas fa-star', faIcon: 'fas fa-star',
name: 'mypages.TrasportiMieiFeedback',
component: () => import('@/modules/viaggi/pages/Myfeedbackpage.vue'),
inmenu: true,
submenu: true,
level_parent: 0,
level_child: 8,
meta: {
requiresAuth: true,
title: 'I Miei Feedback'
}
},
{
active: true,
order: 381,
path: '/viaggi/feedback/viaggi/:rideId',
materialIcon: 'rate_review',
faIcon: 'fas fa-star-half-alt',
name: 'mypages.TrasportiFeedbackRide', name: 'mypages.TrasportiFeedbackRide',
component: () => import('@/modules/viaggi/pages/Myfeedbackpage.vue'), component: () => import('@/modules/viaggi/pages/Myfeedbackpage.vue'),
inmenu: false, inmenu: false,
submenu: false, submenu: false,
level_parent: 0, level_parent: 0,
level_child: 1, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Lascia Feedback' title: 'Lascia Feedback'
@@ -416,38 +448,21 @@ export function getroutesViaggi(site: ISites) {
}, },
{ {
active: true, active: true,
order: 381, order: 382,
path: '/viaggi/feedback/viaggio/:rideId/utente/:toUserId', path: '/viaggi/feedback/viaggi/:rideId/utente/:toUserId',
materialIcon: 'star_rate', materialIcon: 'star_rate',
faIcon: 'fas fa-star-half-alt', faIcon: 'fas fa-star',
name: 'mypages.TrasportiFeedbackUser', name: 'mypages.TrasportiFeedbackUser',
component: () => import('@/modules/viaggi/pages/Myfeedbackpage.vue'), component: () => import('@/modules/viaggi/pages/Myfeedbackpage.vue'),
inmenu: false, inmenu: false,
submenu: false, submenu: false,
level_parent: 0, level_parent: 0,
level_child: 1, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Valuta Utente' title: 'Valuta Utente'
} }
}, },
{
active: true,
order: 382,
path: '/viaggi/miei-feedback',
materialIcon: 'reviews',
faIcon: 'fas fa-comments',
name: 'mypages.TrasportiMieiFeedback',
component: () => import('@/modules/viaggi/pages/Myfeedbackpage.vue'),
inmenu: true,
submenu: true,
level_parent: 0,
level_child: 0.5,
meta: {
requiresAuth: true,
title: 'I Miei Feedback'
}
},
// ---------------------------------------------------------- // ----------------------------------------------------------
// 📩 RICHIESTE PASSAGGIO // 📩 RICHIESTE PASSAGGIO
@@ -463,7 +478,7 @@ export function getroutesViaggi(site: ISites) {
inmenu: true, inmenu: true,
submenu: true, submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 9,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Richieste Passaggio' title: 'Richieste Passaggio'
@@ -477,11 +492,10 @@ export function getroutesViaggi(site: ISites) {
faIcon: 'fas fa-inbox', faIcon: 'fas fa-inbox',
name: 'mypages.TrasportiRichiesteRicevute', name: 'mypages.TrasportiRichiesteRicevute',
component: () => import('@/modules/viaggi/pages/Requestspage.vue'), component: () => import('@/modules/viaggi/pages/Requestspage.vue'),
// props: { defaultTab: 'received' },
inmenu: false, inmenu: false,
submenu: false, submenu: false,
level_parent: 0, level_parent: 0,
level_child: 1, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Richieste Ricevute' title: 'Richieste Ricevute'
@@ -495,11 +509,10 @@ export function getroutesViaggi(site: ISites) {
faIcon: 'fas fa-paper-plane', faIcon: 'fas fa-paper-plane',
name: 'mypages.TrasportiRichiesteInviate', name: 'mypages.TrasportiRichiesteInviate',
component: () => import('@/modules/viaggi/pages/Requestspage.vue'), component: () => import('@/modules/viaggi/pages/Requestspage.vue'),
// props: { defaultTab: 'sent' },
inmenu: false, inmenu: false,
submenu: false, submenu: false,
level_parent: 0, level_parent: 0,
level_child: 1, level_child: 0,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Richieste Inviate' title: 'Richieste Inviate'
@@ -520,10 +533,26 @@ export function getroutesViaggi(site: ISites) {
inmenu: true, inmenu: true,
submenu: true, submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 10,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
title: 'Impostazioni Viaggi' title: 'Impostazioni'
}
},
{
active: true,
order: 400,
path: '/viaggi/notifiche',
materialIcon: 'notifications',
name: 'mypages.TrasportiNotifiche',
component: () => import('@/modules/viaggi/pages/NotificationsPage.vue'),
inmenu: true,
submenu: true,
level_parent: 0,
level_child: 10,
meta: {
requiresAuth: true,
title: 'Notifiche'
} }
}, },
@@ -541,12 +570,31 @@ export function getroutesViaggi(site: ISites) {
inmenu: true, inmenu: true,
submenu: true, submenu: true,
level_parent: 0, level_parent: 0,
level_child: 0.5, level_child: 11,
meta: { meta: {
requiresAuth: false, requiresAuth: false,
title: 'Come Funziona' title: 'Come Funziona'
} }
}, },
// ----------------------------------------------------------
// 🔗 ALIAS LEGACY (per retrocompatibilità)
// ----------------------------------------------------------
{
active: true,
order: 999,
path: '/viaggi/community',
name: 'mypages.TrasportiCommunityLegacy',
component: () => import('@/modules/viaggi/pages/CommunityRidesPage.vue'),
inmenu: false,
submenu: false,
level_parent: 0,
level_child: 0,
meta: {
requiresAuth: false,
title: 'Community'
}
},
]; ];
// ============================================================ // ============================================================
@@ -556,7 +604,7 @@ export function getroutesViaggi(site: ISites) {
{ {
active: site.confpages?.enableTrasporti ?? true, active: site.confpages?.enableTrasporti ?? true,
path: '/viaggi', path: '/viaggi',
order: 1402, order: 302,
faIcon: 'fas fa-car-side', faIcon: 'fas fa-car-side',
materialIcon: 'commute', materialIcon: 'commute',
name: 'mypages.menuTrasporti', name: 'mypages.menuTrasporti',
@@ -567,10 +615,10 @@ export function getroutesViaggi(site: ISites) {
solotitle: true, solotitle: true,
infooter: true, infooter: true,
// Badge per notifiche non lette // Badge per notifiche non lette
//badge: { badge: {
// getter: 'viaggi/getTotalNotifications', getter: 'viaggi/getTotalNotifications',
// color: 'negative' color: 'negative'
//} },
}, },
// Spread di tutte le routes per il Vue Router // Spread di tutte le routes per il Vue Router
...routes_trasporti, ...routes_trasporti,
@@ -591,13 +639,13 @@ export function getTrasportiQuickActions() {
path: '/viaggi/offri' path: '/viaggi/offri'
}, },
{ {
label: 'Cerca Passaggio', label: 'Richiedi Passaggio',
icon: 'hail', icon: 'hail',
color: 'info', color: 'info',
path: '/viaggi/richiedi' path: '/viaggi/richiedi'
}, },
{ {
label: 'Cerca', label: 'Cerca Viaggio',
icon: 'search', icon: 'search',
color: 'primary', color: 'primary',
path: '/viaggi/cerca' path: '/viaggi/cerca'
@@ -616,4 +664,5 @@ export function getTrasportiQuickActions() {
// ============================================================ // ============================================================
export const routesViaggi = { export const routesViaggi = {
routesViaggi: getroutesViaggi, routesViaggi: getroutesViaggi,
getTrasportiQuickActions
}; };

View File

@@ -1298,11 +1298,11 @@ const msg_it = {
date_updated: 'Ult. Aggiornamento', date_updated: 'Ult. Aggiornamento',
}, },
mypages: { mypages: {
TrasportiHome: 'Viaggi', TrasportiHome: 'Viaggi Community',
TrasportiDashboard: 'Dashboard', TrasportiDashboard: 'Dashboard',
TrasportiLista: 'Lista Viaggi', TrasportiLista: 'Lista Viaggi',
TrasportiCerca: 'Cerca Passaggio', TrasportiCerca: 'Cerca Passaggio',
TrasportiMappa: 'Mappa', TrasportiMappa: 'Mappa Viaggi',
TrasportiCrea: 'Nuovo Viaggio', TrasportiCrea: 'Nuovo Viaggio',
TrasportiOffri: 'Offri Passaggio', TrasportiOffri: 'Offri Passaggio',
TrasportiRichiedi: 'Cerca Passaggio', TrasportiRichiedi: 'Cerca Passaggio',
@@ -1325,7 +1325,9 @@ const msg_it = {
TrasportiRichieste: 'Richieste', TrasportiRichieste: 'Richieste',
TrasportiRichiesteRicevute: 'Ricevute', TrasportiRichiesteRicevute: 'Ricevute',
TrasportiRichiesteInviate: 'Inviate', TrasportiRichiesteInviate: 'Inviate',
CommunityRides: 'Viaggi Comunitari',
TrasportiSettings: 'Impostazioni', TrasportiSettings: 'Impostazioni',
TrasportiNotifiche: 'Notifiche',
TrasportiHelp: 'Come Funziona', TrasportiHelp: 'Come Funziona',
menuTrasporti: 'Viaggi', menuTrasporti: 'Viaggi',

View File

@@ -1073,13 +1073,6 @@ export const useGlobalStore = defineStore('GlobalStore', {
}, },
async createPushSubscription() { async createPushSubscription() {
// console.log('ENTER TO createPushSubscription')
// If Already subscribed, don't send to the Server DB
// if (state.wasAlreadySubOnDb) {
// // console.log('wasAlreadySubOnDb!')
// return
// }
if (!this.site.confpages || !this.site.confpages.enablePwa) return; if (!this.site.confpages || !this.site.confpages.enablePwa) return;
if (!('serviceWorker' in navigator)) { if (!('serviceWorker' in navigator)) {
@@ -1144,9 +1137,6 @@ export const useGlobalStore = defineStore('GlobalStore', {
return; return;
} }
// console.log('saveSubscriptionToServer: ', newSub)
// console.log('context', context)
const options = null; const options = null;
let notreg = false; let notreg = false;
@@ -3292,7 +3282,7 @@ export const useGlobalStore = defineStore('GlobalStore', {
async convertPdf( async convertPdf(
pdfFile: any, pdfFile: any,
salvasufiledascaricare, salvasufiledascaricare: boolean,
width: string, width: string,
height: string, height: string,
compressione: string, compressione: string,