Files
myprojplanet_vite/src/modules/trasporti/pages/Requestspage.vue
2025-12-22 23:39:42 +01:00

650 lines
24 KiB
Vue

<!-- RequestsPage.vue -->
<template>
<q-page class="requests-page">
<!-- Header -->
<div class="requests-page__header">
<q-btn flat round icon="arrow_back" color="white" @click="goBack" />
<div>
<h1>Richieste</h1>
<p>Gestisci le richieste di passaggio</p>
</div>
</div>
<!-- Stats -->
<div class="requests-page__stats" v-if="!loading">
<div class="requests-page__stat">
<q-icon name="inbox" color="warning" size="24px" />
<div class="requests-page__stat-info">
<span class="requests-page__stat-value">{{ stats.pending }}</span>
<span class="requests-page__stat-label">In attesa</span>
</div>
</div>
<div class="requests-page__stat">
<q-icon name="check_circle" color="positive" size="24px" />
<div class="requests-page__stat-info">
<span class="requests-page__stat-value">{{ stats.accepted }}</span>
<span class="requests-page__stat-label">Accettate</span>
</div>
</div>
<div class="requests-page__stat">
<q-icon name="cancel" color="negative" size="24px" />
<div class="requests-page__stat-info">
<span class="requests-page__stat-value">{{ stats.rejected }}</span>
<span class="requests-page__stat-label">Rifiutate</span>
</div>
</div>
</div>
<!-- Tabs -->
<q-tabs v-model="activeTab" class="requests-page__tabs" active-color="primary" indicator-color="primary" align="justify">
<q-tab name="received" icon="move_to_inbox">
<template #default>
<div class="requests-page__tab-content">
<span>Ricevute</span>
<q-badge v-if="pendingReceivedCount > 0" color="negative" :label="pendingReceivedCount" />
</div>
</template>
</q-tab>
<q-tab name="sent" icon="send">
<template #default>
<div class="requests-page__tab-content">
<span>Inviate</span>
<q-badge v-if="pendingSentCount > 0" color="info" :label="pendingSentCount" />
</div>
</template>
</q-tab>
</q-tabs>
<!-- Filter Chips -->
<div class="requests-page__filters">
<q-chip
v-for="filter in statusFilters"
:key="filter.value"
:color="activeFilter === filter.value ? 'primary' : 'grey-3'"
:text-color="activeFilter === filter.value ? 'white' : 'grey-8'"
clickable
@click="activeFilter = filter.value"
>
<q-icon :name="filter.icon" size="16px" class="q-mr-xs" />
{{ filter.label }}
</q-chip>
</div>
<!-- Content -->
<div class="requests-page__content">
<!-- Loading -->
<div v-if="loading" class="requests-page__loading">
<q-spinner-dots size="50px" color="primary" />
<p>Caricamento richieste...</p>
</div>
<!-- Empty State -->
<div v-else-if="filteredRequests.length === 0" class="requests-page__empty">
<q-icon :name="activeTab === 'received' ? 'move_to_inbox' : 'send'" size="80px" color="grey-4" />
<h3>{{ emptyTitle }}</h3>
<p>{{ emptyMessage }}</p>
<q-btn v-if="activeTab === 'sent'" color="primary" icon="search" label="Cerca un passaggio" rounded unelevated to="/trasporti/cerca" />
</div>
<!-- Requests List -->
<div v-else class="requests-page__list">
<TransitionGroup name="list">
<div v-for="request in filteredRequests" :key="request._id" class="requests-page__card" :class="`requests-page__card--${request.status}`">
<!-- Status Badge -->
<div class="requests-page__status-badge" :class="`requests-page__status-badge--${request.status}`">
<q-icon :name="getStatusIcon(request.status)" size="14px" />
{{ getStatusLabel(request.status) }}
</div>
<!-- Card Header -->
<div class="requests-page__card-header">
<div class="requests-page__user" @click="viewProfile(request)">
<q-avatar size="48px">
<img v-if="getOtherUser(request)?.profile?.img" :src="getOtherUser(request).profile.img" />
<div v-else class="requests-page__avatar-placeholder">
{{ getInitials(getOtherUser(request)) }}
</div>
</q-avatar>
<div class="requests-page__user-info">
<span class="requests-page__user-name">
{{ getOtherUser(request)?.name }} {{ getOtherUser(request)?.surname }}
</span>
<div class="requests-page__user-rating" v-if="getOtherUser(request)?.rating">
<q-icon name="star" color="amber" size="14px" />
<span>{{ getOtherUser(request).rating.toFixed(1) }}</span>
</div>
</div>
</div>
<div class="requests-page__date">{{ formatTimeAgo(request.createdAt) }}</div>
</div>
<!-- Ride Info -->
<div class="requests-page__ride" @click="viewRide(request.rideId)">
<div class="requests-page__ride-type">
<q-icon
:name="request.rideInfo?.type === 'offer' ? 'directions_car' : 'hail'"
:color="request.rideInfo?.type === 'offer' ? 'positive' : 'info'"
size="20px"
/>
</div>
<div class="requests-page__ride-details">
<div class="requests-page__ride-route">
{{ request.rideInfo?.departure }} {{ request.rideInfo?.destination }}
</div>
<div class="requests-page__ride-datetime">
<q-icon name="event" size="14px" />
{{ formatDate(request.rideInfo?.departureDate) }}
<q-icon name="schedule" size="14px" class="q-ml-sm" />
{{ request.rideInfo?.departureTime }}
</div>
</div>
<q-icon name="chevron_right" color="grey-5" />
</div>
<!-- Request Details -->
<div class="requests-page__details" v-if="request.seats || request.pickupPoint">
<div class="requests-page__detail" v-if="request.seats">
<q-icon name="event_seat" size="16px" color="grey-6" />
<span>{{ request.seats }} {{ request.seats === 1 ? 'posto' : 'posti' }} richiesti</span>
</div>
<div class="requests-page__detail" v-if="request.pickupPoint">
<q-icon name="location_on" size="16px" color="grey-6" />
<span>{{ request.pickupPoint }}</span>
</div>
</div>
<!-- Message -->
<div class="requests-page__message" v-if="request.message">
<q-icon name="chat_bubble_outline" size="16px" color="grey-5" />
<p>{{ request.message }}</p>
</div>
<!-- Response -->
<div class="requests-page__response" v-if="request.response && request.status !== 'pending'">
<q-icon name="reply" size="16px" color="grey-5" />
<p>{{ request.response }}</p>
</div>
<!-- Actions for received pending -->
<div class="requests-page__actions" v-if="activeTab === 'received' && request.status === 'pending'">
<q-btn flat color="negative" icon="close" label="Rifiuta" size="sm" @click="rejectRequest(request)" />
<q-btn unelevated color="positive" icon="check" label="Accetta" size="sm" @click="acceptRequest(request)" />
</div>
<!-- Actions for sent pending -->
<div class="requests-page__actions" v-if="activeTab === 'sent' && request.status === 'pending'">
<q-btn flat color="grey" icon="delete_outline" label="Annulla" size="sm" @click="cancelRequest(request)" />
<q-btn flat color="primary" icon="chat" label="Scrivi" size="sm" @click="openChat(request)" />
</div>
<!-- Actions for accepted -->
<div class="requests-page__actions" v-if="request.status === 'accepted'">
<q-btn flat color="primary" icon="chat" label="Scrivi" size="sm" @click="openChat(request)" />
<q-btn flat color="info" icon="info" label="Dettagli" size="sm" @click="viewRide(request.rideId)" />
</div>
</div>
</TransitionGroup>
<div v-if="hasMore" class="requests-page__load-more">
<q-btn flat color="primary" :loading="loadingMore" @click="loadMore">Carica altre</q-btn>
</div>
</div>
</div>
<!-- Response Dialog -->
<q-dialog v-model="showResponseDialog" persistent>
<q-card class="requests-page__dialog">
<q-card-section class="text-center">
<q-icon :name="responseAction === 'accept' ? 'check_circle' : 'cancel'" :color="responseAction === 'accept' ? 'positive' : 'negative'" size="60px" />
<h3>{{ responseAction === 'accept' ? 'Accetta richiesta' : 'Rifiuta richiesta' }}</h3>
</q-card-section>
<q-card-section>
<q-input v-model="responseMessage" type="textarea" outlined autogrow :label="responseAction === 'accept' ? 'Messaggio (opzionale)' : 'Motivo (opzionale)'" :maxlength="300" counter />
</q-card-section>
<q-card-actions align="center" class="q-pb-md">
<q-btn flat label="Annulla" color="grey" v-close-popup />
<q-btn unelevated :label="responseAction === 'accept' ? 'Conferma' : 'Rifiuta'" :color="responseAction === 'accept' ? 'positive' : 'negative'" :loading="responding" @click="submitResponse" />
</q-card-actions>
</q-card>
</q-dialog>
</q-page>
</template>
<script lang="ts">
import { defineComponent, ref, computed, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useQuasar, date as qdate } from 'quasar';
import { Api } from '@api';
interface RequestStats {
pending: number;
accepted: number;
rejected: number;
}
interface RideRequest {
_id: string;
rideId: string;
fromUserId: string;
toUserId: string;
fromUser?: any;
toUser?: any;
seats: number;
pickupPoint?: string;
message?: string;
response?: string;
status: 'pending' | 'accepted' | 'rejected' | 'cancelled';
createdAt: string;
rideInfo?: {
departure: string;
destination: string;
departureDate: string;
departureTime: string;
type: 'offer' | 'request';
};
}
export default defineComponent({
name: 'RequestsPage',
setup() {
const router = useRouter();
const $q = useQuasar();
const loading = ref(true);
const loadingMore = ref(false);
const responding = ref(false);
const activeTab = ref<'received' | 'sent'>('received');
const activeFilter = ref('all');
const stats = ref<RequestStats>({ pending: 0, accepted: 0, rejected: 0 });
const receivedRequests = ref<RideRequest[]>([]);
const sentRequests = ref<RideRequest[]>([]);
const currentPage = ref(1);
const hasMore = ref(false);
const showResponseDialog = ref(false);
const selectedRequest = ref<RideRequest | null>(null);
const responseAction = ref<'accept' | 'reject'>('accept');
const responseMessage = ref('');
const statusFilters = [
{ value: 'all', label: 'Tutte', icon: 'list' },
{ value: 'pending', label: 'In attesa', icon: 'hourglass_empty' },
{ value: 'accepted', label: 'Accettate', icon: 'check_circle' },
{ value: 'rejected', label: 'Rifiutate', icon: 'cancel' }
];
const pendingReceivedCount = computed(() => receivedRequests.value.filter(r => r.status === 'pending').length);
const pendingSentCount = computed(() => sentRequests.value.filter(r => r.status === 'pending').length);
const filteredRequests = computed(() => {
const requests = activeTab.value === 'received' ? receivedRequests.value : sentRequests.value;
if (activeFilter.value === 'all') return requests;
return requests.filter(r => r.status === activeFilter.value);
});
const emptyTitle = computed(() => {
if (activeFilter.value !== 'all') {
return `Nessuna richiesta ${statusFilters.find(f => f.value === activeFilter.value)?.label.toLowerCase()}`;
}
return activeTab.value === 'received' ? 'Nessuna richiesta ricevuta' : 'Nessuna richiesta inviata';
});
const emptyMessage = computed(() => {
if (activeFilter.value !== 'all') return 'Prova a cambiare i filtri';
return activeTab.value === 'received'
? 'Quando qualcuno richiederà un passaggio sui tuoi viaggi, lo vedrai qui'
: 'Cerca un passaggio e invia la tua prima richiesta';
});
const goBack = () => router.back();
const getInitials = (user: any) => {
if (!user) return '?';
return `${(user.name || '').charAt(0)}${(user.surname || '').charAt(0)}`.toUpperCase();
};
const getOtherUser = (request: RideRequest) => {
return activeTab.value === 'received' ? request.fromUser : request.toUser;
};
const getStatusIcon = (status: string): string => {
const icons: Record<string, string> = { pending: 'hourglass_empty', accepted: 'check_circle', rejected: 'cancel', cancelled: 'block' };
return icons[status] || 'help';
};
const getStatusLabel = (status: string): string => {
const labels: Record<string, string> = { pending: 'In attesa', accepted: 'Accettata', rejected: 'Rifiutata', cancelled: 'Annullata' };
return labels[status] || status;
};
const formatDate = (dateStr?: string) => dateStr ? qdate.formatDate(dateStr, 'DD MMM YYYY') : '';
const formatTimeAgo = (dateStr: string) => {
const diffMs = Date.now() - new Date(dateStr).getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Adesso';
if (diffMins < 60) return `${diffMins} min fa`;
if (diffHours < 24) return `${diffHours} ore fa`;
if (diffDays === 1) return 'Ieri';
if (diffDays < 7) return `${diffDays} giorni fa`;
return formatDate(dateStr);
};
const viewProfile = (request: RideRequest) => {
const user = getOtherUser(request);
if (user?._id) router.push(`/trasporti/profilo/${user._id}`);
};
const viewRide = (rideId?: string) => {
if (rideId) router.push(`/trasporti/ride/${rideId}`);
};
const openChat = (request: RideRequest) => {
const user = getOtherUser(request);
if (user?._id) router.push(`/trasporti/chat?userId=${user._id}&rideId=${request.rideId}`);
};
const acceptRequest = (request: RideRequest) => {
selectedRequest.value = request;
responseAction.value = 'accept';
responseMessage.value = '';
showResponseDialog.value = true;
};
const rejectRequest = (request: RideRequest) => {
selectedRequest.value = request;
responseAction.value = 'reject';
responseMessage.value = '';
showResponseDialog.value = true;
};
const submitResponse = async () => {
if (!selectedRequest.value) return;
responding.value = true;
try {
const endpoint = responseAction.value === 'accept'
? `/api/trasporti/richieste/${selectedRequest.value._id}/accept`
: `/api/trasporti/richieste/${selectedRequest.value._id}/reject`;
const response = await Api.SendReqWithData(endpoint, 'PUT', { message: responseMessage.value });
if (response.success) {
const index = receivedRequests.value.findIndex(r => r._id === selectedRequest.value?._id);
if (index !== -1) {
receivedRequests.value[index].status = responseAction.value === 'accept' ? 'accepted' : 'rejected';
receivedRequests.value[index].response = responseMessage.value;
}
stats.value.pending = Math.max(0, stats.value.pending - 1);
if (responseAction.value === 'accept') stats.value.accepted++;
else stats.value.rejected++;
$q.notify({ type: 'positive', message: responseAction.value === 'accept' ? 'Richiesta accettata!' : 'Richiesta rifiutata' });
showResponseDialog.value = false;
}
} catch (error: any) {
$q.notify({ type: 'negative', message: error.data?.message || error.message || 'Errore' });
} finally {
responding.value = false;
}
};
const cancelRequest = async (request: RideRequest) => {
$q.dialog({ title: 'Annulla richiesta', message: 'Sei sicuro?', cancel: true }).onOk(async () => {
try {
const response = await Api.SendReqWithData(`/api/trasporti/richieste/${request._id}/cancel`, 'PUT');
if (response.success) {
const index = sentRequests.value.findIndex(r => r._id === request._id);
if (index !== -1) sentRequests.value[index].status = 'cancelled';
$q.notify({ type: 'positive', message: 'Richiesta annullata' });
}
} catch (error: any) {
$q.notify({ type: 'negative', message: error.data?.message || error.message || 'Errore' });
}
});
};
const loadRequests = async () => {
loading.value = true;
try {
const [statsRes, receivedRes, sentRes] = await Promise.all([
Api.SendReqWithData('/api/trasporti/richieste/stats', 'GET'),
Api.SendReqWithData('/api/trasporti/richieste/received', 'GET'),
Api.SendReqWithData('/api/trasporti/richieste/sent', 'GET')
]);
if (statsRes.success) stats.value = statsRes.data;
if (receivedRes.success) {
receivedRequests.value = receivedRes.data.requests || [];
hasMore.value = receivedRes.data.hasMore || false;
}
if (sentRes.success) sentRequests.value = sentRes.data.requests || [];
} catch (error: any) {
$q.notify({ type: 'negative', message: error.data?.message || error.message || 'Errore nel caricamento' });
} finally {
loading.value = false;
}
};
const loadMore = async () => {
loadingMore.value = true;
currentPage.value++;
try {
const endpoint = activeTab.value === 'received' ? '/api/trasporti/richieste/received' : '/api/trasporti/richieste/sent';
const response = await Api.SendReqWithData(`${endpoint}?page=${currentPage.value}`, 'GET');
if (response.success) {
const newRequests = response.data.requests || [];
if (activeTab.value === 'received') receivedRequests.value.push(...newRequests);
else sentRequests.value.push(...newRequests);
hasMore.value = response.data.hasMore || false;
}
} catch (error: any) {
$q.notify({ type: 'negative', message: 'Errore' });
} finally {
loadingMore.value = false;
}
};
watch(activeTab, () => { currentPage.value = 1; activeFilter.value = 'all'; });
onMounted(() => loadRequests());
return {
loading, loadingMore, responding, activeTab, activeFilter, stats, statusFilters,
filteredRequests, hasMore, pendingReceivedCount, pendingSentCount, emptyTitle, emptyMessage,
showResponseDialog, selectedRequest, responseAction, responseMessage,
goBack, getInitials, getOtherUser, getStatusIcon, getStatusLabel, formatDate, formatTimeAgo,
viewProfile, viewRide, openChat, acceptRequest, rejectRequest, submitResponse, cancelRequest, loadMore
};
}
});
</script>
<style lang="scss" scoped>
.requests-page {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
min-height: 100vh;
padding-bottom: 80px;
&__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; }
}
&__stats {
display: flex;
justify-content: space-around;
padding: 16px 20px;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
&__stat {
display: flex;
align-items: center;
gap: 10px;
}
&__stat-info { display: flex; flex-direction: column; }
&__stat-value { font-size: 20px; font-weight: 700; color: #333; }
&__stat-label { font-size: 11px; color: #888; }
&__tabs { background: white; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); }
&__tab-content { display: flex; align-items: center; gap: 6px; }
&__filters {
display: flex;
gap: 8px;
padding: 12px 16px;
overflow-x: auto;
&::-webkit-scrollbar { display: none; }
}
&__content { padding: 16px; }
&__loading, &__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
h3 { margin: 20px 0 8px; color: #333; font-size: 18px; }
p { color: #888; margin: 0 0 20px; max-width: 280px; }
}
&__list { display: flex; flex-direction: column; gap: 16px; }
&__card {
background: white;
border-radius: 16px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
position: relative;
&--pending { border-left: 4px solid #ff9800; }
&--accepted { border-left: 4px solid #4caf50; }
&--rejected { border-left: 4px solid #f44336; }
&--cancelled { border-left: 4px solid #9e9e9e; opacity: 0.7; }
}
&__status-badge {
position: absolute;
top: 12px;
right: 12px;
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
&--pending { background: #fff3e0; color: #e65100; }
&--accepted { background: #e8f5e9; color: #2e7d32; }
&--rejected { background: #ffebee; color: #c62828; }
&--cancelled { background: #f5f5f5; color: #616161; }
}
&__card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 12px;
padding-right: 80px;
}
&__user { display: flex; align-items: center; gap: 12px; cursor: pointer; }
&__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%;
}
&__user-info { display: flex; flex-direction: column; gap: 2px; }
&__user-name { font-weight: 600; color: #333; }
&__user-rating { display: flex; align-items: center; gap: 4px; font-size: 12px; color: #666; }
&__date { font-size: 11px; color: #999; }
&__ride {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 10px;
margin-bottom: 12px;
cursor: pointer;
&:hover { background: #e9ecef; }
}
&__ride-type {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: white;
border-radius: 10px;
}
&__ride-details { flex: 1; }
&__ride-route { font-weight: 600; color: #333; margin-bottom: 4px; }
&__ride-datetime { display: flex; align-items: center; gap: 4px; font-size: 12px; color: #666; }
&__details { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 12px; }
&__detail { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #555; }
&__message, &__response {
display: flex;
gap: 8px;
padding: 12px;
background: #fafafa;
border-radius: 8px;
margin-bottom: 12px;
p { margin: 0; font-size: 13px; color: #555; line-height: 1.5; }
}
&__response { background: #e8f5e9; p { color: #2e7d32; } }
&__actions {
display: flex;
gap: 12px;
justify-content: flex-end;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
&__load-more { display: flex; justify-content: center; padding: 20px; }
&__dialog {
border-radius: 16px;
min-width: 320px;
max-width: 400px;
h3 { margin: 16px 0 8px; font-size: 18px; }
}
}
.list-enter-active, .list-leave-active { transition: all 0.3s ease; }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateY(20px); }
.body--dark {
.requests-page {
background: linear-gradient(135deg, #1a1a2e 0%, #16162a 100%);
&__stats, &__card { background: #1e1e30; }
&__stat-value, &__user-name, &__ride-route { color: #fff; }
&__ride { background: rgba(255, 255, 255, 0.05); &:hover { background: rgba(255, 255, 255, 0.1); } }
&__ride-type { background: rgba(255, 255, 255, 0.1); }
&__message { background: rgba(255, 255, 255, 0.03); }
&__actions { border-color: rgba(255, 255, 255, 0.1); }
}
}
</style>