650 lines
24 KiB
Vue
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.SendReq(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.SendReq(`/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.SendReq('/api/trasporti/richieste/stats', 'GET'),
|
|
Api.SendReq('/api/trasporti/richieste/received', 'GET'),
|
|
Api.SendReq('/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.SendReq(`${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>
|