Files
myprojplanet_vite/src/modules/viaggi/pages/Myfeedbackpage.vue
2025-12-24 19:46:49 +01:00

796 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- MyFeedbackPage.vue -->
<template>
<q-page class="my-feedback-page">
<!-- Header -->
<div class="my-feedback-page__header">
<q-btn
flat
round
icon="arrow_back"
color="white"
@click="goBack"
/>
<div>
<h1>I Miei Feedback</h1>
<p>Le valutazioni che hai ricevuto</p>
</div>
</div>
<!-- Stats Summary -->
<div class="my-feedback-page__stats" v-if="!loading && stats">
<div class="my-feedback-page__rating-card">
<div class="my-feedback-page__rating-value">
{{ stats.averageRating?.toFixed(1) || '' }}
</div>
<div class="my-feedback-page__rating-stars">
<q-icon
v-for="n in 5"
:key="n"
:name="n <= Math.round(stats.averageRating || 0) ? 'star' : 'star_border'"
:color="n <= Math.round(stats.averageRating || 0) ? 'amber' : 'grey-4'"
size="20px"
/>
</div>
<div class="my-feedback-page__rating-count">
{{ stats.totalCount || 0 }} valutazioni
</div>
</div>
<div class="my-feedback-page__stats-grid">
<div class="my-feedback-page__stat-item">
<q-icon name="directions_car" color="positive" size="24px" />
<span class="my-feedback-page__stat-value">{{ stats.asDriver || 0 }}</span>
<span class="my-feedback-page__stat-label">Come conducente</span>
</div>
<div class="my-feedback-page__stat-item">
<q-icon name="hail" color="info" size="24px" />
<span class="my-feedback-page__stat-value">{{ stats.asPassenger || 0 }}</span>
<span class="my-feedback-page__stat-label">Come passeggero</span>
</div>
</div>
</div>
<!-- Tabs -->
<q-tabs
v-model="activeTab"
class="my-feedback-page__tabs"
active-color="primary"
indicator-color="primary"
align="justify"
>
<q-tab name="received" label="Ricevuti" icon="inbox" />
<q-tab name="given" label="Dati" icon="send" />
</q-tabs>
<!-- Content -->
<div class="my-feedback-page__content">
<!-- Loading -->
<div v-if="loading" class="my-feedback-page__loading">
<q-spinner-dots size="50px" color="primary" />
<p>Caricamento feedback...</p>
</div>
<!-- Empty State -->
<div v-else-if="filteredFeedbacks.length === 0" class="my-feedback-page__empty">
<q-icon
:name="activeTab === 'received' ? 'star_border' : 'rate_review'"
size="80px"
color="grey-4"
/>
<h3>{{ activeTab === 'received' ? 'Nessun feedback ricevuto' : 'Nessun feedback dato' }}</h3>
<p>
{{ activeTab === 'received'
? 'Completa i tuoi primi viaggi per ricevere valutazioni'
: 'Non hai ancora lasciato feedback ad altri utenti'
}}
</p>
<q-btn
color="primary"
icon="explore"
label="Esplora viaggi"
rounded
unelevated
to="/viaggi"
/>
</div>
<!-- Feedback List -->
<div v-else class="my-feedback-page__list">
<TransitionGroup name="list">
<div
v-for="feedback in filteredFeedbacks"
:key="feedback._id"
class="my-feedback-page__card"
>
<!-- Card Header -->
<div class="my-feedback-page__card-header">
<div class="my-feedback-page__user" @click="viewProfile(feedback)">
<q-avatar size="44px">
<img
v-if="getOtherUser(feedback)?.profile?.img"
:src="getOtherUser(feedback).profile.img"
/>
<div v-else class="my-feedback-page__avatar-placeholder">
{{ getInitials(getOtherUser(feedback)) }}
</div>
</q-avatar>
<div class="my-feedback-page__user-info">
<span class="my-feedback-page__user-name">
{{ getOtherUser(feedback)?.name }} {{ getOtherUser(feedback)?.surname }}
</span>
<span class="my-feedback-page__user-role">
<q-icon
:name="feedback.toUserRole === 'driver' ? 'directions_car' : 'hail'"
size="14px"
/>
{{ feedback.toUserRole === 'driver' ? 'Come conducente' : 'Come passeggero' }}
</span>
</div>
</div>
<div class="my-feedback-page__rating">
<div class="my-feedback-page__rating-number">{{ feedback.rating }}</div>
<div class="my-feedback-page__rating-mini-stars">
<q-icon
v-for="n in 5"
:key="n"
:name="n <= feedback.rating ? 'star' : 'star_border'"
:color="n <= feedback.rating ? 'amber' : 'grey-4'"
size="12px"
/>
</div>
</div>
</div>
<!-- Ride Info -->
<div
class="my-feedback-page__ride-info"
v-if="feedback.rideInfo"
@click="viewRide(feedback.rideId)"
>
<q-icon name="route" size="16px" color="grey-6" />
<span>{{ feedback.rideInfo.departure }} {{ feedback.rideInfo.destination }}</span>
<span class="my-feedback-page__ride-date">
{{ formatDate(feedback.rideInfo.departureDate) }}
</span>
</div>
<!-- Categories -->
<div class="my-feedback-page__categories" v-if="feedback.categories?.length">
<div
v-for="cat in feedback.categories"
:key="cat.key"
class="my-feedback-page__category"
>
<span class="my-feedback-page__category-label">{{ cat.label }}</span>
<div class="my-feedback-page__category-stars">
<q-icon
v-for="n in 5"
:key="n"
:name="n <= cat.value ? 'star' : 'star_border'"
:color="n <= cat.value ? 'primary' : 'grey-4'"
size="14px"
/>
</div>
</div>
</div>
<!-- Tags -->
<div class="my-feedback-page__tags" v-if="feedback.tags?.length">
<q-chip
v-for="tag in feedback.tags"
:key="tag"
:color="getTagColor(tag)"
text-color="white"
size="sm"
dense
>
{{ tag }}
</q-chip>
</div>
<!-- Comment -->
<div class="my-feedback-page__comment" v-if="feedback.comment">
<q-icon name="format_quote" size="16px" color="grey-5" />
<p>{{ feedback.comment }}</p>
</div>
<!-- Footer -->
<div class="my-feedback-page__card-footer">
<span class="my-feedback-page__date">
{{ formatTimeAgo(feedback.createdAt) }}
</span>
<q-chip
v-if="feedback.isPublic"
color="green-1"
text-color="green-8"
size="sm"
icon="visibility"
dense
>
Pubblico
</q-chip>
<q-chip
v-else
color="grey-3"
text-color="grey-7"
size="sm"
icon="visibility_off"
dense
>
Privato
</q-chip>
</div>
</div>
</TransitionGroup>
<!-- Load More -->
<div v-if="hasMore" class="my-feedback-page__load-more">
<q-btn
flat
color="primary"
:loading="loadingMore"
@click="loadMore"
>
Carica altri
</q-btn>
</div>
</div>
</div>
</q-page>
</template>
<script lang="ts">
import { defineComponent, ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useQuasar, date as qdate } from 'quasar';
import { Api } from '@api';
import { useAuth } from '../composables/useAuth';
interface FeedbackStats {
averageRating: number;
totalCount: number;
asDriver: number;
asPassenger: number;
}
interface FeedbackCategory {
key: string;
label: string;
value: number;
}
interface FeedbackItem {
_id: string;
rideId: string;
fromUserId: string;
toUserId: string;
fromUser?: any;
toUser?: any;
toUserRole: 'driver' | 'passenger';
rating: number;
categories?: FeedbackCategory[];
tags?: string[];
comment?: string;
isPublic: boolean;
createdAt: string;
rideInfo?: {
departure: string;
destination: string;
departureDate: string;
type: string;
};
}
export default defineComponent({
name: 'MyFeedbackPage',
setup() {
const router = useRouter();
const $q = useQuasar();
// State
const loading = ref(true);
const loadingMore = ref(false);
const activeTab = ref<'received' | 'given'>('received');
const stats = ref<FeedbackStats | null>(null);
const receivedFeedbacks = ref<FeedbackItem[]>([]);
const givenFeedbacks = ref<FeedbackItem[]>([]);
const currentPage = ref(1);
const hasMore = ref(false);
const { user: currentUser } = useAuth();
const currentUserId = computed(() => currentUser.value?._id);
// Computed
const filteredFeedbacks = computed(() => {
return activeTab.value === 'received'
? receivedFeedbacks.value
: givenFeedbacks.value;
});
// Methods
const goBack = () => {
router.back();
};
const getInitials = (user: any) => {
if (!user) return '?';
const name = user.name || '';
const surname = user.surname || '';
return `${name.charAt(0)}${surname.charAt(0)}`.toUpperCase();
};
const getOtherUser = (feedback: FeedbackItem) => {
return activeTab.value === 'received' ? feedback.fromUser : feedback.toUser;
};
const formatDate = (dateStr: string) => {
return qdate.formatDate(dateStr, 'DD MMM YYYY');
};
const formatTimeAgo = (dateStr: string) => {
const now = new Date();
const date = new Date(dateStr);
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) return 'Oggi';
if (diffDays === 1) return 'Ieri';
if (diffDays < 7) return `${diffDays} giorni fa`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)} settimane fa`;
return formatDate(dateStr);
};
const getTagColor = (tag: string): string => {
const positiveWords = ['puntuale', 'gentile', 'sicuro', 'pulito', 'preciso'];
const isPositive = positiveWords.some(w => tag.toLowerCase().includes(w));
return isPositive ? 'positive' : 'primary';
};
const viewProfile = (feedback: FeedbackItem) => {
const user = getOtherUser(feedback);
if (user?._id) {
router.push(`/viaggi/profilo/${user._id}`);
}
};
const viewRide = (rideId: string) => {
router.push(`/viaggi/ride/${rideId}`);
};
const loadFeedbacks = async () => {
loading.value = true;
try {
const [statsRes, receivedRes, givenRes] = await Promise.all([
Api.SendReqWithData('/api/viaggi/feedback/user/' + currentUserId.value + '/stats', 'GET'),
Api.SendReqWithData('/api/viaggi/feedback/my/received', 'GET'),
Api.SendReqWithData('/api/viaggi/feedback/my/given', 'GET')
]);
if (statsRes.success) {
stats.value = statsRes.data;
}
if (receivedRes.success) {
receivedFeedbacks.value = receivedRes.data.feedbacks || [];
hasMore.value = receivedRes.data.hasMore || false;
}
if (givenRes.success) {
givenFeedbacks.value = givenRes.data.feedbacks || [];
}
} catch (error: any) {
$q.notify({
type: 'negative',
message: error.data?.message || error.message || 'Errore nel caricamento dei feedback'
});
} finally {
loading.value = false;
}
};
const loadMore = async () => {
loadingMore.value = true;
currentPage.value++;
try {
const endpoint = activeTab.value === 'received'
? '/api/viaggi/feedback/my/received'
: '/api/viaggi/feedback/my/given';
const response = await Api.SendReqWithData(`${endpoint}?page=${currentPage.value}`, 'GET');
if (response.success) {
const newFeedbacks = response.data.feedbacks || [];
if (activeTab.value === 'received') {
receivedFeedbacks.value.push(...newFeedbacks);
} else {
givenFeedbacks.value.push(...newFeedbacks);
}
hasMore.value = response.data.hasMore || false;
}
} catch (error: any) {
$q.notify({
type: 'negative',
message: 'Errore nel caricamento'
});
} finally {
loadingMore.value = false;
}
};
onMounted(() => {
loadFeedbacks();
});
return {
loading,
loadingMore,
activeTab,
stats,
filteredFeedbacks,
hasMore,
goBack,
getInitials,
getOtherUser,
formatDate,
formatTimeAgo,
getTagColor,
viewProfile,
viewRide,
loadMore
};
}
});
</script>
<style lang="scss" scoped>
.my-feedback-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;
}
}
// Stats
&__stats {
padding: 20px;
}
&__rating-card {
background: white;
border-radius: 16px;
padding: 24px;
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
margin-bottom: 16px;
}
&__rating-value {
font-size: 48px;
font-weight: 700;
color: #333;
line-height: 1;
}
&__rating-stars {
display: flex;
justify-content: center;
gap: 4px;
margin: 8px 0;
}
&__rating-count {
color: #666;
font-size: 14px;
}
&__stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
&__stat-item {
background: white;
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
&__stat-value {
font-size: 24px;
font-weight: 700;
color: #333;
}
&__stat-label {
font-size: 12px;
color: #888;
text-align: center;
}
// Tabs
&__tabs {
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
// Content
&__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;
}
}
// List
&__list {
display: flex;
flex-direction: column;
gap: 16px;
}
// Card
&__card {
background: white;
border-radius: 16px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
&__card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 12px;
}
&__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-role {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #888;
}
&__rating {
text-align: right;
}
&__rating-number {
font-size: 24px;
font-weight: 700;
color: #ffc107;
line-height: 1;
}
&__rating-mini-stars {
display: flex;
gap: 1px;
}
// Ride Info
&__ride-info {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: #f8f9fa;
border-radius: 8px;
font-size: 13px;
color: #555;
margin-bottom: 12px;
cursor: pointer;
&:hover {
background: #e9ecef;
}
}
&__ride-date {
margin-left: auto;
color: #888;
font-size: 12px;
}
// Categories
&__categories {
margin-bottom: 12px;
}
&__category {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
&__category-label {
font-size: 13px;
color: #555;
}
&__category-stars {
display: flex;
gap: 2px;
}
// Tags
&__tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
}
// Comment
&__comment {
display: flex;
gap: 8px;
padding: 12px;
background: #fafafa;
border-radius: 8px;
margin-bottom: 12px;
p {
margin: 0;
font-size: 14px;
color: #555;
font-style: italic;
line-height: 1.5;
}
}
// Footer
&__card-footer {
display: flex;
align-items: center;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
&__date {
font-size: 12px;
color: #999;
}
// Load More
&__load-more {
display: flex;
justify-content: center;
padding: 20px;
}
}
// Transitions
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(20px);
}
// Dark Mode
.body--dark {
.my-feedback-page {
background: linear-gradient(135deg, #1a1a2e 0%, #16162a 100%);
&__rating-card,
&__stat-item,
&__card {
background: #1e1e30;
}
&__rating-value,
&__stat-value,
&__user-name {
color: #fff;
}
&__ride-info {
background: rgba(255, 255, 255, 0.05);
&:hover {
background: rgba(255, 255, 255, 0.1);
}
}
&__comment {
background: rgba(255, 255, 255, 0.03);
}
&__card-footer {
border-color: rgba(255, 255, 255, 0.1);
}
}
}
</style>