655 lines
16 KiB
TypeScript
655 lines
16 KiB
TypeScript
import { ref, reactive, computed, watch } from 'vue';
|
|
import { Api } from '@api'; // Adatta al tuo path
|
|
import type {
|
|
Ride,
|
|
RideFormData,
|
|
RideSearchFilters,
|
|
RideType,
|
|
RideStatus,
|
|
ApiResponse,
|
|
PaginatedResponse,
|
|
MyRidesResponse,
|
|
RidesStatsResponse,
|
|
Waypoint,
|
|
Location,
|
|
} from '../types';
|
|
|
|
const STOR_TRASP_CITIE = 'trasp_ride_filters';
|
|
|
|
// ============================================================
|
|
// STATE
|
|
// ============================================================
|
|
|
|
const rides = ref<Ride[]>([]);
|
|
const currentRide = ref<Ride | null>(null);
|
|
const myRides = reactive<{
|
|
all: Ride[];
|
|
upcoming: Ride[];
|
|
past: Ride[];
|
|
}>({
|
|
all: [],
|
|
upcoming: [],
|
|
past: [],
|
|
});
|
|
const stats = ref<RidesStatsResponse | null>(null);
|
|
|
|
const loading = ref(false);
|
|
const error = ref<string | null>(null);
|
|
|
|
const pagination = reactive({
|
|
page: 1,
|
|
limit: 20,
|
|
total: 0,
|
|
pages: 0,
|
|
});
|
|
|
|
// Carica filtri da localStorage
|
|
const loadFiltersFromStorage = (): RideSearchFilters => {
|
|
try {
|
|
const stored = localStorage.getItem(STOR_TRASP_CITIE);
|
|
if (stored) {
|
|
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
|
|
// ============================================================
|
|
|
|
export function useRides() {
|
|
// ------------------------------------------------------------
|
|
// COMPUTED
|
|
// ------------------------------------------------------------
|
|
|
|
const hasRides = computed(() => rides.value.length > 0);
|
|
const hasMorePages = computed(() => pagination.page < pagination.pages);
|
|
|
|
const offersCount = computed(() =>
|
|
Array.isArray(rides.value) ? rides.value.filter((r) => r.type === 'offer').length : 0
|
|
);
|
|
|
|
const requestsCount = computed(() =>
|
|
Array.isArray(rides.value)
|
|
? rides.value.filter((r) => r.type === 'request').length
|
|
: 0
|
|
);
|
|
|
|
// ✅ Fixed: was returning array instead of count, and added .length
|
|
const activeRides = computed(() =>
|
|
Array.isArray(rides.value)
|
|
? rides.value.filter((r) => ['active', 'full'].includes(r.status)).length
|
|
: 0
|
|
);
|
|
|
|
// ------------------------------------------------------------
|
|
// API CALLS
|
|
// ------------------------------------------------------------
|
|
|
|
/**
|
|
* Ottieni lista viaggi con filtri
|
|
*/
|
|
const fetchRides = async (
|
|
options: {
|
|
reset?: boolean;
|
|
filters?: RideSearchFilters;
|
|
} = {}
|
|
) => {
|
|
try {
|
|
loading.value = true;
|
|
error.value = null;
|
|
|
|
if (options.reset) {
|
|
pagination.page = 1;
|
|
rides.value = [];
|
|
}
|
|
|
|
if (options.filters) {
|
|
Object.assign(filters, options.filters);
|
|
}
|
|
|
|
const queryParams = new URLSearchParams();
|
|
queryParams.append('page', pagination.page.toString());
|
|
queryParams.append('limit', pagination.limit.toString());
|
|
|
|
if (filters.type) queryParams.append('type', filters.type);
|
|
if (filters.from) queryParams.append('departureCity', filters.from);
|
|
if (filters.to) queryParams.append('destinationCity', filters.to);
|
|
if (filters.date) queryParams.append('date', filters.date);
|
|
if (filters.seats) queryParams.append('minSeats', filters.seats.toString());
|
|
if (filters.passingThrough)
|
|
queryParams.append('passingThrough', filters.passingThrough);
|
|
|
|
const response = (await Api.SendReqWithData(
|
|
`/api/viaggi/rides?${queryParams.toString()}`,
|
|
'GET'
|
|
)) as PaginatedResponse<Ride>;
|
|
|
|
if (response.success) {
|
|
// ✅ Ensure response.data is always an array
|
|
const newRides = Array.isArray(response.data) ? response.data : [];
|
|
|
|
if (options.reset) {
|
|
rides.value = newRides;
|
|
} else {
|
|
rides.value = [...rides.value, ...newRides];
|
|
}
|
|
|
|
if (response?.data.pagination) {
|
|
Object.assign(pagination, response?.data.pagination);
|
|
}
|
|
} else {
|
|
throw new Error('Errore nel recupero dei viaggi');
|
|
}
|
|
|
|
return response;
|
|
} catch (err: any) {
|
|
error.value = err.data?.message || err.message || 'Errore nel recupero dei viaggi';
|
|
rides.value = []; // ✅ Reset to empty array on error
|
|
throw err;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Ricerca viaggi avanzata
|
|
*/
|
|
const searchRides = async (searchFilters: RideSearchFilters) => {
|
|
try {
|
|
loading.value = true;
|
|
error.value = null;
|
|
|
|
const queryParams = new URLSearchParams();
|
|
if (searchFilters.from) queryParams.append('from', searchFilters.from);
|
|
if (searchFilters.to) queryParams.append('to', searchFilters.to);
|
|
if (searchFilters.date) queryParams.append('date', searchFilters.date);
|
|
if (searchFilters.seats)
|
|
queryParams.append('seats', searchFilters.seats.toString());
|
|
if (searchFilters.type) queryParams.append('type', searchFilters.type);
|
|
queryParams.append('page', pagination.page.toString());
|
|
queryParams.append('limit', pagination.limit.toString());
|
|
|
|
const response = (await Api.SendReqWithData(
|
|
`/api/viaggi/rides/search?${queryParams.toString()}`,
|
|
'GET'
|
|
)) as PaginatedResponse<Ride>;
|
|
|
|
if (response.success) {
|
|
// ✅ Ensure response.data is always an array
|
|
rides.value = Array.isArray(response.data) ? response.data : [];
|
|
|
|
if (response?.data.pagination) {
|
|
Object.assign(pagination, response?.data.pagination);
|
|
}
|
|
}
|
|
|
|
return response;
|
|
} catch (err: any) {
|
|
error.value = err.data?.message || err.message || 'Errore nella ricerca';
|
|
rides.value = []; // ✅ Reset to empty array on error
|
|
throw err;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Ottieni singolo viaggio
|
|
*/
|
|
const fetchRide = async (rideId: string) => {
|
|
try {
|
|
loading.value = true;
|
|
error.value = null;
|
|
|
|
const response = (await Api.SendReqWithData(
|
|
`/api/viaggi/rides/${rideId}`,
|
|
'GET'
|
|
)) as ApiResponse<Ride>;
|
|
|
|
if (response.success && response.data) {
|
|
currentRide.value = response.data;
|
|
} else {
|
|
throw new Error('Viaggio non trovato');
|
|
}
|
|
|
|
return response;
|
|
} catch (err: any) {
|
|
error.value = err.data?.message || err.message || 'Errore nel recupero del viaggio';
|
|
throw err;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Crea nuovo viaggio
|
|
*/
|
|
const createRide = async (rideData: Partial<RideFormData>) => {
|
|
try {
|
|
loading.value = true;
|
|
error.value = null;
|
|
|
|
const response = (await Api.SendReqWithData(
|
|
'/api/viaggi/rides',
|
|
'POST',
|
|
rideData
|
|
)) as ApiResponse<Ride>;
|
|
|
|
if (response.success && response.data) {
|
|
// Aggiungi in testa alla lista
|
|
rides.value.unshift(response.data);
|
|
currentRide.value = response.data;
|
|
}
|
|
|
|
return response;
|
|
} catch (err: any) {
|
|
error.value =
|
|
err.data?.message || err.message || 'Errore nella creazione del viaggio';
|
|
throw err;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Aggiorna viaggio
|
|
*/
|
|
const updateRide = async (rideId: string, updateData: Partial<RideFormData>) => {
|
|
try {
|
|
loading.value = true;
|
|
error.value = null;
|
|
|
|
const response = (await Api.SendReqWithData(
|
|
`/api/viaggi/rides/${rideId}`,
|
|
'PUT',
|
|
updateData
|
|
)) as ApiResponse<Ride>;
|
|
|
|
if (response.success && response.data) {
|
|
// Aggiorna nella lista
|
|
const index = rides.value.findIndex((r) => r._id === rideId);
|
|
if (index !== -1) {
|
|
rides.value[index] = response.data;
|
|
}
|
|
if (currentRide.value?._id === rideId) {
|
|
currentRide.value = response.data;
|
|
}
|
|
}
|
|
|
|
return response;
|
|
} catch (err: any) {
|
|
error.value =
|
|
err.data?.message || err.message || "Errore nell'aggiornamento del viaggio";
|
|
throw err;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Cancella viaggio
|
|
*/
|
|
const deleteRide = async (rideId: string, reason?: string) => {
|
|
try {
|
|
loading.value = true;
|
|
error.value = null;
|
|
|
|
const response = (await Api.SendReqWithData(
|
|
`/api/viaggi/rides/${rideId}`,
|
|
'DELETE',
|
|
{
|
|
reason,
|
|
}
|
|
)) as ApiResponse<void>;
|
|
|
|
if (response.success) {
|
|
// Rimuovi dalla lista
|
|
rides.value = rides.value.filter((r) => r._id !== rideId);
|
|
if (currentRide.value?._id === rideId) {
|
|
currentRide.value = null;
|
|
}
|
|
}
|
|
|
|
return response;
|
|
} catch (err: any) {
|
|
error.value =
|
|
err.data?.message || err.message || 'Errore nella cancellazione del viaggio';
|
|
throw err;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Completa viaggio
|
|
*/
|
|
const completeRideApi = async (rideId: string) => {
|
|
try {
|
|
loading.value = true;
|
|
error.value = null;
|
|
|
|
const response = (await Api.SendReqWithData(
|
|
`/api/viaggi/rides/${rideId}/complete`,
|
|
'POST'
|
|
)) as ApiResponse<Ride>;
|
|
|
|
if (response.success && response.data) {
|
|
const index = rides.value.findIndex((r) => r._id === rideId);
|
|
if (index !== -1) {
|
|
rides.value[index] = response.data;
|
|
}
|
|
if (currentRide.value?._id === rideId) {
|
|
currentRide.value = response.data;
|
|
}
|
|
}
|
|
|
|
return response;
|
|
} catch (err: any) {
|
|
error.value =
|
|
err.data?.message || err.message || 'Errore nel completamento del viaggio';
|
|
throw err;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Ottieni i miei viaggi
|
|
*/
|
|
const fetchMyRides = async (options?: {
|
|
type?: RideType;
|
|
role?: 'driver' | 'passenger';
|
|
status?: RideStatus;
|
|
}) => {
|
|
try {
|
|
loading.value = true;
|
|
error.value = null;
|
|
|
|
const queryParams = new URLSearchParams();
|
|
if (options?.type) queryParams.append('type', options.type);
|
|
if (options?.role) queryParams.append('role', options.role);
|
|
if (options?.status) queryParams.append('status', options.status);
|
|
queryParams.append('page', pagination.page.toString());
|
|
queryParams.append('limit', pagination.limit.toString());
|
|
|
|
const response = (await Api.SendReqWithData(
|
|
`/api/viaggi/rides/my?${queryParams.toString()}`,
|
|
'GET'
|
|
)) as MyRidesResponse;
|
|
|
|
if (response.success && response.data) {
|
|
myRides.all = response.data.all;
|
|
myRides.upcoming = response.data.upcoming;
|
|
myRides.past = response.data.past;
|
|
Object.assign(pagination, response?.data.pagination);
|
|
}
|
|
|
|
return response;
|
|
} catch (err: any) {
|
|
error.value =
|
|
err.data?.message || err.message || 'Errore nel recupero dei tuoi viaggi';
|
|
throw err;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Ottieni statistiche per widget
|
|
*/
|
|
const fetchStats = async () => {
|
|
try {
|
|
const response = (await Api.SendReqWithData(
|
|
'/api/viaggi/rides/stats',
|
|
'GET'
|
|
)) as ApiResponse<RidesStatsResponse>;
|
|
|
|
if (response.success && response.data) {
|
|
stats.value = response.data;
|
|
}
|
|
|
|
return response;
|
|
} catch (err: any) {
|
|
console.error('Errore recupero statistiche:', err);
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
// ------------------------------------------------------------
|
|
// UTILITIES
|
|
// ------------------------------------------------------------
|
|
|
|
/**
|
|
* Carica pagina successiva
|
|
*/
|
|
const loadMore = async () => {
|
|
if (hasMorePages.value && !loading.value) {
|
|
pagination.page++;
|
|
await fetchRides({ reset: false });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Reset filtri
|
|
*/
|
|
/**
|
|
* Reset filtri
|
|
*/
|
|
const resetFilters = () => {
|
|
filters.type = undefined;
|
|
filters.from = '';
|
|
filters.to = '';
|
|
filters.date = '';
|
|
filters.seats = 1;
|
|
filters.passingThrough = '';
|
|
|
|
// Rimuovi da localStorage
|
|
try {
|
|
localStorage.removeItem(STOR_TRASP_CITIE);
|
|
} catch (error) {
|
|
console.error('Errore rimozione filtri da localStorage:', error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Pulisci stato
|
|
*/
|
|
const clearState = () => {
|
|
rides.value = [];
|
|
currentRide.value = null;
|
|
myRides.all = [];
|
|
myRides.upcoming = [];
|
|
myRides.past = [];
|
|
error.value = null;
|
|
pagination.page = 1;
|
|
pagination.total = 0;
|
|
pagination.pages = 0;
|
|
};
|
|
|
|
/**
|
|
* Formatta data viaggio
|
|
*/
|
|
const formatRideDate = (date: Date | string) => {
|
|
const d = new Date(date);
|
|
const today = new Date();
|
|
const tomorrow = new Date(today);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
|
|
if (d.toDateString() === today.toDateString()) {
|
|
return `Oggi, ${d.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' })}`;
|
|
} else if (d.toDateString() === tomorrow.toDateString()) {
|
|
return `Domani, ${d.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' })}`;
|
|
} else {
|
|
return d.toLocaleDateString('it-IT', {
|
|
weekday: 'short',
|
|
day: 'numeric',
|
|
month: 'short',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Formatta durata viaggio
|
|
*/
|
|
const formatDuration = (minutes: number) => {
|
|
const hours = Math.floor(minutes / 60);
|
|
const mins = minutes % 60;
|
|
if (hours === 0) return `${mins} min`;
|
|
if (mins === 0) return `${hours} h`;
|
|
return `${hours} h ${mins} min`;
|
|
};
|
|
|
|
/**
|
|
* Formatta distanza
|
|
*/
|
|
const formatDistance = (km: number) => {
|
|
if (km < 1) return `${Math.round(km * 1000)} m`;
|
|
return `${km.toFixed(1)} km`;
|
|
};
|
|
|
|
/**
|
|
* Ottieni colore status
|
|
*/
|
|
const getStatusColor = (status: RideStatus): string => {
|
|
const colors: Record<RideStatus, string> = {
|
|
draft: 'grey',
|
|
active: 'positive',
|
|
full: 'warning',
|
|
in_progress: 'info',
|
|
completed: 'positive',
|
|
cancelled: 'negative',
|
|
expired: 'grey',
|
|
};
|
|
return colors[status] || 'grey';
|
|
};
|
|
|
|
/**
|
|
* Ottieni label status
|
|
*/
|
|
const getStatusLabel = (status: RideStatus): string => {
|
|
const labels: Record<RideStatus, string> = {
|
|
draft: 'Bozza',
|
|
active: 'Attivo',
|
|
full: 'Completo',
|
|
in_progress: 'In corso',
|
|
completed: 'Completato',
|
|
cancelled: 'Cancellato',
|
|
expired: 'Scaduto',
|
|
};
|
|
return labels[status] || status;
|
|
};
|
|
|
|
/**
|
|
* Verifica se il viaggio è nel passato
|
|
*/
|
|
const isPastRide = (ride: Ride) => {
|
|
return new Date(ride.departureDate) < new Date();
|
|
};
|
|
|
|
/**
|
|
* Verifica se l'utente può prenotare
|
|
*/
|
|
const canBook = (ride: Ride, userId: string) => {
|
|
if (ride.type !== 'offer') return false;
|
|
if (ride.status !== 'active') return false;
|
|
if (ride.passengers && ride.passengers.available <= 0) return false;
|
|
if (typeof ride.userId === 'string' && ride.userId === userId) return false;
|
|
if (typeof ride.userId === 'object' && ride.userId._id === userId) return false;
|
|
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 {
|
|
// State
|
|
rides,
|
|
currentRide,
|
|
myRides,
|
|
stats,
|
|
loading,
|
|
error,
|
|
pagination,
|
|
filters,
|
|
|
|
// Computed
|
|
hasRides,
|
|
hasMorePages,
|
|
offersCount,
|
|
requestsCount,
|
|
activeRides,
|
|
|
|
// API Methods
|
|
fetchRides,
|
|
searchRides,
|
|
fetchRide,
|
|
createRide,
|
|
updateRide,
|
|
deleteRide,
|
|
completeRideApi,
|
|
fetchMyRides,
|
|
fetchStats,
|
|
|
|
// Utilities
|
|
loadMore,
|
|
resetFilters,
|
|
clearState,
|
|
formatRideDate,
|
|
formatDuration,
|
|
formatDistance,
|
|
getStatusColor,
|
|
getStatusLabel,
|
|
isPastRide,
|
|
canBook,
|
|
fetchCancelledRides,
|
|
};
|
|
}
|