- Parte 3 : Viaggi
- Chat
This commit is contained in:
549
src/modules/viaggi/components/widgets/RideWidget.scss
Normal file
549
src/modules/viaggi/components/widgets/RideWidget.scss
Normal file
@@ -0,0 +1,549 @@
|
||||
// RideWidget.scss
|
||||
.ride-widget {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 30px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
&--expanded {
|
||||
box-shadow: 0 8px 40px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
// Header
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #5a6fd6 0%, #6a4190 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&__header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
&__icon-wrapper {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
&__icon-badge {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
background: #ff5252;
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 6px;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
&__title-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 12px;
|
||||
margin: 2px 0 0;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&__header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__stats-mini {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__stat-dot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__expand-icon {
|
||||
font-size: 24px;
|
||||
opacity: 0.9;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
// Content
|
||||
&__content {
|
||||
padding: 16px 20px 20px;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
// Stats
|
||||
&__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__stat-card {
|
||||
background: white;
|
||||
border-radius: 14px;
|
||||
padding: 14px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
&--offers {
|
||||
border-left: 3px solid #4caf50;
|
||||
}
|
||||
|
||||
&--requests {
|
||||
border-left: 3px solid #f44336;
|
||||
}
|
||||
|
||||
&--matches {
|
||||
border-left: 3px solid #ff9800;
|
||||
}
|
||||
}
|
||||
|
||||
&__stat-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&__stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__stat-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #1a1a2e;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__stat-label {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
// Actions
|
||||
&__actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__action-btn {
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
&--offer {
|
||||
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #43a047 0%, #256427 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&--request {
|
||||
background: linear-gradient(135deg, #f44336 0%, #c62828 100%);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #e53935 0%, #b71c1c 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recent Rides
|
||||
&__recent {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__recent-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&__rides-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__ride-item {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f8f9ff;
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&__ride-type {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__type-badge {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&__ride-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__ride-route {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1a1a2e;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__ride-city {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__ride-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
&__ride-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__price-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&__price-value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
// Empty State
|
||||
&__empty {
|
||||
text-align: center;
|
||||
padding: 24px 16px;
|
||||
|
||||
p {
|
||||
color: #888;
|
||||
margin: 12px 0;
|
||||
}
|
||||
}
|
||||
|
||||
// My Rides
|
||||
&__my-rides {
|
||||
background: white;
|
||||
border-radius: 14px;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&__my-rides-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__my-ride-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
background: #f8f9fc;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f0f2ff;
|
||||
}
|
||||
}
|
||||
|
||||
&__my-ride-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
&__my-ride-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__my-ride-route {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1a1a2e;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__my-ride-date {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
&__see-all {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
// Alert
|
||||
&__alert {
|
||||
background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: #8d6e00;
|
||||
|
||||
strong {
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.q-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Messages
|
||||
&__messages {
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: #1565c0;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.q-icon:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #eee;
|
||||
margin-top: 4px;
|
||||
|
||||
.q-btn {
|
||||
flex: 1;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
|
||||
:deep(.q-icon) {
|
||||
font-size: 18px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
:deep(.q-btn__content) {
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slide Transition
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
max-height: 600px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slide-enter-from,
|
||||
.slide-leave-to {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
// Dark Mode
|
||||
.body--dark {
|
||||
.ride-widget {
|
||||
background: #1e1e30;
|
||||
|
||||
&__content {
|
||||
background: #16162a;
|
||||
}
|
||||
|
||||
&__stat-card,
|
||||
&__ride-item,
|
||||
&__my-rides {
|
||||
background: #1e1e30;
|
||||
}
|
||||
|
||||
&__stat-value,
|
||||
&__ride-route,
|
||||
&__my-ride-route {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&__my-ride-item {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
|
||||
&:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&__my-ride-icon {
|
||||
background: #2d2d44;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
border-color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 400px) {
|
||||
.ride-widget {
|
||||
&__stats {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__stat-card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.q-btn {
|
||||
flex: 0 0 50%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
286
src/modules/viaggi/components/widgets/RideWidget.ts
Normal file
286
src/modules/viaggi/components/widgets/RideWidget.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
// RideWidget.ts
|
||||
import { defineComponent, ref, computed, onMounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Api } from '@api';
|
||||
import type { Ride, ContribType } from '../../types/viaggi.types';
|
||||
|
||||
interface WidgetStats {
|
||||
offers: number;
|
||||
requests: number;
|
||||
matches: number;
|
||||
}
|
||||
|
||||
interface WidgetData {
|
||||
stats: WidgetStats;
|
||||
recentRides: Ride[];
|
||||
myActiveRides: Ride[];
|
||||
pendingRequests: number;
|
||||
unreadMessages: number;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RideWidget',
|
||||
|
||||
props: {
|
||||
// Se vuoi che il widget parta espanso
|
||||
defaultExpanded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Numero massimo di ride da mostrare
|
||||
maxRides: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
// Auto-refresh interval in ms (0 = disabled)
|
||||
refreshInterval: {
|
||||
type: Number,
|
||||
default: 60000 // 1 minuto
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['loaded', 'error'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const router = useRouter();
|
||||
|
||||
// State
|
||||
const isExpanded = ref(props.defaultExpanded);
|
||||
const loading = ref(false);
|
||||
const stats = ref<WidgetStats>({
|
||||
offers: 0,
|
||||
requests: 0,
|
||||
matches: 0
|
||||
});
|
||||
const recentRides = ref<Ride[]>([]);
|
||||
const myActiveRides = ref<Ride[]>([]);
|
||||
const pendingRequests = ref(0);
|
||||
const unreadMessages = ref(0);
|
||||
|
||||
// Computed
|
||||
const totalCount = computed(() => stats.value.offers + stats.value.requests);
|
||||
|
||||
// Methods
|
||||
const toggleExpand = () => {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
if (isExpanded.value && recentRides.value.length === 0) {
|
||||
loadWidgetData();
|
||||
}
|
||||
};
|
||||
|
||||
const loadWidgetData = async () => {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await Api.SendReqWithData('/api/viaggi/widget/data', 'GET', {});
|
||||
|
||||
if (response.success) {
|
||||
const data: WidgetData = response.data;
|
||||
|
||||
stats.value = data.stats || { offers: 0, requests: 0, matches: 0 };
|
||||
recentRides.value = data.recentRides || [];
|
||||
myActiveRides.value = data.myActiveRides || [];
|
||||
pendingRequests.value = data.pendingRequests || 0;
|
||||
unreadMessages.value = data.unreadMessages || 0;
|
||||
|
||||
emit('loaded', data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Errore caricamento widget trasporti:', error);
|
||||
emit('error', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const response = await Api.SendReqWithData('/api/viaggi/stats/summary', 'GET');
|
||||
|
||||
if (response.success) {
|
||||
stats.value = response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Errore caricamento stats:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (date: string | Date): string => {
|
||||
const d = new Date(date);
|
||||
const now = new Date();
|
||||
const diff = d.getTime() - now.getTime();
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (days === 0) {
|
||||
return `Oggi, ${d.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' })}`;
|
||||
} else if (days === 1) {
|
||||
return `Domani, ${d.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' })}`;
|
||||
} else if (days > 1 && days <= 6) {
|
||||
return d.toLocaleDateString('it-IT', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} else {
|
||||
return d.toLocaleDateString('it-IT', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getContribIcon = (contribution: any): string => {
|
||||
if (!contribution?.types?.length) return '💶';
|
||||
|
||||
const iconMap: Record<string, string> = {
|
||||
'Dono': '🎁',
|
||||
'Offerta Libera': '💸',
|
||||
'Baratto': '🤝',
|
||||
'Scambio Lavoro': '💪',
|
||||
'Monete Alternative': '🪙',
|
||||
'RIS': '🍚',
|
||||
'Euro': '💶',
|
||||
'Bitcoin': '₿',
|
||||
'Banca del Tempo': '⏳'
|
||||
};
|
||||
|
||||
const firstType = contribution.types[0];
|
||||
return iconMap[firstType?.label] || '💶';
|
||||
};
|
||||
|
||||
const getContribLabel = (contribution: any): string => {
|
||||
if (!contribution?.types?.length) return 'Gratuito';
|
||||
|
||||
const labels = contribution.types.map((t: ContribType) => t.label);
|
||||
return labels.join(', ');
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string): string => {
|
||||
const colors: Record<string, string> = {
|
||||
'active': 'positive',
|
||||
'pending': 'warning',
|
||||
'completed': 'info',
|
||||
'cancelled': 'negative'
|
||||
};
|
||||
return colors[status] || 'grey';
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string): string => {
|
||||
const labels: Record<string, string> = {
|
||||
'active': 'Attivo',
|
||||
'pending': 'In attesa',
|
||||
'completed': 'Completato',
|
||||
'cancelled': 'Annullato'
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
// Navigation
|
||||
const goToCreate = (type: 'offer' | 'request') => {
|
||||
router.push({
|
||||
path: '/viaggi/crea',
|
||||
query: { type }
|
||||
});
|
||||
};
|
||||
|
||||
const goToList = () => {
|
||||
router.push('/viaggi');
|
||||
};
|
||||
|
||||
const goToRide = (rideId: string) => {
|
||||
router.push(`/viaggi/ride/${rideId}`);
|
||||
};
|
||||
|
||||
const goToMyRides = () => {
|
||||
router.push('/viaggi/rides/my');
|
||||
};
|
||||
|
||||
const goToSearch = () => {
|
||||
router.push('/viaggi/cerca');
|
||||
};
|
||||
|
||||
const goToMap = () => {
|
||||
router.push('/viaggi/mappa');
|
||||
};
|
||||
|
||||
const goToHistory = () => {
|
||||
router.push('/viaggi/storico');
|
||||
};
|
||||
|
||||
const goToChat = () => {
|
||||
router.push('/viaggi/chat');
|
||||
};
|
||||
|
||||
// Auto-refresh
|
||||
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const startAutoRefresh = () => {
|
||||
if (props.refreshInterval > 0) {
|
||||
refreshTimer = setInterval(() => {
|
||||
if (isExpanded.value) {
|
||||
loadWidgetData();
|
||||
} else {
|
||||
loadStats();
|
||||
}
|
||||
}, props.refreshInterval);
|
||||
}
|
||||
};
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadStats();
|
||||
if (props.defaultExpanded) {
|
||||
loadWidgetData();
|
||||
}
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
watch(() => props.refreshInterval, () => {
|
||||
stopAutoRefresh();
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
isExpanded,
|
||||
loading,
|
||||
stats,
|
||||
recentRides,
|
||||
myActiveRides,
|
||||
pendingRequests,
|
||||
unreadMessages,
|
||||
|
||||
// Computed
|
||||
totalCount,
|
||||
|
||||
// Methods
|
||||
toggleExpand,
|
||||
formatDate,
|
||||
getContribIcon,
|
||||
getContribLabel,
|
||||
getStatusColor,
|
||||
getStatusLabel,
|
||||
|
||||
// Navigation
|
||||
goToCreate,
|
||||
goToList,
|
||||
goToRide,
|
||||
goToMyRides,
|
||||
goToSearch,
|
||||
goToMap,
|
||||
goToHistory,
|
||||
goToChat
|
||||
};
|
||||
}
|
||||
});
|
||||
241
src/modules/viaggi/components/widgets/RideWidget.vue
Normal file
241
src/modules/viaggi/components/widgets/RideWidget.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<!-- RideWidget.vue -->
|
||||
<template>
|
||||
<div class="ride-widget" :class="{ 'ride-widget--expanded': isExpanded }">
|
||||
<!-- Header -->
|
||||
<div class="ride-widget__header" @click="toggleExpand">
|
||||
<div class="ride-widget__header-left">
|
||||
<div class="ride-widget__icon-wrapper">
|
||||
<q-icon name="directions_car" class="ride-widget__icon" />
|
||||
<span class="ride-widget__icon-badge" v-if="totalCount > 0">{{ totalCount }}</span>
|
||||
</div>
|
||||
<div class="ride-widget__title-section">
|
||||
<h3 class="ride-widget__title">Viaggi</h3>
|
||||
<p class="ride-widget__subtitle">Viaggi solidali</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ride-widget__header-right">
|
||||
<div class="ride-widget__stats-mini" v-if="!isExpanded">
|
||||
<span class="ride-widget__stat-dot ride-widget__stat-dot--offer">
|
||||
🟢 {{ stats.offers }}
|
||||
</span>
|
||||
<span class="ride-widget__stat-dot ride-widget__stat-dot--request">
|
||||
🔴 {{ stats.requests }}
|
||||
</span>
|
||||
</div>
|
||||
<q-icon
|
||||
:name="isExpanded ? 'expand_less' : 'expand_more'"
|
||||
class="ride-widget__expand-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expanded Content -->
|
||||
<transition name="slide">
|
||||
<div v-if="isExpanded" class="ride-widget__content">
|
||||
<!-- Stats Cards -->
|
||||
<div class="ride-widget__stats">
|
||||
<div class="ride-widget__stat-card ride-widget__stat-card--offers">
|
||||
<div class="ride-widget__stat-icon">🟢</div>
|
||||
<div class="ride-widget__stat-info">
|
||||
<span class="ride-widget__stat-value">{{ stats.offers }}</span>
|
||||
<span class="ride-widget__stat-label">Offerte</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ride-widget__stat-card ride-widget__stat-card--requests">
|
||||
<div class="ride-widget__stat-icon">🔴</div>
|
||||
<div class="ride-widget__stat-info">
|
||||
<span class="ride-widget__stat-value">{{ stats.requests }}</span>
|
||||
<span class="ride-widget__stat-label">Richieste</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ride-widget__stat-card ride-widget__stat-card--matches">
|
||||
<div class="ride-widget__stat-icon">🤝</div>
|
||||
<div class="ride-widget__stat-info">
|
||||
<span class="ride-widget__stat-value">{{ stats.matches }}</span>
|
||||
<span class="ride-widget__stat-label">Match</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="ride-widget__actions">
|
||||
<q-btn
|
||||
class="ride-widget__action-btn ride-widget__action-btn--offer"
|
||||
unelevated
|
||||
no-caps
|
||||
@click="goToCreate('offer')"
|
||||
>
|
||||
<q-icon name="add_circle" size="20px" />
|
||||
<span>Offri passaggio</span>
|
||||
</q-btn>
|
||||
|
||||
<q-btn
|
||||
class="ride-widget__action-btn ride-widget__action-btn--request"
|
||||
unelevated
|
||||
no-caps
|
||||
@click="goToCreate('request')"
|
||||
>
|
||||
<q-icon name="hail" size="20px" />
|
||||
<span>Cerca passaggio</span>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<!-- Recent Rides Preview -->
|
||||
<div class="ride-widget__recent" v-if="recentRides.length > 0">
|
||||
<div class="ride-widget__recent-header">
|
||||
<span>Ultimi viaggi disponibili</span>
|
||||
<q-btn flat dense no-caps color="primary" label="Vedi tutti" @click="goToList" />
|
||||
</div>
|
||||
|
||||
<div class="ride-widget__rides-list">
|
||||
<div
|
||||
v-for="ride in recentRides"
|
||||
:key="ride._id"
|
||||
class="ride-widget__ride-item"
|
||||
@click="goToRide(ride._id)"
|
||||
>
|
||||
<div class="ride-widget__ride-type">
|
||||
<span v-if="ride.type === 'offer'" class="ride-widget__type-badge ride-widget__type-badge--offer">
|
||||
🟢
|
||||
</span>
|
||||
<span v-else class="ride-widget__type-badge ride-widget__type-badge--request">
|
||||
🔴
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="ride-widget__ride-info">
|
||||
<div class="ride-widget__ride-route">
|
||||
<span class="ride-widget__ride-city">{{ ride.departure?.city }}</span>
|
||||
<q-icon name="arrow_forward" size="14px" color="grey" />
|
||||
<span class="ride-widget__ride-city">{{ ride.destination?.city }}</span>
|
||||
</div>
|
||||
<div class="ride-widget__ride-meta">
|
||||
<q-icon name="event" size="12px" />
|
||||
<span>{{ formatDate(ride.departureDate) }}</span>
|
||||
<template v-if="ride.availableSeats">
|
||||
<q-icon name="person" size="12px" class="q-ml-sm" />
|
||||
<span>{{ ride.availableSeats }} posti</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ride-widget__ride-price" v-if="ride.contribution">
|
||||
<span class="ride-widget__price-icon">{{ getContribIcon(ride.contribution) }}</span>
|
||||
<span class="ride-widget__price-value" v-if="ride.contribution.euroPrice">
|
||||
€{{ ride.contribution.euroPrice }}
|
||||
</span>
|
||||
<span class="ride-widget__price-value" v-else-if="ride.contribution.risPrice">
|
||||
{{ ride.contribution.risPrice }} RIS
|
||||
</span>
|
||||
<span class="ride-widget__price-value" v-else>
|
||||
{{ getContribLabel(ride.contribution) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<q-icon name="chevron_right" color="grey" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="!loading" class="ride-widget__empty">
|
||||
<q-icon name="no_transfer" size="40px" color="grey-4" />
|
||||
<p>Nessun viaggio disponibile</p>
|
||||
<q-btn
|
||||
flat
|
||||
color="primary"
|
||||
label="Esplora"
|
||||
no-caps
|
||||
@click="goToList"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- My Active Rides -->
|
||||
<div class="ride-widget__my-rides" v-if="myActiveRides.length > 0">
|
||||
<div class="ride-widget__section-title">
|
||||
<q-icon name="person" size="18px" />
|
||||
<span>I miei viaggi attivi</span>
|
||||
</div>
|
||||
|
||||
<div class="ride-widget__my-rides-list">
|
||||
<div
|
||||
v-for="ride in myActiveRides.slice(0, 2)"
|
||||
:key="ride._id"
|
||||
class="ride-widget__my-ride-item"
|
||||
@click="goToRide(ride._id)"
|
||||
>
|
||||
<div class="ride-widget__my-ride-icon">
|
||||
<q-icon
|
||||
:name="ride.type === 'offer' ? 'directions_car' : 'hail'"
|
||||
:color="ride.type === 'offer' ? 'positive' : 'negative'"
|
||||
/>
|
||||
</div>
|
||||
<div class="ride-widget__my-ride-info">
|
||||
<span class="ride-widget__my-ride-route">
|
||||
{{ ride.departure?.city }} → {{ ride.destination?.city }}
|
||||
</span>
|
||||
<span class="ride-widget__my-ride-date">
|
||||
{{ formatDate(ride.departureDate) }}
|
||||
</span>
|
||||
</div>
|
||||
<q-badge
|
||||
:color="getStatusColor(ride.status)"
|
||||
:label="getStatusLabel(ride.status)"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
v-if="myActiveRides.length > 2"
|
||||
flat
|
||||
dense
|
||||
no-caps
|
||||
color="primary"
|
||||
class="ride-widget__see-all"
|
||||
@click="goToMyRides"
|
||||
>
|
||||
Vedi tutti ({{ myActiveRides.length }})
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<!-- Pending Requests Alert -->
|
||||
<div class="ride-widget__alert" v-if="pendingRequests > 0">
|
||||
<q-icon name="notifications_active" color="warning" size="20px" />
|
||||
<span>Hai <strong>{{ pendingRequests }}</strong> richieste in attesa</span>
|
||||
<q-btn flat dense no-caps color="warning" label="Gestisci" @click="goToMyRides" />
|
||||
</div>
|
||||
|
||||
<!-- Unread Messages -->
|
||||
<div class="ride-widget__messages" v-if="unreadMessages > 0" @click="goToChat">
|
||||
<q-icon name="chat_bubble" color="primary" />
|
||||
<span>{{ unreadMessages }} messaggi non letti</span>
|
||||
<q-icon name="chevron_right" color="grey" />
|
||||
</div>
|
||||
|
||||
<!-- Footer Links -->
|
||||
<div class="ride-widget__footer">
|
||||
<q-btn flat dense no-caps icon="search" label="Cerca" @click="goToSearch" />
|
||||
<q-btn flat dense no-caps icon="map" label="Mappa" @click="goToMap" />
|
||||
<q-btn flat dense no-caps icon="history" label="Storico" @click="goToHistory" />
|
||||
<q-btn flat dense no-caps icon="chat" label="Chat" @click="goToChat">
|
||||
<q-badge v-if="unreadMessages > 0" color="negative" floating>
|
||||
{{ unreadMessages }}
|
||||
</q-badge>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<q-inner-loading :showing="loading">
|
||||
<q-spinner-dots size="40px" color="primary" />
|
||||
</q-inner-loading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./RideWidget.ts" />
|
||||
<style lang="scss" src="./RideWidget.scss" />
|
||||
Reference in New Issue
Block a user