- Trasporti- Passo 2

This commit is contained in:
Surya Paolo
2025-12-22 23:39:42 +01:00
parent c9fc1a83d0
commit 11e946bfc6
34 changed files with 1682 additions and 1029 deletions

View File

@@ -4,10 +4,15 @@
gap: 8px; gap: 8px;
margin-bottom: 8px; margin-bottom: 8px;
padding: 0 16px; padding: 0 16px;
max-width: 100%; max-width: 85%;
// Messaggi degli altri - allineati a sinistra (default)
align-self: flex-start;
// Messaggi propri - allineati a destra
&--own { &--own {
flex-direction: row-reverse; flex-direction: row-reverse;
align-self: flex-end;
.message-bubble__bubble { .message-bubble__bubble {
background: linear-gradient(135deg, var(--q-primary), var(--q-primary-dark, #1565c0)); background: linear-gradient(135deg, var(--q-primary), var(--q-primary-dark, #1565c0));
@@ -19,6 +24,10 @@
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
} }
.message-bubble__edited {
color: rgba(255, 255, 255, 0.6);
}
.message-bubble__footer { .message-bubble__footer {
justify-content: flex-end; justify-content: flex-end;
} }
@@ -26,54 +35,72 @@
.message-bubble__reactions { .message-bubble__reactions {
justify-content: flex-end; justify-content: flex-end;
} }
.message-bubble__reply {
background: rgba(255, 255, 255, 0.15);
.message-bubble__reply-bar {
background: rgba(255, 255, 255, 0.5);
}
.message-bubble__reply-sender {
color: rgba(255, 255, 255, 0.9);
}
.message-bubble__reply-text {
color: rgba(255, 255, 255, 0.7);
}
}
// Link nei messaggi propri
.message-bubble__text a {
color: #bbdefb;
}
} }
// Messaggi di sistema - centrati
&--system { &--system {
justify-content: center; align-self: center;
padding: 8px 16px; max-width: 90%;
}
&__system {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(0, 0, 0, 0.04);
border-radius: 16px;
font-size: 13px;
color: var(--q-grey-7);
} }
// Avatar
&__avatar { &__avatar {
background: linear-gradient(135deg, var(--q-secondary), var(--q-primary)); flex-shrink: 0;
color: white; background: #e0e0e0;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
flex-shrink: 0; color: #666;
} }
// Content wrapper
&__content { &__content {
max-width: 70%; display: flex;
min-width: 80px; flex-direction: column;
gap: 4px;
min-width: 0;
max-width: 100%;
} }
// Sender name
&__sender { &__sender {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: var(--q-primary); color: var(--q-primary);
margin-bottom: 4px;
margin-left: 12px; margin-left: 12px;
} }
// Reply preview
&__reply { &__reply {
display: flex; display: flex;
align-items: stretch;
gap: 8px; gap: 8px;
padding: 8px 12px; padding: 8px 12px;
margin-bottom: 4px; background: rgba(0, 0, 0, 0.05);
background: rgba(0, 0, 0, 0.04); border-radius: 8px;
border-radius: 12px;
cursor: pointer; cursor: pointer;
transition: background 0.2s ease; margin-bottom: 4px;
transition: background 0.2s;
&:hover { &:hover {
background: rgba(0, 0, 0, 0.08); background: rgba(0, 0, 0, 0.08);
@@ -84,13 +111,14 @@
width: 3px; width: 3px;
background: var(--q-primary); background: var(--q-primary);
border-radius: 2px; border-radius: 2px;
flex-shrink: 0;
} }
&__reply-content { &__reply-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
overflow: hidden; min-width: 0;
} }
&__reply-sender { &__reply-sender {
@@ -101,63 +129,76 @@
&__reply-text { &__reply-text {
font-size: 12px; font-size: 12px;
color: var(--q-grey-7); color: #666;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
// Bubble principale
&__bubble { &__bubble {
background: #f0f0f0; background: white;
border-radius: 18px 18px 18px 4px;
padding: 10px 14px; padding: 10px 14px;
border-radius: 18px 18px 18px 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
position: relative; position: relative;
word-wrap: break-word;
overflow-wrap: break-word;
} }
// Testo messaggio
&__text { &__text {
margin: 0; margin: 0;
font-size: 15px; font-size: 15px;
line-height: 1.4; line-height: 1.4;
word-wrap: break-word; white-space: pre-wrap;
a { a {
color: inherit; color: var(--q-primary);
text-decoration: underline; text-decoration: none;
&:hover {
text-decoration: underline;
}
} }
} }
// Messaggio eliminato
&__deleted { &__deleted {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
font-style: italic; font-style: italic;
color: var(--q-grey-6); color: #999;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 6px;
} }
// Footer
&__footer { &__footer {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
margin-top: 4px; margin-top: 4px;
justify-content: flex-start;
} }
&__time { &__time {
font-size: 11px; font-size: 11px;
color: var(--q-grey-6); color: #999;
} }
&__edited { &__edited {
font-size: 10px; font-size: 10px;
color: #999;
font-style: italic; font-style: italic;
color: var(--q-grey-5);
} }
&__status { &__status {
margin-left: 2px; margin-left: 2px;
} }
// Reactions
&__reactions { &__reactions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -168,43 +209,61 @@
&__reaction { &__reaction {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 2px;
padding: 2px 8px; padding: 2px 8px;
background: rgba(0, 0, 0, 0.06); background: white;
border-radius: 12px; border-radius: 12px;
font-size: 14px; font-size: 14px;
cursor: pointer; cursor: pointer;
transition: background 0.2s ease; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
&:hover { &:hover {
background: rgba(0, 0, 0, 0.1); transform: scale(1.1);
} }
} }
// Menu button
&__menu-btn { &__menu-btn {
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s;
flex-shrink: 0;
} }
&:hover &__menu-btn { &:hover &__menu-btn {
opacity: 1; opacity: 1;
} }
// System message
&__system {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(0, 0, 0, 0.05);
border-radius: 16px;
font-size: 13px;
color: #666;
text-align: center;
}
// Special messages // Special messages
&__special { &__special {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 12px; gap: 12px;
padding: 8px; padding: 4px 0;
background: rgba(var(--q-primary-rgb), 0.08);
border-radius: 12px;
margin-bottom: 4px;
&--success { &--success {
background: rgba(var(--q-positive-rgb), 0.08); .message-bubble__special-title {
color: var(--q-positive);
}
} }
&--error { &--error {
background: rgba(var(--q-negative-rgb), 0.08); .message-bubble__special-title {
color: var(--q-negative);
}
} }
} }
@@ -220,28 +279,30 @@
} }
&__special-text { &__special-text {
font-size: 13px; font-size: 14px;
color: var(--q-grey-8); opacity: 0.9;
} }
// Location message
&__location { &__location {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 8px; padding: 8px 0;
} }
&__location-preview { &__location-preview {
width: 100%; width: 100%;
height: 80px; height: 100px;
background: linear-gradient(135deg, #e8f5e9, #c8e6c9); background: #e8f5e9;
border-radius: 8px; border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
// Ride share
&__ride-share { &__ride-share {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -250,50 +311,22 @@
} }
} }
// Dark mode // Responsive
.body--dark { @media (max-width: 600px) {
.message-bubble { .message-bubble {
&__system { max-width: 90%;
background: rgba(255, 255, 255, 0.08); padding: 0 12px;
color: rgba(255, 255, 255, 0.7);
}
&__bubble { &__bubble {
background: #2d2d2d; padding: 8px 12px;
color: white;
} }
&--own .message-bubble__bubble { &__text {
background: linear-gradient(135deg, var(--q-primary), #1565c0); font-size: 14px;
} }
&__reply { &__menu-btn {
background: rgba(255, 255, 255, 0.08); opacity: 1; // Sempre visibile su mobile
&:hover {
background: rgba(255, 255, 255, 0.12);
}
}
&__reply-text {
color: rgba(255, 255, 255, 0.6);
}
&__reaction {
background: rgba(255, 255, 255, 0.1);
&:hover {
background: rgba(255, 255, 255, 0.15);
}
}
}
}
// Responsive
@media (max-width: 599px) {
.message-bubble {
&__content {
max-width: 85%;
} }
} }
} }

View File

@@ -29,14 +29,24 @@ export default defineComponent({
replyTo: { replyTo: {
type: Object as PropType<Message | null>, type: Object as PropType<Message | null>,
default: null default: null
},
// AGGIUNGI QUESTA PROP
sender: {
type: Object as PropType<UserBasic | null>,
default: null
} }
}, },
emits: ['reply', 'delete', 'reply-click', 'reaction-click', 'ride-click'], emits: ['reply', 'delete', 'reply-click', 'reaction-click', 'ride-click', 'react'],
setup(props, { emit }) { setup(props, { emit }) {
// Sender info // Sender info - USA LA PROP sender SE DISPONIBILE
const sender = computed(() => { const senderData = computed(() => {
// Prima controlla la prop sender
if (props.sender) {
return props.sender;
}
// Fallback: estrai da message.senderId se è un oggetto
if (typeof props.message.senderId === 'object') { if (typeof props.message.senderId === 'object') {
return props.message.senderId as UserBasic; return props.message.senderId as UserBasic;
} }
@@ -44,14 +54,14 @@ export default defineComponent({
}); });
const senderName = computed(() => { const senderName = computed(() => {
if (sender.value?.name) { if (senderData.value?.name) {
return `${sender.value.name} ${sender.value.surname?.[0] || ''}`.trim(); return `${senderData.value.name} ${senderData.value.surname?.[0] || ''}`.trim();
} }
return sender.value?.username || 'Utente'; return senderData.value?.username || 'Utente';
}); });
const senderImg = computed(() => { const senderImg = computed(() => {
return (sender.value as any)?.profile?.img; return (senderData.value as any)?.profile?.img;
}); });
const senderInitials = computed(() => { const senderInitials = computed(() => {
@@ -148,7 +158,7 @@ export default defineComponent({
}; };
return { return {
sender, senderData,
senderName, senderName,
senderImg, senderImg,
senderInitials, senderInitials,

View File

@@ -238,9 +238,9 @@ export default defineComponent({
// Methods // Methods
const loadRecentTripsFromServer = async () => { const loadRecentTripsFromServer = async () => {
try { try {
const response = await Api.SendReq('/api/trasporti/cities/recent', 'GET'); const response = await Api.SendReqWithData('/api/trasporti/cities/recent', 'GET');
if (response.success && response.data?.data?.cities) { if (response.success && response.data?.cities) {
serverRecentTrips.value = response.data.data.cities; serverRecentTrips.value = response.data.cities;
} }
} catch (error) { } catch (error) {
console.error('Error loading recent trips:', error); console.error('Error loading recent trips:', error);

View File

@@ -72,10 +72,10 @@ export default defineComponent({
loading.value = true; loading.value = true;
try { try {
const response = await Api.SendReq('/api/trasporti/widget/data', 'GET', {}); const response = await Api.SendReqWithData('/api/trasporti/widget/data', 'GET', {});
if (response.success) { if (response.success) {
const data: WidgetData = response.data.data; const data: WidgetData = response.data;
stats.value = data.stats || { offers: 0, requests: 0, matches: 0 }; stats.value = data.stats || { offers: 0, requests: 0, matches: 0 };
recentRides.value = data.recentRides || []; recentRides.value = data.recentRides || [];
@@ -95,10 +95,10 @@ export default defineComponent({
const loadStats = async () => { const loadStats = async () => {
try { try {
const response = await Api.SendReq('/api/trasporti/stats/summary', 'GET'); const response = await Api.SendReqWithData('/api/trasporti/stats/summary', 'GET');
if (response.success) { if (response.success) {
stats.value = response.data.data; stats.value = response.data;
} }
} catch (error) { } catch (error) {
console.error('Errore caricamento stats:', error); console.error('Errore caricamento stats:', error);

View File

@@ -0,0 +1,16 @@
// composables/useAuth.ts
import { computed } from 'vue';
import { useUserStore } from '@store/UserStore';
export function useAuth() {
const userStore = useUserStore()
const user = computed(() => userStore.my);
const isAuthenticated = computed(() => !!userStore.my);
const userId = computed(() => userStore.my?._id);
return {
user,
isAuthenticated,
userId
};
}

View File

@@ -2,6 +2,7 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { Api } from '@api'; import { Api } from '@api';
import type { Chat, Message } from '../types/trasporti.types'; import type { Chat, Message } from '../types/trasporti.types';
import { tools } from 'app/src/store/Modules/tools';
// ============================================================ // ============================================================
// STATE // STATE
@@ -21,13 +22,27 @@ const error = ref<string | null>(null);
const onlineUsers = ref<string[]>([]); const onlineUsers = ref<string[]>([]);
const typingUsers = ref<string[]>([]); const typingUsers = ref<string[]>([]);
interface FetchMessagesOptions {
loadOlder?: boolean; // Carica messaggi più vecchi
loadNewer?: boolean; // Carica messaggi più recenti
reset?: boolean; // Reset completo
limit?: number;
}
const hasOlderMessages = ref(true);
const hasNewerMessages = ref(false);
// POLLING STATE
let pollingInterval: ReturnType<typeof setInterval> | null = null;
const isPolling = ref(false);
const POLLING_INTERVAL = 3000; // 3 secondi
// ============================================================ // ============================================================
// COMPOSABLE // COMPOSABLE
// ============================================================ // ============================================================
export function useChat() { export function useChat() {
// ID app per trasporti // ID app per trasporti
const IDAPP = 'trasporti';
// ------------------------------------------------------------ // ------------------------------------------------------------
// COMPUTED // COMPUTED
@@ -64,16 +79,16 @@ export function useChat() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/chats?idapp=${IDAPP}&page=${page}&limit=${limit}`, `/api/trasporti/chats?page=${page}&limit=${limit}`,
'GET' 'GET'
); );
if (response.success && response.data.data) { if (response.success && response.data) {
chats.value = response.data.data; chats.value = response.data;
// Calcola unread totale // Calcola unread totale
totalUnreadCount.value = response.data.data.reduce( totalUnreadCount.value = response.data.reduce(
(sum: number, chat: any) => sum + (chat.unreadCount || 0), (sum: number, chat: any) => sum + (chat.unreadCount || 0),
0 0
); );
@@ -96,21 +111,16 @@ export function useChat() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData('/api/trasporti/chats/direct', 'POST', {
'/api/trasporti/chats/direct', otherUserId,
'POST', rideId,
{ });
idapp: IDAPP,
otherUserId,
rideId
}
);
if (response.success && response.data.data) { if (response.success && response.data) {
currentChat.value = response.data.data; currentChat.value = response.data;
// Aggiungi alla lista se non presente // Aggiungi alla lista se non presente
const exists = chats.value.find(c => c._id === currentChat.value?._id); const exists = chats.value.find((c) => c._id === currentChat.value?._id);
if (!exists && currentChat.value) { if (!exists && currentChat.value) {
chats.value.unshift(currentChat.value); chats.value.unshift(currentChat.value);
} }
@@ -140,13 +150,10 @@ export function useChat() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(`/api/trasporti/chats/${chatId}`, 'GET');
`/api/trasporti/chats/${chatId}`,
'GET'
);
if (response.success && response.data?.data) { if (response.success && response.data) {
currentChat.value = response.data?.data; currentChat.value = response.data;
} }
return response; return response;
@@ -168,48 +175,86 @@ export function useChat() {
const response = await fetchMessages(chatId, options); const response = await fetchMessages(chatId, options);
return response.data || []; return response.data || [];
}; };
const fetchMessages = async (
chatId: string,
options: FetchMessagesOptions = {}
) => {
try {
loadingMessages.value = true;
error.value = null;
/** const params = new URLSearchParams({ idapp: tools.getIdApp() });
* Ottieni messaggi di una chat
*/
const fetchMessages = async (
chatId: string,
options?: { before?: string; after?: string; limit?: number }
) => {
try {
loadingMessages.value = true;
error.value = null;
const params = new URLSearchParams({ idapp: IDAPP }); // ✅ Determina i parametri in base al tipo di caricamento
if (options?.before) params.append('before', options.before); if (options.loadOlder && messages.value.length > 0) {
if (options?.after) params.append('after', options.after); // Carica messaggi più vecchi del primo messaggio attuale
if (options?.limit) params.append('limit', options.limit.toString()); const oldestMessage = messages.value[0];
params.append('before', oldestMessage.createdAt);
const response = await Api.SendReq( } else if (options.loadNewer && messages.value.length > 0) {
`/api/trasporti/chats/${chatId}/messages?${params.toString()}`, // Carica messaggi più recenti dell'ultimo messaggio attuale
'GET' const newestMessage = messages.value[messages.value.length - 1];
); params.append('after', newestMessage.createdAt);
if (response.success && response.data?.data) {
const newMessages = response.data?.data;
if (options?.before) {
// Caricamento messaggi precedenti - aggiungi all'inizio
messages.value = [...newMessages, ...messages.value];
} else {
// Primo caricamento
messages.value = newMessages;
}
}
return response;
} catch (err: any) {
error.value = err.message || 'Errore nel recupero dei messaggi';
throw err;
} finally {
loadingMessages.value = false;
} }
};
if (options.limit) {
params.append('limit', options.limit.toString());
}
const response = await Api.SendReqWithData(
`/api/trasporti/chats/${chatId}/messages?${params.toString()}`,
'GET'
);
if (response.success && response.data) {
const newMessages = response.data;
// ✅ Gestione chiara dei diversi scenari
if (options.reset || messages.value.length === 0) {
// Primo caricamento o reset
messages.value = newMessages;
hasOlderMessages.value = response.hasMore;
} else if (options.loadOlder) {
// Aggiungi messaggi più vecchi all'inizio
messages.value = [...newMessages, ...messages.value];
hasOlderMessages.value = response.hasMore;
} else if (options.loadNewer) {
// Aggiungi messaggi più recenti alla fine
messages.value = [...messages.value, ...newMessages];
hasNewerMessages.value = response.hasMore;
}
}
return response;
} catch (err: any) {
error.value = err.message || 'Errore nel recupero dei messaggi';
throw err;
} finally {
loadingMessages.value = false;
}
};
// ✅ Metodi helper per chiamare facilmente
const loadInitialMessages = (chatId: string) => {
return fetchMessages(chatId, { reset: true });
};
const loadOlderMessages = (chatId: string) => {
if (!hasOlderMessages.value || loadingMessages.value) return;
return fetchMessages(chatId, { loadOlder: true });
};
const loadNewerMessages = (chatId: string) => {
if (!hasNewerMessages.value || loadingMessages.value) return;
return fetchMessages(chatId, { loadNewer: true });
};
// ✅ Aggiungi un nuovo messaggio ricevuto in tempo reale
const addNewMessage = (message: Message) => {
// Evita duplicati
if (!messages.value.find(m => m._id === message._id)) {
messages.value.push(message);
}
};
/** /**
* Invia messaggio - compatibile con ChatPage * Invia messaggio - compatibile con ChatPage
@@ -231,20 +276,19 @@ export function useChat() {
// Supporta sia content che text // Supporta sia content che text
const messageText = payload.content || payload.text || ''; const messageText = payload.content || payload.text || '';
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/chats/${chatId}/messages`, `/api/trasporti/chats/${chatId}/messages`,
'POST', 'POST',
{ {
idapp: IDAPP,
text: messageText, text: messageText,
type: payload.type || 'text', type: payload.type || 'text',
metadata: payload.metadata, metadata: payload.metadata,
replyTo: payload.replyTo replyTo: payload.replyTo,
} }
); );
if (response.success && response.data?.data) { if (response.success && response.data) {
const newMessage = response.data?.data; const newMessage = response.data;
messages.value.push(newMessage); messages.value.push(newMessage);
// Aggiorna lastMessage nella chat // Aggiorna lastMessage nella chat
@@ -253,7 +297,7 @@ export function useChat() {
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.message || 'Errore nell\'invio del messaggio'; error.value = err.message || "Errore nell'invio del messaggio";
throw err; throw err;
} finally { } finally {
sending.value = false; sending.value = false;
@@ -264,13 +308,13 @@ export function useChat() {
* Aggiorna lastMessage nella chat locale * Aggiorna lastMessage nella chat locale
*/ */
const updateChatLastMessage = (chatId: string, message: Message) => { const updateChatLastMessage = (chatId: string, message: Message) => {
const chatIndex = chats.value.findIndex(c => c._id === chatId); const chatIndex = chats.value.findIndex((c) => c._id === chatId);
if (chatIndex !== -1) { if (chatIndex !== -1) {
chats.value[chatIndex].lastMessage = { chats.value[chatIndex].lastMessage = {
text: message.text || '', text: message.text || '',
senderId: message.senderId as any, senderId: message.senderId as any,
timestamp: message.createdAt, timestamp: message.createdAt,
type: message.type || 'text' type: message.type || 'text',
}; };
chats.value[chatIndex].updatedAt = message.createdAt; chats.value[chatIndex].updatedAt = message.createdAt;
} }
@@ -280,7 +324,7 @@ export function useChat() {
text: message.text || '', text: message.text || '',
senderId: message.senderId as any, senderId: message.senderId as any,
timestamp: message.createdAt, timestamp: message.createdAt,
type: message.type || 'text' type: message.type || 'text',
}; };
currentChat.value.updatedAt = message.createdAt; currentChat.value.updatedAt = message.createdAt;
} }
@@ -291,14 +335,14 @@ export function useChat() {
*/ */
const markAsRead = async (chatId: string) => { const markAsRead = async (chatId: string) => {
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/chats/${chatId}/read`, `/api/trasporti/chats/${chatId}/read`,
'PUT' 'PUT'
); );
if (response.success) { if (response.success) {
// Aggiorna contatore locale // Aggiorna contatore locale
const chatIndex = chats.value.findIndex(c => c._id === chatId); const chatIndex = chats.value.findIndex((c) => c._id === chatId);
if (chatIndex !== -1) { if (chatIndex !== -1) {
const unread = chats.value[chatIndex].unreadCount || 0; const unread = chats.value[chatIndex].unreadCount || 0;
totalUnreadCount.value = Math.max(0, totalUnreadCount.value - unread); totalUnreadCount.value = Math.max(0, totalUnreadCount.value - unread);
@@ -321,13 +365,13 @@ export function useChat() {
*/ */
const fetchUnreadCount = async () => { const fetchUnreadCount = async () => {
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/chats/unread/count?idapp=${IDAPP}`, `/api/trasporti/chats/unread/count?idapp=${IDAPP}`,
'GET' 'GET'
); );
if (response.success && response.data?.data) { if (response.success && response.data) {
totalUnreadCount.value = response.data?.data.total || 0; totalUnreadCount.value = response.data.total || 0;
} }
return response; return response;
@@ -341,13 +385,13 @@ export function useChat() {
*/ */
const deleteMessage = async (chatId: string, messageId: string) => { const deleteMessage = async (chatId: string, messageId: string) => {
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/chats/${chatId}/messages/${messageId}`, `/api/trasporti/chats/${chatId}/messages/${messageId}`,
'DELETE' 'DELETE'
); );
if (response.success) { if (response.success) {
const index = messages.value.findIndex(m => m._id === messageId); const index = messages.value.findIndex((m) => m._id === messageId);
if (index !== -1) { if (index !== -1) {
messages.value[index].isDeleted = true; messages.value[index].isDeleted = true;
messages.value[index].text = '[Messaggio eliminato]'; messages.value[index].text = '[Messaggio eliminato]';
@@ -356,7 +400,7 @@ export function useChat() {
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.message || 'Errore nell\'eliminazione del messaggio'; error.value = err.message || "Errore nell'eliminazione del messaggio";
throw err; throw err;
} }
}; };
@@ -366,7 +410,7 @@ export function useChat() {
*/ */
const toggleBlockChat = async (chatId: string, block: boolean) => { const toggleBlockChat = async (chatId: string, block: boolean) => {
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/chats/${chatId}/block`, `/api/trasporti/chats/${chatId}/block`,
'PUT', 'PUT',
{ block } { block }
@@ -384,7 +428,7 @@ export function useChat() {
*/ */
const toggleMuteChat = async (chatId: string, mute: boolean) => { const toggleMuteChat = async (chatId: string, mute: boolean) => {
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/chats/${chatId}/mute`, `/api/trasporti/chats/${chatId}/mute`,
'PUT', 'PUT',
{ mute } { mute }
@@ -473,6 +517,64 @@ export function useChat() {
messages.value = []; messages.value = [];
}; };
// ============================================================
// POLLING
// ============================================================
/**
* Avvia polling per nuovi messaggi
*/
const startPolling = (chatId: string, intervalMs = POLLING_INTERVAL) => {
stopPolling(); // Ferma eventuale polling precedente
isPolling.value = true;
console.log('[useChat] Polling avviato per chat:', chatId);
pollingInterval = setInterval(async () => {
try {
// Solo se ci sono già messaggi caricati
if (messages.value.length > 0) {
const newestMessage = messages.value[messages.value.length - 1];
const params = new URLSearchParams({
idapp: tools.getIdApp(),
after: newestMessage.createdAt
});
const response = await Api.SendReqWithData(
`/api/trasporti/chats/${chatId}/messages?${params.toString()}`,
'GET'
);
if (response.success && response.data && response.data.length > 0) {
console.log('[useChat] Polling - ricevuti', response.data.length, 'nuovi messaggi');
// Aggiungi nuovi messaggi evitando duplicati
response.data.forEach((newMsg: Message) => {
if (!messages.value.find(m => m._id === newMsg._id)) {
messages.value.push(newMsg);
}
});
}
}
} catch (err) {
console.error('[useChat] Errore polling:', err);
}
}, intervalMs);
};
/**
* Ferma polling
*/
const stopPolling = () => {
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null;
isPolling.value = false;
console.log('[useChat] Polling fermato');
}
};
// ------------------------------------------------------------ // ------------------------------------------------------------
// RETURN // RETURN
// ------------------------------------------------------------ // ------------------------------------------------------------
@@ -489,12 +591,16 @@ export function useChat() {
error, error,
onlineUsers, onlineUsers,
typingUsers, typingUsers,
isPolling, // AGGIUNGI
// Computed // Computed
hasChats, hasChats,
hasUnread, hasUnread,
sortedChats, sortedChats,
sortedMessages, sortedMessages,
hasOlderMessages, // AGGIUNGI
hasNewerMessages, // AGGIUNGI
// API Methods // API Methods
fetchChats, fetchChats,
@@ -509,6 +615,14 @@ export function useChat() {
toggleBlockChat, toggleBlockChat,
toggleMuteChat, toggleMuteChat,
deleteMessage, deleteMessage,
loadOlderMessages, // AGGIUNGI
loadNewerMessages, // AGGIUNGI
addNewMessage, // AGGIUNGI
// Polling
startPolling, // AGGIUNGI
stopPolling, // AGGIUNGI
// Real-time (placeholder) // Real-time (placeholder)
sendTyping, sendTyping,
@@ -519,6 +633,6 @@ export function useChat() {
formatMessageTime, formatMessageTime,
openChat, openChat,
clearState, clearState,
closeCurrentChat closeCurrentChat,
}; };
} }

View File

@@ -55,13 +55,13 @@ export function useCitySuggestions() {
lastQuery.value = query; lastQuery.value = query;
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/cities/suggestions?q=${encodeURIComponent(query)}`, `/api/trasporti/cities/suggestions?q=${encodeURIComponent(query)}`,
'GET' 'GET'
); );
if (response.success) { if (response.success) {
suggestions.value = response.data?.data.suggestions || []; suggestions.value = response.data.suggestions || [];
} else { } else {
error.value = response.message || 'Errore nel caricamento dei suggerimenti'; error.value = response.message || 'Errore nel caricamento dei suggerimenti';
suggestions.value = []; suggestions.value = [];

View File

@@ -68,13 +68,13 @@ export function useContribTypes() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
'/api/trasporti/contrib-types', '/api/trasporti/contrib-types',
'GET' 'GET'
) as ApiResponse<ContribType[]>; ) as ApiResponse<ContribType[]>;
if (response.success && response.data?.data) { if (response.success && response.data) {
contribTypes.value = response.data.data; contribTypes.value = response.data;
fetched = true; fetched = true;
} }

View File

@@ -4,9 +4,8 @@ import type {
DriverProfile, DriverProfile,
Vehicle, Vehicle,
UserPreferences, UserPreferences,
DriverPublicProfile, DriverPublicProfile
ApiResponse } from '../types/trasporti.types';
} from '../types';
// ============================================================ // ============================================================
// STATE // STATE
@@ -53,18 +52,18 @@ export function useDriverProfile() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/driver/${userId}`, `/api/trasporti/driver/user/${userId}`,
'GET' 'GET'
) as ApiResponse<DriverPublicProfile>; );
if (response.success && response.data?.data) { if (response.success && response.data) {
driverProfile.value = response.data.data; driverProfile.value = response.data;
} }
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nel recupero del profilo'; error.value = err.message || 'Errore nel recupero del profilo';
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -72,27 +71,38 @@ export function useDriverProfile() {
}; };
/** /**
* Aggiorna il mio profilo conducente * Aggiorna il mio profilo conducente e/o preferenze
*/ */
const updateDriverProfile = async (profileData: Partial<DriverProfile>) => { const updateDriverProfile = async (data: {
driverProfile?: Partial<DriverProfile>;
preferences?: Partial<UserPreferences>;
}) => {
try { try {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
'/api/trasporti/driver/profile', '/api/trasporti/driver/profile',
'PUT', 'PUT',
{ driverProfile: profileData } {
) as ApiResponse<{ driverProfile: DriverProfile; preferences: UserPreferences }>; idapp: 'trasporti',
...data
}
);
if (response.success && response.data?.data) { if (response.success && response.data) {
myDriverProfile.value = response.data?.data.driverProfile; // Il backend ritorna user.profile che contiene driverProfile e preferences
myPreferences.value = response.data?.data.preferences; if (response.data.driverProfile) {
myDriverProfile.value = response.data.driverProfile;
}
if (response.data.preferences) {
myPreferences.value = response.data.preferences;
}
} }
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nell\'aggiornamento del profilo'; error.value = err.message || 'Errore nell\'aggiornamento del profilo';
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -100,53 +110,33 @@ export function useDriverProfile() {
}; };
/** /**
* Aggiorna le mie preferenze * Aggiorna solo le preferenze
*/ */
const updatePreferences = async (preferences: Partial<UserPreferences>) => { const updatePreferences = async (preferences: Partial<UserPreferences>) => {
try { return await updateDriverProfile({ preferences });
loading.value = true;
error.value = null;
const response = await Api.SendReq(
'/api/trasporti/driver/profile',
'PUT',
{ preferences }
) as ApiResponse<{ driverProfile: DriverProfile; preferences: UserPreferences }>;
if (response.success && response.data?.data) {
myPreferences.value = response.data?.data.preferences;
}
return response;
} catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nell\'aggiornamento delle preferenze';
throw err;
} finally {
loading.value = false;
}
}; };
/** /**
* Aggiungi veicolo * Aggiungi veicolo
*/ */
const addVehicle = async (vehicle: Omit<Vehicle, '_id'>) => { const addVehicle = async (vehicleData: Omit<Vehicle, '_id'>) => {
try { try {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
'/api/trasporti/driver/vehicles', '/api/trasporti/driver/vehicles',
'POST', 'POST',
{ vehicle } { vehicle: vehicleData }
) as ApiResponse<Vehicle[]>; );
if (response.success && response.data?.data) { if (response.success && response.data) {
myVehicles.value = response.data.data; myVehicles.value = response.data;
} }
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nell\'aggiunta del veicolo'; error.value = err.message || 'Errore nell\'aggiunta del veicolo';
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -156,24 +146,24 @@ export function useDriverProfile() {
/** /**
* Aggiorna veicolo * Aggiorna veicolo
*/ */
const updateVehicle = async (vehicleId: string, vehicle: Partial<Vehicle>) => { const updateVehicle = async (vehicleId: string, vehicleData: Partial<Vehicle>) => {
try { try {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/driver/vehicles/${vehicleId}`, `/api/trasporti/driver/vehicles/${vehicleId}`,
'PUT', 'PUT',
{ vehicle } { vehicle: vehicleData }
) as ApiResponse<Vehicle[]>; );
if (response.success && response.data?.data) { if (response.success && response.data) {
myVehicles.value = response.data.data; myVehicles.value = response.data;
} }
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nell\'aggiornamento del veicolo'; error.value = err.message || 'Errore nell\'aggiornamento del veicolo';
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -188,10 +178,10 @@ export function useDriverProfile() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/driver/vehicles/${vehicleId}`, `/api/trasporti/driver/vehicles/${vehicleId}`,
'DELETE' 'DELETE'
) as ApiResponse<void>; );
if (response.success) { if (response.success) {
myVehicles.value = myVehicles.value.filter(v => v._id !== vehicleId); myVehicles.value = myVehicles.value.filter(v => v._id !== vehicleId);
@@ -199,7 +189,7 @@ export function useDriverProfile() {
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nella rimozione del veicolo'; error.value = err.message || 'Errore nella rimozione del veicolo';
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -214,10 +204,10 @@ export function useDriverProfile() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/driver/vehicles/${vehicleId}/default`, `/api/trasporti/driver/vehicles/${vehicleId}/default`,
'POST' 'POST'
) as ApiResponse<void>; );
if (response.success) { if (response.success) {
// Aggiorna localmente // Aggiorna localmente
@@ -229,7 +219,7 @@ export function useDriverProfile() {
return response; return response;
} catch (err: any) { } catch (err: any) {
error.value = err.data?.message || err.message ||'Errore nell\'impostazione del veicolo predefinito'; error.value = err.message || 'Errore nell\'impostazione del veicolo predefinito';
throw err; throw err;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -332,7 +322,7 @@ export function useDriverProfile() {
}; };
/** /**
* Inizializza profilo dal user corrente * Inizializza profilo dal user corrente (userStore)
*/ */
const initFromUser = (user: any) => { const initFromUser = (user: any) => {
if (user?.profile?.driverProfile) { if (user?.profile?.driverProfile) {

View File

@@ -59,14 +59,14 @@ export function useFeedback() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
'/api/trasporti/feedback', '/api/trasporti/feedback',
'POST', 'POST',
feedbackData feedbackData
) as ApiResponse<Feedback>; ) as ApiResponse<Feedback>;
if (response.success && response.data?.data) { if (response.success && response.data) {
myGivenFeedback.value.unshift(response.data?.data); myGivenFeedback.value.unshift(response.data);
} }
return response; return response;
@@ -91,7 +91,7 @@ export function useFeedback() {
if (options?.page) queryParams.append('page', options.page.toString()); if (options?.page) queryParams.append('page', options.page.toString());
if (options?.limit) queryParams.append('limit', options.limit.toString()); if (options?.limit) queryParams.append('limit', options.limit.toString());
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/feedback/user/${userId}?${queryParams.toString()}`, `/api/trasporti/feedback/user/${userId}?${queryParams.toString()}`,
'GET' 'GET'
) as ApiResponse<{ ) as ApiResponse<{
@@ -100,10 +100,10 @@ export function useFeedback() {
distribution: RatingDistribution[]; distribution: RatingDistribution[];
}>; }>;
if (response.success && response.data?.data) { if (response.success && response.data) {
feedbacks.value = response.data?.data.feedbacks; feedbacks.value = response.data.feedbacks;
currentUserStats.value = response.data?.data.stats; currentUserStats.value = response.data.stats;
ratingDistribution.value = response.data?.data.distribution; ratingDistribution.value = response.data.distribution;
} }
return response; return response;
@@ -120,7 +120,7 @@ export function useFeedback() {
*/ */
const fetchUserStats = async (userId: string) => { const fetchUserStats = async (userId: string) => {
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/feedback/user/${userId}/stats`, `/api/trasporti/feedback/user/${userId}/stats`,
'GET' 'GET'
) as ApiResponse<{ ) as ApiResponse<{
@@ -129,8 +129,8 @@ export function useFeedback() {
commonTags: { _id: FeedbackTag; count: number }[]; commonTags: { _id: FeedbackTag; count: number }[];
}>; }>;
if (response.success && response.data?.data) { if (response.success && response.data) {
currentUserStats.value = response.data?.data.stats; currentUserStats.value = response.data.stats;
} }
return response; return response;
@@ -148,13 +148,13 @@ export function useFeedback() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/feedback/ride/${rideId}`, `/api/trasporti/feedback/ride/${rideId}`,
'GET' 'GET'
) as ApiResponse<Feedback[]>; ) as ApiResponse<Feedback[]>;
if (response.success && response.data?.data) { if (response.success && response.data) {
feedbacks.value = response.data.data; feedbacks.value = response.data;
} }
return response; return response;
@@ -171,12 +171,12 @@ export function useFeedback() {
*/ */
const canLeaveFeedback = async (rideId: string, toUserId: string) => { const canLeaveFeedback = async (rideId: string, toUserId: string) => {
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/feedback/can-leave/${rideId}/${toUserId}`, `/api/trasporti/feedback/can-leave/${rideId}/${toUserId}`,
'GET' 'GET'
) as ApiResponse<{ canLeave: boolean; reason?: string }>; ) as ApiResponse<{ canLeave: boolean; reason?: string }>;
return response.data?.data; return response.data;
} catch (err: any) { } catch (err: any) {
console.error('Errore verifica feedback:', err); console.error('Errore verifica feedback:', err);
return { canLeave: false, reason: 'Errore nella verifica' }; return { canLeave: false, reason: 'Errore nella verifica' };
@@ -194,14 +194,14 @@ export function useFeedback() {
const queryParams = new URLSearchParams(); const queryParams = new URLSearchParams();
if (role) queryParams.append('role', role); if (role) queryParams.append('role', role);
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/feedback/my/received?${queryParams.toString()}`, `/api/trasporti/feedback/my/received?${queryParams.toString()}`,
'GET' 'GET'
) as ApiResponse<{ feedbacks: Feedback[]; stats: FeedbackStats }>; ) as ApiResponse<{ feedbacks: Feedback[]; stats: FeedbackStats }>;
if (response.success && response.data?.data) { if (response.success && response.data) {
myReceivedFeedback.value = response.data?.data.feedbacks; myReceivedFeedback.value = response.data.feedbacks;
currentUserStats.value = response.data?.data.stats; currentUserStats.value = response.data.stats;
} }
return response; return response;
@@ -221,13 +221,13 @@ export function useFeedback() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
'/api/trasporti/feedback/my/given', '/api/trasporti/feedback/my/given',
'GET' 'GET'
) as PaginatedResponse<Feedback>; ) as PaginatedResponse<Feedback>;
if (response.success) { if (response.success) {
myGivenFeedback.value = response.data.data; myGivenFeedback.value = response.data;
} }
return response; return response;
@@ -247,17 +247,17 @@ export function useFeedback() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/feedback/${feedbackId}/response`, `/api/trasporti/feedback/${feedbackId}/response`,
'POST', 'POST',
{ text } { text }
) as ApiResponse<Feedback>; ) as ApiResponse<Feedback>;
if (response.success && response.data?.data) { if (response.success && response.data) {
// Aggiorna nella lista // Aggiorna nella lista
const index = myReceivedFeedback.value.findIndex(f => f._id === feedbackId); const index = myReceivedFeedback.value.findIndex(f => f._id === feedbackId);
if (index !== -1) { if (index !== -1) {
myReceivedFeedback.value[index] = response.data.data; myReceivedFeedback.value[index] = response.data;
} }
} }
@@ -275,7 +275,7 @@ export function useFeedback() {
*/ */
const reportFeedback = async (feedbackId: string, reason: string) => { const reportFeedback = async (feedbackId: string, reason: string) => {
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/feedback/${feedbackId}/report`, `/api/trasporti/feedback/${feedbackId}/report`,
'POST', 'POST',
{ reason } { reason }
@@ -293,15 +293,15 @@ export function useFeedback() {
*/ */
const markAsHelpful = async (feedbackId: string) => { const markAsHelpful = async (feedbackId: string) => {
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/feedback/${feedbackId}/helpful`, `/api/trasporti/feedback/${feedbackId}/helpful`,
'POST' 'POST'
) as ApiResponse<{ helpfulCount: number }>; ) as ApiResponse<{ helpfulCount: number }>;
if (response.success && response.data?.data) { if (response.success && response.data) {
const feedback = feedbacks.value.find(f => f._id === feedbackId); const feedback = feedbacks.value.find(f => f._id === feedbackId);
if (feedback && feedback.helpful) { if (feedback && feedback.helpful) {
feedback.helpful.count = response.data?.data.helpfulCount; feedback.helpful.count = response.data.helpfulCount;
} }
} }

View File

@@ -96,7 +96,7 @@ export function useRealtimeChat() {
// Simula utenti online (in produzione questi dati verrebbero dal server) // Simula utenti online (in produzione questi dati verrebbero dal server)
if (currentChat.value?.participants) { if (currentChat.value?.participants) {
currentChat.value.participants.forEach(participant => { currentChat.value.participants.forEach((participant: any) => {
if (participant._id) { if (participant._id) {
simulateUserOnline(participant._id); simulateUserOnline(participant._id);
} }
@@ -127,7 +127,7 @@ export function useRealtimeChat() {
console.log(`Typing in chat: ${chatId}`); console.log(`Typing in chat: ${chatId}`);
// In produzione: // In produzione:
// await Api.SendReq(`/api/trasporti/chats/${chatId}/typing`, 'POST'); // await Api.SendReqWithData(`/api/trasporti/chats/${chatId}/typing`, 'POST');
}; };
/** /**

View File

@@ -69,15 +69,15 @@ export function useRideRequests() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
'/api/trasporti/requests', '/api/trasporti/requests',
'POST', 'POST',
requestData, requestData,
false, true false, true
) as ApiResponse<{ request: RideRequest; chatId: string }>; ) as ApiResponse<{ request: RideRequest; chatId: string }>;
if (response.success && response.data.data) { if (response.success && response.data) {
sentRequests.value.unshift(response.data.data.request); sentRequests.value.unshift(response.data.request);
} }
return response; return response;
@@ -102,15 +102,15 @@ export function useRideRequests() {
queryParams.append('page', pagination.page.toString()); queryParams.append('page', pagination.page.toString());
queryParams.append('limit', pagination.limit.toString()); queryParams.append('limit', pagination.limit.toString());
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/requests/received?${queryParams.toString()}`, `/api/trasporti/requests/received?${queryParams.toString()}`,
'GET' 'GET'
) as RequestsReceivedResponse; ) as RequestsReceivedResponse;
if (response.success) { if (response.success) {
receivedRequests.value = response.data.data; receivedRequests.value = response.data.requests;
requestCounts.value = response.data?.data.counts; requestCounts.value = response.data.counts;
Object.assign(pagination, response.data?.data.pagination); Object.assign(pagination, response.data.pagination);
} }
return response; return response;
@@ -135,13 +135,13 @@ export function useRideRequests() {
queryParams.append('page', pagination.page.toString()); queryParams.append('page', pagination.page.toString());
queryParams.append('limit', pagination.limit.toString()); queryParams.append('limit', pagination.limit.toString());
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/requests/sent?${queryParams.toString()}`, `/api/trasporti/requests/sent?${queryParams.toString()}`,
'GET' 'GET'
) as PaginatedResponse<RideRequest>; ) as PaginatedResponse<RideRequest>;
if (response.success) { if (response.success) {
sentRequests.value = response.data.data; sentRequests.value = response.data;
Object.assign(pagination, response?.data.pagination); Object.assign(pagination, response?.data.pagination);
} }
@@ -165,7 +165,7 @@ export function useRideRequests() {
const queryParams = new URLSearchParams(); const queryParams = new URLSearchParams();
if (status) queryParams.append('status', status); if (status) queryParams.append('status', status);
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/requests/ride/${rideId}?${queryParams.toString()}`, `/api/trasporti/requests/ride/${rideId}?${queryParams.toString()}`,
'GET' 'GET'
) as ApiResponse<RideRequest[]>; ) as ApiResponse<RideRequest[]>;
@@ -187,13 +187,13 @@ export function useRideRequests() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/requests/${requestId}`, `/api/trasporti/requests/${requestId}`,
'GET' 'GET'
) as ApiResponse<RideRequest>; ) as ApiResponse<RideRequest>;
if (response.success && response.data?.data) { if (response.success && response.data) {
currentRequest.value = response.data.data; currentRequest.value = response.data;
} }
return response; return response;
@@ -213,15 +213,15 @@ export function useRideRequests() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/requests/${requestId}/accept`, `/api/trasporti/requests/${requestId}/accept`,
'POST', 'POST',
{ responseMessage } { responseMessage }
) as ApiResponse<RideRequest>; ) as ApiResponse<RideRequest>;
if (response.success && response.data?.data) { if (response.success && response.data) {
// Aggiorna nella lista // Aggiorna nella lista
updateRequestInList(requestId, response.data?.data); updateRequestInList(requestId, response.data);
} }
return response; return response;
@@ -241,14 +241,14 @@ export function useRideRequests() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/requests/${requestId}/reject`, `/api/trasporti/requests/${requestId}/reject`,
'POST', 'POST',
{ responseMessage } { responseMessage }
) as ApiResponse<RideRequest>; ) as ApiResponse<RideRequest>;
if (response.success && response.dat?.data) { if (response.success && response.dat?.data) {
updateRequestInList(requestId, response.data?.data); updateRequestInList(requestId, response.data);
} }
return response; return response;
@@ -268,14 +268,14 @@ export function useRideRequests() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/requests/${requestId}/cancel`, `/api/trasporti/requests/${requestId}/cancel`,
'POST', 'POST',
{ reason } { reason }
) as ApiResponse<RideRequest>; ) as ApiResponse<RideRequest>;
if (response.success && response.data?.data) { if (response.success && response.data) {
updateRequestInList(requestId, response.data?.data); updateRequestInList(requestId, response.data);
} }
return response; return response;

View File

@@ -116,14 +116,14 @@ export function useRides() {
if (filters.passingThrough) if (filters.passingThrough)
queryParams.append('passingThrough', filters.passingThrough); queryParams.append('passingThrough', filters.passingThrough);
const response = (await Api.SendReq( const response = (await Api.SendReqWithData(
`/api/trasporti/rides?${queryParams.toString()}`, `/api/trasporti/rides?${queryParams.toString()}`,
'GET' 'GET'
)) as PaginatedResponse<Ride>; )) as PaginatedResponse<Ride>;
if (response.success) { if (response.success) {
// ✅ Ensure response.data is always an array // ✅ Ensure response.data is always an array
const newRides = Array.isArray(response.data.data) ? response.data.data : []; const newRides = Array.isArray(response.data) ? response.data : [];
if (options.reset) { if (options.reset) {
rides.value = newRides; rides.value = newRides;
@@ -166,14 +166,14 @@ export function useRides() {
queryParams.append('page', pagination.page.toString()); queryParams.append('page', pagination.page.toString());
queryParams.append('limit', pagination.limit.toString()); queryParams.append('limit', pagination.limit.toString());
const response = (await Api.SendReq( const response = (await Api.SendReqWithData(
`/api/trasporti/rides/search?${queryParams.toString()}`, `/api/trasporti/rides/search?${queryParams.toString()}`,
'GET' 'GET'
)) as PaginatedResponse<Ride>; )) as PaginatedResponse<Ride>;
if (response.success) { if (response.success) {
// ✅ Ensure response.data is always an array // ✅ Ensure response.data is always an array
rides.value = Array.isArray(response.data.data) ? response.data.data : []; rides.value = Array.isArray(response.data) ? response.data : [];
if (response?.data.pagination) { if (response?.data.pagination) {
Object.assign(pagination, response?.data.pagination); Object.assign(pagination, response?.data.pagination);
@@ -198,13 +198,13 @@ export function useRides() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = (await Api.SendReq( const response = (await Api.SendReqWithData(
`/api/trasporti/rides/${rideId}`, `/api/trasporti/rides/${rideId}`,
'GET' 'GET'
)) as ApiResponse<Ride>; )) as ApiResponse<Ride>;
if (response.success && response.data?.data) { if (response.success && response.data) {
currentRide.value = response.data.data; currentRide.value = response.data;
} else { } else {
throw new Error('Viaggio non trovato'); throw new Error('Viaggio non trovato');
} }
@@ -226,16 +226,16 @@ export function useRides() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = (await Api.SendReq( const response = (await Api.SendReqWithData(
'/api/trasporti/rides', '/api/trasporti/rides',
'POST', 'POST',
rideData rideData
)) as ApiResponse<Ride>; )) as ApiResponse<Ride>;
if (response.success && response.data?.data) { if (response.success && response.data) {
// Aggiungi in testa alla lista // Aggiungi in testa alla lista
rides.value.unshift(response.data?.data); rides.value.unshift(response.data);
currentRide.value = response.data.data; currentRide.value = response.data;
} }
return response; return response;
@@ -255,20 +255,20 @@ export function useRides() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = (await Api.SendReq( const response = (await Api.SendReqWithData(
`/api/trasporti/rides/${rideId}`, `/api/trasporti/rides/${rideId}`,
'PUT', 'PUT',
updateData updateData
)) as ApiResponse<Ride>; )) as ApiResponse<Ride>;
if (response.success && response.data?.data) { if (response.success && response.data) {
// Aggiorna nella lista // Aggiorna nella lista
const index = rides.value.findIndex((r) => r._id === rideId); const index = rides.value.findIndex((r) => r._id === rideId);
if (index !== -1) { if (index !== -1) {
rides.value[index] = response.data.data; rides.value[index] = response.data;
} }
if (currentRide.value?._id === rideId) { if (currentRide.value?._id === rideId) {
currentRide.value = response.data.data; currentRide.value = response.data;
} }
} }
@@ -289,7 +289,7 @@ export function useRides() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = (await Api.SendReq(`/api/trasporti/rides/${rideId}`, 'DELETE', { const response = (await Api.SendReqWithData(`/api/trasporti/rides/${rideId}`, 'DELETE', {
reason, reason,
})) as ApiResponse<void>; })) as ApiResponse<void>;
@@ -318,18 +318,18 @@ export function useRides() {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = (await Api.SendReq( const response = (await Api.SendReqWithData(
`/api/trasporti/rides/${rideId}/complete`, `/api/trasporti/rides/${rideId}/complete`,
'POST' 'POST'
)) as ApiResponse<Ride>; )) as ApiResponse<Ride>;
if (response.success && response.data?.data) { if (response.success && response.data) {
const index = rides.value.findIndex((r) => r._id === rideId); const index = rides.value.findIndex((r) => r._id === rideId);
if (index !== -1) { if (index !== -1) {
rides.value[index] = response.data.data; rides.value[index] = response.data;
} }
if (currentRide.value?._id === rideId) { if (currentRide.value?._id === rideId) {
currentRide.value = response.data.data; currentRide.value = response.data;
} }
} }
@@ -361,15 +361,15 @@ export function useRides() {
queryParams.append('page', pagination.page.toString()); queryParams.append('page', pagination.page.toString());
queryParams.append('limit', pagination.limit.toString()); queryParams.append('limit', pagination.limit.toString());
const response = (await Api.SendReq( const response = (await Api.SendReqWithData(
`/api/trasporti/rides/my?${queryParams.toString()}`, `/api/trasporti/rides/my?${queryParams.toString()}`,
'GET' 'GET'
)) as MyRidesResponse; )) as MyRidesResponse;
if (response.success && response.data?.data) { if (response.success && response.data) {
myRides.all = response.data.data.all; myRides.all = response.data.all;
myRides.upcoming = response.data.data.upcoming; myRides.upcoming = response.data.upcoming;
myRides.past = response.data.data.past; myRides.past = response.data.past;
Object.assign(pagination, response?.data.pagination); Object.assign(pagination, response?.data.pagination);
} }
@@ -387,13 +387,13 @@ export function useRides() {
*/ */
const fetchStats = async () => { const fetchStats = async () => {
try { try {
const response = (await Api.SendReq( const response = (await Api.SendReqWithData(
'/api/trasporti/rides/stats', '/api/trasporti/rides/stats',
'GET' 'GET'
)) as ApiResponse<RidesStatsResponse>; )) as ApiResponse<RidesStatsResponse>;
if (response.success && response.data) { if (response.success && response.data) {
stats.value = response.data.data; stats.value = response.data;
} }
return response; return response;

View File

@@ -1,142 +1,88 @@
// ChatListPage.scss // ChatListPage.scss
.chat-list-page { .chat-list-page {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
min-height: 100vh;
// Header
&__header { &__header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 16px;
color: white; background: white;
padding: 24px 20px; border-bottom: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 0 0 24px 24px;
margin-bottom: 8px;
} }
&__title-section { &__title-section {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 12px;
margin-bottom: 20px;
} }
&__icon { &__icon {
font-size: 40px; font-size: 32px;
opacity: 0.9;
} }
&__title { &__title {
font-size: 28px;
font-weight: 700;
margin: 0; margin: 0;
letter-spacing: -0.5px; font-size: 24px;
font-weight: 600;
} }
&__subtitle { &__subtitle {
margin: 4px 0 0; margin: 0;
opacity: 0.85; color: $grey-6;
font-size: 14px; font-size: 14px;
} }
&__search { &__search {
:deep(.q-field__control) { margin-top: 12px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
&::before {
border-color: transparent;
}
}
:deep(.q-field__native),
:deep(.q-field__prefix),
:deep(.q-field__suffix) {
color: white;
}
:deep(.q-field__native::placeholder) {
color: rgba(255, 255, 255, 0.7);
}
} }
// Tabs
&__tabs { &__tabs {
background: white; background: white;
margin: 0 12px; border-bottom: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
:deep(.q-tab) {
text-transform: none;
font-weight: 500;
}
} }
&__tab-content { &__tab-content {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 4px;
} }
// Content
&__content { &__content {
padding: 16px 12px; min-height: calc(100vh - 200px);
} }
&__loading { &__loading,
&__empty {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 60px 20px; padding: 48px 24px;
color: #666;
p {
margin-top: 16px;
}
}
&__empty {
text-align: center; text-align: center;
padding: 60px 24px;
h3 { h3 {
font-size: 20px; margin: 16px 0 8px;
font-weight: 600; font-size: 18px;
color: #333; font-weight: 500;
margin: 20px 0 8px;
} }
p { p {
color: #666; margin: 0;
margin-bottom: 24px; color: $grey-6;
} }
} }
// List
&__list { &__list {
background: transparent; background: white;
} }
&__item { &__item {
position: relative;
background: white; background: white;
border-radius: 16px; transition: background-color 0.3s;
margin-bottom: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: all 0.2s ease;
&:hover { &:active {
transform: translateY(-2px); background: rgba(0, 0, 0, 0.04);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
} }
&--unread { &--unread {
border-left: 4px solid var(--q-primary); background: rgba($primary, 0.02);
background: linear-gradient(90deg, rgba(102, 126, 234, 0.05) 0%, white 100%);
}
:deep(.q-item) {
padding: 16px;
} }
} }
@@ -145,26 +91,19 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0 24px; height: 100%;
padding: 0 20px;
color: white; color: white;
font-size: 12px;
font-weight: 500;
.q-icon {
font-size: 24px;
margin-bottom: 4px;
}
&--archive { &--archive {
background: #2196f3; background: $orange;
} }
&--delete { &--delete {
background: #f44336; background: $negative;
} }
} }
// Avatar
&__avatar-wrapper { &__avatar-wrapper {
position: relative; position: relative;
} }
@@ -175,125 +114,80 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: $primary;
color: white; color: white;
font-weight: 600; font-weight: 600;
font-size: 18px; font-size: 20px;
border-radius: 50%;
} }
&__online-dot { &__online-dot {
position: absolute; position: absolute;
bottom: 2px; bottom: 2px;
right: 2px; right: 2px;
width: 14px; width: 12px;
height: 14px; height: 12px;
background: #4caf50; background: $positive;
border: 3px solid white; border: 2px solid white;
border-radius: 50%; border-radius: 50%;
} }
&__ride-badge { &__ride-badge {
padding: 3px; .q-badge__content {
min-height: 18px; padding: 2px 4px;
min-width: 18px; }
} }
// Content
&__name { &__name {
font-weight: 600;
font-size: 16px; font-size: 16px;
color: #1a1a2e; margin-bottom: 2px;
} }
&__ride-info { &__ride-info {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; color: $primary;
color: #667eea; font-size: 12px;
font-weight: 500; margin-bottom: 2px;
margin-top: 2px;
} }
&__last-message { &__last-message {
color: #666;
margin-top: 6px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; color: $grey-7;
font-size: 14px;
&--unread { &--unread {
font-weight: 600; color: $grey-9;
color: #333; font-weight: 500;
} }
} }
// Meta
&__meta { &__meta {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
gap: 8px; gap: 4px;
} }
&__time { &__time {
font-size: 12px; font-size: 12px;
color: #999; color: $grey-6;
white-space: nowrap;
} }
&__unread-badge { &__unread-badge {
font-size: 11px;
font-weight: 600; font-weight: 600;
animation: pulse 2s infinite;
} }
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
// Load more
&__load-more { &__load-more {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 16px; padding: 16px;
} }
// Search dialog
&__search-dialog { &__search-dialog {
width: 100%; max-height: 80vh;
max-width: 500px; overflow: auto;
border-radius: 20px;
margin-top: 60px;
}
}
// Dark mode
.body--dark {
.chat-list-page {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
&__tabs {
background: #1e1e30;
}
&__item {
background: #1e1e30;
&--unread {
background: linear-gradient(90deg, rgba(102, 126, 234, 0.1) 0%, #1e1e30 100%);
}
}
&__name {
color: #fff;
}
&__last-message--unread {
color: #e0e0e0;
}
} }
} }

View File

@@ -1,11 +1,11 @@
// ChatListPage.ts // ChatListPage.ts
import { defineComponent, ref, computed, onMounted, onUnmounted } from 'vue'; import { defineComponent, ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useChat } from '../composables/useChat'; import { useChat } from '../composables/useChat';
import { useAuth } from '@/composables/useAuth'; // Il tuo composable auth esistente import { useAuth } from '../composables/useAuth';
import type { Chat, User, Message } from '../types/trasporti.types'; import { Api } from '@api';
import { debounce } from 'quasar'; import type { Chat, User, Message } from '../types';
export default defineComponent({ export default defineComponent({
name: 'ChatListPage', name: 'ChatListPage',
@@ -17,12 +17,12 @@ export default defineComponent({
const { const {
chats, chats,
loading, loading,
loadChats, fetchChats,
archiveChat, getOrCreateDirectChat,
toggleMuteChat,
deleteChat, deleteChat,
createChat, onlineUsers,
searchUsers: searchUsersApi, totalUnreadCount
onlineUsers
} = useChat(); } = useChat();
// State // State
@@ -31,18 +31,19 @@ export default defineComponent({
const loadingMore = ref(false); const loadingMore = ref(false);
const hasMore = ref(true); const hasMore = ref(true);
const page = ref(1); const page = ref(1);
// ✅ User search
const showUserSearch = ref(false); const showUserSearch = ref(false);
const showGroupCreate = ref(false);
const userSearchQuery = ref(''); const userSearchQuery = ref('');
const searchedUsers = ref<User[]>([]); const searchedUsers = ref<User[]>([]);
const searchingUsers = ref(false); const searchingUsers = ref(false);
// ✅ Group chat
const showGroupCreate = ref(false);
// Computed // Computed
const currentUserId = computed(() => currentUser.value?._id); const currentUserId = computed(() => currentUser.value?._id);
const unreadCount = computed(() => totalUnreadCount.value);
const unreadCount = computed(() => {
return chats.value.reduce((total, chat) => total + (chat.unreadCount || 0), 0);
});
const filteredChats = computed(() => { const filteredChats = computed(() => {
let result = [...chats.value]; let result = [...chats.value];
@@ -50,7 +51,7 @@ export default defineComponent({
// Filter by tab // Filter by tab
switch (activeTab.value) { switch (activeTab.value) {
case 'unread': case 'unread':
result = result.filter(chat => chat.unreadCount > 0); result = result.filter(chat => (chat.unreadCount || 0) > 0);
break; break;
case 'rides': case 'rides':
result = result.filter(chat => chat.rideId); result = result.filter(chat => chat.rideId);
@@ -69,23 +70,38 @@ export default defineComponent({
const otherUser = getOtherParticipant(chat); const otherUser = getOtherParticipant(chat);
const fullName = `${otherUser?.name || ''} ${otherUser?.surname || ''}`.toLowerCase(); const fullName = `${otherUser?.name || ''} ${otherUser?.surname || ''}`.toLowerCase();
const username = otherUser?.username?.toLowerCase() || ''; const username = otherUser?.username?.toLowerCase() || '';
const rideInfo = chat.rideInfo
? `${chat.rideInfo.departure} ${chat.rideInfo.destination}`.toLowerCase() let rideInfo = '';
: ''; if (chat.rideId) {
const rideData = typeof chat.rideId === 'object' ? chat.rideId : null;
if (rideData) {
const departure = typeof rideData.departure === 'string'
? rideData.departure
: rideData.departure?.city || '';
const destination = typeof rideData.destination === 'string'
? rideData.destination
: rideData.destination?.city || '';
rideInfo = `${departure} ${destination}`.toLowerCase();
}
}
const lastMessageText = chat.lastMessage?.text?.toLowerCase() || '';
return fullName.includes(query) || return fullName.includes(query) ||
username.includes(query) || username.includes(query) ||
rideInfo.includes(query); rideInfo.includes(query) ||
lastMessageText.includes(query);
}); });
} }
// Sort: pinned first, then by last message date // Sort by last message date
result.sort((a, b) => { result.sort((a, b) => {
// Pinned chats first
if (a.pinned && !b.pinned) return -1; if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1; if (!a.pinned && b.pinned) return 1;
const dateA = new Date(a.lastMessage?.createdAt || a.updatedAt).getTime(); const dateA = new Date(a.lastMessage?.timestamp || a.updatedAt).getTime();
const dateB = new Date(b.lastMessage?.createdAt || b.updatedAt).getTime(); const dateB = new Date(b.lastMessage?.timestamp || b.updatedAt).getTime();
return dateB - dateA; return dateB - dateA;
}); });
@@ -97,7 +113,7 @@ export default defineComponent({
switch (activeTab.value) { switch (activeTab.value) {
case 'unread': return 'mark_email_read'; case 'unread': return 'mark_email_read';
case 'rides': return 'no_transfer'; case 'rides': return 'no_transfer';
case 'archived': return 'inventory_2'; case 'archived': return 'unarchive';
default: return 'forum'; default: return 'forum';
} }
}); });
@@ -117,21 +133,25 @@ export default defineComponent({
switch (activeTab.value) { switch (activeTab.value) {
case 'unread': return 'Non hai messaggi da leggere'; case 'unread': return 'Non hai messaggi da leggere';
case 'rides': return 'Le chat relative ai viaggi appariranno qui'; case 'rides': return 'Le chat relative ai viaggi appariranno qui';
case 'archived': return 'Le chat archiviate appariranno qui'; case 'archived': return 'Le conversazioni archiviate appariranno qui';
default: return 'Inizia a cercare viaggi per connetterti con altri utenti'; default: return 'Inizia a cercare viaggi per connetterti con altri utenti';
} }
}); });
// Methods // Methods
const getOtherParticipant = (chat: Chat): User | undefined => { const getOtherParticipant = (chat: Chat): User | undefined => {
return chat.participants?.find(p => p._id !== currentUserId.value); if (!chat.participants || chat.participants.length === 0) return undefined;
return chat.participants.find(p => {
const pId = typeof p === 'string' ? p : p._id;
return pId !== currentUserId.value;
}) as User | undefined;
}; };
const getInitials = (user?: User): string => { const getInitials = (user?: User): string => {
if (!user) return '?'; if (!user) return '?';
const name = user.name || ''; const name = user.name || '';
const surname = user.surname || ''; const surname = user.surname || '';
return `${name.charAt(0)}${surname.charAt(0)}`.toUpperCase(); return `${name.charAt(0)}${surname.charAt(0)}`.toUpperCase() || '?';
}; };
const isOnline = (userId?: string): boolean => { const isOnline = (userId?: string): boolean => {
@@ -164,22 +184,37 @@ export default defineComponent({
} }
}; };
const getMessagePreview = (message?: Message): string => { // ✅ Fixed: Riceve lastMessage invece di chat
if (!message) return 'Nessun messaggio'; const getMessagePreview = (lastMessage?: Message | null): string => {
if (!lastMessage) return 'Nessun messaggio';
if (message.type === 'image') return '📷 Foto'; const msgType = lastMessage.type || 'text';
if (message.type === 'location') return '📍 Posizione';
if (message.type === 'ride_request') return '🚗 Richiesta passaggio';
if (message.type === 'ride_accepted') return '✅ Passaggio accettato';
if (message.type === 'ride_rejected') return '❌ Passaggio rifiutato';
return message.content || ''; if (msgType === 'image') return '📷 Foto';
if (msgType === 'location') return '📍 Posizione';
if (msgType === 'ride_request') return '🚗 Richiesta passaggio';
if (msgType === 'ride_accepted') return '✅ Passaggio accettato';
if (msgType === 'ride_rejected') return '❌ Passaggio rifiutato';
return lastMessage.text || 'Messaggio';
}; };
const getMessageStatusIcon = (message?: Message): string => { // ✅ Fixed: Riceve lastMessage invece di chat
if (!message) return ''; const getMessageStatusIcon = (lastMessage?: Message | null): string => {
if (message.read) return 'done_all'; if (!lastMessage) return '';
if (message.delivered) return 'done_all';
const senderId = typeof lastMessage.senderId === 'string'
? lastMessage.senderId
: lastMessage.senderId?._id;
if (senderId !== currentUserId.value) return '';
// Check read status
if (lastMessage.readBy && Array.isArray(lastMessage.readBy)) {
const allRead = lastMessage.readBy.length > 1; // > 1 perché include il mittente
return allRead ? 'done_all' : 'done';
}
return 'done'; return 'done';
}; };
@@ -187,14 +222,36 @@ export default defineComponent({
router.push(`/trasporti/chat/${chat._id}`); router.push(`/trasporti/chat/${chat._id}`);
}; };
// ✅ Added: Mute chat
const onMuteChat = async (chat: Chat) => {
try {
const isMuted = chat.mutedBy?.includes(currentUserId.value as any) || false;
await toggleMuteChat(chat._id, !isMuted);
$q.notify({
type: 'info',
message: isMuted ? 'Notifiche attivate' : 'Notifiche silenziate',
icon: isMuted ? 'notifications' : 'notifications_off'
});
await fetchChats();
} catch (error) {
$q.notify({
type: 'negative',
message: 'Errore nell\'aggiornamento'
});
}
};
// ✅ Added: Archive chat
const onArchiveChat = async (chat: Chat) => { const onArchiveChat = async (chat: Chat) => {
try { try {
await archiveChat(chat._id, !chat.archived); // TODO: Implementa nel backend
$q.notify({ $q.notify({
type: 'positive', type: 'positive',
message: chat.archived ? 'Chat ripristinata' : 'Chat archiviata', message: 'Conversazione archiviata'
icon: 'archive'
}); });
await fetchChats();
} catch (error) { } catch (error) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
@@ -203,20 +260,28 @@ export default defineComponent({
} }
}; };
// ✅ Fixed: Delete chat
const onDeleteChat = async (chat: Chat) => { const onDeleteChat = async (chat: Chat) => {
$q.dialog({ $q.dialog({
title: 'Elimina conversazione', title: 'Elimina conversazione',
message: 'Sei sicuro di voler eliminare questa conversazione? L\'azione non è reversibile.', message: 'Sei sicuro di voler eliminare questa conversazione?',
cancel: true, cancel: {
label: 'Annulla',
flat: true
},
ok: {
label: 'Elimina',
color: 'negative'
},
persistent: true persistent: true
}).onOk(async () => { }).onOk(async () => {
try { try {
await deleteChat(chat._id); await deleteChat(chat._id);
$q.notify({ $q.notify({
type: 'positive', type: 'positive',
message: 'Conversazione eliminata', message: 'Conversazione eliminata'
icon: 'delete'
}); });
await fetchChats();
} catch (error) { } catch (error) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
@@ -226,38 +291,45 @@ export default defineComponent({
}); });
}; };
const loadMore = async () => { // ✅ Added: Search users
loadingMore.value = true; const searchUsers = async () => {
try { if (!userSearchQuery.value || userSearchQuery.value.length < 2) {
page.value++;
const newChats = await loadChats({ page: page.value, limit: 20 });
if (newChats.length < 20) {
hasMore.value = false;
}
} finally {
loadingMore.value = false;
}
};
const searchUsers = debounce(async (query: string) => {
if (!query || query.length < 2) {
searchedUsers.value = []; searchedUsers.value = [];
return; return;
} }
searchingUsers.value = true; searchingUsers.value = true;
try { try {
searchedUsers.value = await searchUsersApi(query); const response = await Api.SendReq(
`/api/users/search?q=${encodeURIComponent(userSearchQuery.value)}`,
'GET'
);
if (response.success && response.data) {
// Escludi utente corrente
searchedUsers.value = response.data.filter(
(u: User) => u._id !== currentUserId.value
);
}
} catch (error) {
console.error('Error searching users:', error);
searchedUsers.value = [];
} finally { } finally {
searchingUsers.value = false; searchingUsers.value = false;
} }
}, 300); };
// ✅ Added: Start chat with user
const startChatWith = async (user: User) => { const startChatWith = async (user: User) => {
try { try {
const chat = await createChat([user._id]); const chat = await getOrCreateDirectChat(user._id);
showUserSearch.value = false; showUserSearch.value = false;
router.push(`/trasporti/chat/${chat._id}`); userSearchQuery.value = '';
searchedUsers.value = [];
if (chat) {
router.push(`/trasporti/chat/${chat._id}`);
}
} catch (error) { } catch (error) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
@@ -266,9 +338,29 @@ export default defineComponent({
} }
}; };
const loadMore = async () => {
if (!hasMore.value || loadingMore.value) return;
loadingMore.value = true;
try {
page.value++;
const result = await fetchChats(page.value, 20);
if (!result || result.data.length < 20) {
hasMore.value = false;
}
} finally {
loadingMore.value = false;
}
};
const startNewChat = () => {
showUserSearch.value = true;
};
// Lifecycle // Lifecycle
onMounted(() => { onMounted(async () => {
loadChats({ page: 1, limit: 20 }); await fetchChats(1, 20);
}); });
return { return {
@@ -300,9 +392,11 @@ export default defineComponent({
getMessagePreview, getMessagePreview,
getMessageStatusIcon, getMessageStatusIcon,
openChat, openChat,
onMuteChat,
onArchiveChat, onArchiveChat,
onDeleteChat, onDeleteChat,
loadMore, loadMore,
startNewChat,
searchUsers, searchUsers,
startChatWith startChatWith
}; };

View File

@@ -4,7 +4,7 @@
<!-- Header --> <!-- Header -->
<div class="chat-list-page__header"> <div class="chat-list-page__header">
<div class="chat-list-page__title-section"> <div class="chat-list-page__title-section">
<q-icon name="forum" class="chat-list-page__icon" /> <q-icon name="forum" class="chat-list-page__icon" size="32px" color="primary" />
<div> <div>
<h1 class="chat-list-page__title">Messaggi</h1> <h1 class="chat-list-page__title">Messaggi</h1>
<p class="chat-list-page__subtitle">Le tue conversazioni</p> <p class="chat-list-page__subtitle">Le tue conversazioni</p>
@@ -17,12 +17,12 @@
placeholder="Cerca conversazione..." placeholder="Cerca conversazione..."
outlined outlined
dense dense
class="chat-list-page__search" class="chat-list-page__search q-mt-md"
> >
<template #prepend> <template v-slot:prepend>
<q-icon name="search" /> <q-icon name="search" />
</template> </template>
<template v-if="searchQuery" #append> <template v-if="searchQuery" v-slot:append>
<q-icon name="close" class="cursor-pointer" @click="searchQuery = ''" /> <q-icon name="close" class="cursor-pointer" @click="searchQuery = ''" />
</template> </template>
</q-input> </q-input>
@@ -35,13 +35,19 @@
active-color="primary" active-color="primary"
indicator-color="primary" indicator-color="primary"
align="justify" align="justify"
dense
> >
<q-tab name="all" label="Tutte" icon="inbox" /> <q-tab name="all" label="Tutte" icon="inbox" />
<q-tab name="unread" icon="mark_email_unread"> <q-tab name="unread" icon="mark_email_unread">
<template #default> <template v-slot:default>
<div class="chat-list-page__tab-content"> <div class="chat-list-page__tab-content">
<span>Non lette</span> <span>Non lette</span>
<q-badge v-if="unreadCount > 0" color="negative" :label="unreadCount" /> <q-badge
v-if="unreadCount > 0"
color="negative"
:label="unreadCount > 99 ? '99+' : unreadCount"
class="q-ml-xs"
/>
</div> </div>
</template> </template>
</q-tab> </q-tab>
@@ -52,16 +58,16 @@
<!-- Content --> <!-- Content -->
<div class="chat-list-page__content"> <div class="chat-list-page__content">
<!-- Loading --> <!-- Loading -->
<div v-if="loading" class="chat-list-page__loading"> <div v-if="loading && filteredChats.length === 0" class="chat-list-page__loading">
<q-spinner-dots size="50px" color="primary" /> <q-spinner-dots size="50px" color="primary" />
<p>Caricamento conversazioni...</p> <p class="text-grey q-mt-md">Caricamento conversazioni...</p>
</div> </div>
<!-- Empty State --> <!-- Empty State -->
<div v-else-if="filteredChats.length === 0" class="chat-list-page__empty"> <div v-else-if="filteredChats.length === 0" class="chat-list-page__empty">
<q-icon :name="emptyStateIcon" size="80px" color="grey-4" /> <q-icon :name="emptyStateIcon" size="80px" color="grey-4" />
<h3>{{ emptyStateTitle }}</h3> <h3 class="q-mt-md q-mb-sm">{{ emptyStateTitle }}</h3>
<p>{{ emptyStateMessage }}</p> <p class="text-grey">{{ emptyStateMessage }}</p>
<q-btn <q-btn
v-if="activeTab === 'all' && !searchQuery" v-if="activeTab === 'all' && !searchQuery"
color="primary" color="primary"
@@ -69,6 +75,7 @@
label="Esplora viaggi" label="Esplora viaggi"
rounded rounded
unelevated unelevated
class="q-mt-md"
to="/trasporti" to="/trasporti"
/> />
</div> </div>
@@ -79,24 +86,27 @@
v-for="chat in filteredChats" v-for="chat in filteredChats"
:key="chat._id" :key="chat._id"
class="chat-list-page__item" class="chat-list-page__item"
:class="{ 'chat-list-page__item--unread': chat.unreadCount > 0 }" :class="{ 'chat-list-page__item--unread': (chat.unreadCount || 0) > 0 }"
@left="onArchiveChat(chat)" @left="(details) => { details.reset(); onArchiveChat(chat); }"
@right="onDeleteChat(chat)" @right="(details) => { details.reset(); onDeleteChat(chat); }"
> >
<template #left> <!-- Left Slide Action: Archive -->
<template v-slot:left>
<div class="chat-list-page__slide-action chat-list-page__slide-action--archive"> <div class="chat-list-page__slide-action chat-list-page__slide-action--archive">
<q-icon name="archive" /> <q-icon name="archive" size="24px" />
<span>Archivia</span> <span class="text-caption q-mt-xs">Archivia</span>
</div> </div>
</template> </template>
<template #right> <!-- Right Slide Action: Delete -->
<template v-slot:right>
<div class="chat-list-page__slide-action chat-list-page__slide-action--delete"> <div class="chat-list-page__slide-action chat-list-page__slide-action--delete">
<q-icon name="delete" /> <q-icon name="delete" size="24px" />
<span>Elimina</span> <span class="text-caption q-mt-xs">Elimina</span>
</div> </div>
</template> </template>
<!-- Chat Item -->
<q-item clickable @click="openChat(chat)"> <q-item clickable @click="openChat(chat)">
<!-- Avatar --> <!-- Avatar -->
<q-item-section avatar> <q-item-section avatar>
@@ -105,7 +115,7 @@
<img <img
v-if="getOtherParticipant(chat)?.profile?.img" v-if="getOtherParticipant(chat)?.profile?.img"
:src="getOtherParticipant(chat).profile.img" :src="getOtherParticipant(chat).profile.img"
:alt="getOtherParticipant(chat).name" :alt="getOtherParticipant(chat)?.name"
/> />
<div v-else class="chat-list-page__avatar-placeholder"> <div v-else class="chat-list-page__avatar-placeholder">
{{ getInitials(getOtherParticipant(chat)) }} {{ getInitials(getOtherParticipant(chat)) }}
@@ -120,27 +130,53 @@
<!-- Ride type badge --> <!-- Ride type badge -->
<q-badge <q-badge
v-if="chat.rideInfo" v-if="chat.rideId && typeof chat.rideId === 'object' && chat.rideId.type"
:color="chat.rideInfo.type === 'offer' ? 'positive' : 'negative'" :color="chat.rideId.type === 'offer' ? 'positive' : 'warning'"
floating floating
rounded rounded
class="chat-list-page__ride-badge" class="chat-list-page__ride-badge"
> >
<q-icon :name="chat.rideInfo.type === 'offer' ? 'directions_car' : 'hail'" size="12px" /> <q-icon
:name="chat.rideId.type === 'offer' ? 'directions_car' : 'hail'"
size="12px"
/>
</q-badge> </q-badge>
</div> </div>
</q-item-section> </q-item-section>
<!-- Content --> <!-- Content -->
<q-item-section> <q-item-section>
<!-- Name -->
<q-item-label class="chat-list-page__name"> <q-item-label class="chat-list-page__name">
{{ getOtherParticipant(chat)?.name }} {{ getOtherParticipant(chat)?.surname }} <span class="text-weight-medium">
{{ getOtherParticipant(chat)?.name || 'Utente' }}
{{ getOtherParticipant(chat)?.surname || '' }}
</span>
<q-icon
v-if="chat.pinned"
name="push_pin"
size="16px"
color="grey"
class="q-ml-xs"
/>
</q-item-label> </q-item-label>
<!-- Ride info --> <!-- Ride info -->
<q-item-label v-if="chat.rideInfo" caption class="chat-list-page__ride-info"> <q-item-label
<q-icon name="place" size="14px" /> v-if="chat.rideId && typeof chat.rideId === 'object'"
{{ chat.rideInfo.departure }} {{ chat.rideInfo.destination }} caption
class="chat-list-page__ride-info"
>
<q-icon name="place" size="14px" class="q-mr-xs" />
<template v-if="chat.rideId.departure && chat.rideId.destination">
{{ typeof chat.rideId.departure === 'string'
? chat.rideId.departure
: chat.rideId.departure.city }}
{{ typeof chat.rideId.destination === 'string'
? chat.rideId.destination
: chat.rideId.destination.city }}
</template>
</q-item-label> </q-item-label>
<!-- Last message --> <!-- Last message -->
@@ -148,38 +184,46 @@
caption caption
lines="1" lines="1"
class="chat-list-page__last-message" class="chat-list-page__last-message"
:class="{ 'chat-list-page__last-message--unread': chat.unreadCount > 0 }" :class="{
'chat-list-page__last-message--unread': (chat.unreadCount || 0) > 0,
'text-weight-medium': (chat.unreadCount || 0) > 0
}"
> >
<q-icon <q-icon
v-if="chat.lastMessage?.senderId === currentUserId" v-if="chat.lastMessage && chat.lastMessage.senderId === currentUserId"
:name="getMessageStatusIcon(chat.lastMessage)" :name="getMessageStatusIcon(chat.lastMessage)"
size="14px" size="14px"
:color="chat.lastMessage?.read ? 'primary' : 'grey'" :color="chat.lastMessage.readBy && chat.lastMessage.readBy.length > 1 ? 'primary' : 'grey'"
class="q-mr-xs"
/> />
{{ getMessagePreview(chat.lastMessage) }} {{ getMessagePreview(chat.lastMessage) }}
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
<!-- Right side --> <!-- Right side: Time & Badge -->
<q-item-section side> <q-item-section side top>
<div class="chat-list-page__meta"> <div class="chat-list-page__meta">
<!-- Time -->
<q-item-label caption class="chat-list-page__time"> <q-item-label caption class="chat-list-page__time">
{{ formatTime(chat.lastMessage?.createdAt || chat.updatedAt) }} {{ formatTime(chat.lastMessage?.timestamp || chat.updatedAt) }}
</q-item-label> </q-item-label>
<!-- Unread badge -->
<q-badge <q-badge
v-if="chat.unreadCount > 0" v-if="(chat.unreadCount || 0) > 0"
color="primary" color="primary"
:label="chat.unreadCount > 99 ? '99+' : chat.unreadCount" :label="chat.unreadCount > 99 ? '99+' : chat.unreadCount"
rounded rounded
class="chat-list-page__unread-badge" class="chat-list-page__unread-badge q-mt-xs"
/> />
<!-- Muted icon -->
<q-icon <q-icon
v-else-if="chat.pinned" v-else-if="chat.mutedBy && chat.mutedBy.includes(currentUserId)"
name="push_pin" name="notifications_off"
size="18px" size="18px"
color="grey" color="grey"
class="q-mt-xs"
/> />
</div> </div>
</q-item-section> </q-item-section>
@@ -188,11 +232,12 @@
</q-list> </q-list>
<!-- Load More --> <!-- Load More -->
<div v-if="hasMore && !loading" class="chat-list-page__load-more"> <div v-if="hasMore && filteredChats.length > 0" class="chat-list-page__load-more">
<q-btn <q-btn
flat flat
color="primary" color="primary"
label="Carica altre conversazioni" label="Carica altre conversazioni"
icon="expand_more"
:loading="loadingMore" :loading="loadingMore"
@click="loadMore" @click="loadMore"
/> />
@@ -211,12 +256,16 @@
color="secondary" color="secondary"
icon="person_search" icon="person_search"
label="Cerca utente" label="Cerca utente"
external-label
label-position="left"
@click="showUserSearch = true" @click="showUserSearch = true"
/> />
<q-fab-action <q-fab-action
color="accent" color="accent"
icon="group" icon="group"
label="Nuovo gruppo" label="Nuovo gruppo"
external-label
label-position="left"
@click="showGroupCreate = true" @click="showGroupCreate = true"
/> />
</q-fab> </q-fab>
@@ -224,9 +273,11 @@
<!-- User Search Dialog --> <!-- User Search Dialog -->
<q-dialog v-model="showUserSearch" position="top"> <q-dialog v-model="showUserSearch" position="top">
<q-card class="chat-list-page__search-dialog"> <q-card class="chat-list-page__search-dialog" style="width: 100%; max-width: 500px;">
<q-card-section> <q-card-section class="row items-center q-pb-none">
<div class="text-h6">Nuova conversazione</div> <div class="text-h6">Nuova conversazione</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
@@ -234,40 +285,91 @@
v-model="userSearchQuery" v-model="userSearchQuery"
placeholder="Cerca per nome o username..." placeholder="Cerca per nome o username..."
outlined outlined
dense
autofocus autofocus
@update:model-value="searchUsers" @update:model-value="searchUsers"
> >
<template #prepend> <template v-slot:prepend>
<q-icon name="search" /> <q-icon name="search" />
</template> </template>
<template v-if="userSearchQuery" v-slot:append>
<q-icon
name="close"
class="cursor-pointer"
@click="userSearchQuery = ''; searchedUsers = []"
/>
</template>
</q-input> </q-input>
<q-list v-if="searchedUsers.length > 0" class="q-mt-md"> <!-- Searching spinner -->
<div v-if="searchingUsers" class="text-center q-pa-md">
<q-spinner color="primary" size="40px" />
</div>
<!-- Search results -->
<q-list v-else-if="searchedUsers.length > 0" class="q-mt-md">
<q-item <q-item
v-for="user in searchedUsers" v-for="user in searchedUsers"
:key="user._id" :key="user._id"
clickable clickable
v-ripple
@click="startChatWith(user)" @click="startChatWith(user)"
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar> <q-avatar>
<img v-if="user.profile?.img" :src="user.profile.img" /> <img v-if="user.profile?.img" :src="user.profile.img" />
<span v-else>{{ getInitials(user) }}</span> <div v-else class="chat-list-page__avatar-placeholder">
{{ getInitials(user) }}
</div>
</q-avatar> </q-avatar>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label>{{ user.name }} {{ user.surname }}</q-item-label> <q-item-label>{{ user.name }} {{ user.surname }}</q-item-label>
<q-item-label caption>@{{ user.username }}</q-item-label> <q-item-label caption>@{{ user.username }}</q-item-label>
</q-item-section> </q-item-section>
<q-item-section side>
<q-icon name="chevron_right" color="grey" />
</q-item-section>
</q-item> </q-item>
</q-list> </q-list>
<div v-else-if="userSearchQuery && !searchingUsers" class="text-center q-pa-md text-grey"> <!-- No results -->
Nessun utente trovato <div
v-else-if="userSearchQuery && userSearchQuery.length >= 2 && !searchingUsers"
class="text-center q-pa-md text-grey"
>
<q-icon name="person_off" size="48px" color="grey-4" />
<p class="q-mt-md">Nessun utente trovato</p>
</div>
<!-- Hint -->
<div
v-else-if="!userSearchQuery || userSearchQuery.length < 2"
class="text-center q-pa-md text-grey"
>
<q-icon name="search" size="48px" color="grey-4" />
<p class="q-mt-md">Digita almeno 2 caratteri per cercare</p>
</div> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-dialog> </q-dialog>
<!-- Group Create Dialog (placeholder) -->
<q-dialog v-model="showGroupCreate">
<q-card style="min-width: 350px;">
<q-card-section>
<div class="text-h6">Crea gruppo</div>
</q-card-section>
<q-card-section class="q-pt-none">
<p class="text-grey">Funzionalità in arrivo...</p>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Chiudi" color="primary" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</q-page> </q-page>
</template> </template>

View File

@@ -1,71 +1,90 @@
// ChatPage.scss
.chat-page { .chat-page {
height: 90vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; background: #e5ddd5;
background: #f0f2f5; // Padding per la tab fissa in basso (40px)
padding-bottom: 30px;
// Header // Header locale (NON q-header)
&__header { &__header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); position: sticky;
top: 0;
z-index: 100;
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
:deep(.q-toolbar) { &__toolbar {
min-height: 64px; display: flex;
align-items: center;
padding: 8px;
min-height: 56px;
gap: 8px;
}
&__toolbar-title {
flex: 1;
min-width: 0;
margin-left: 8px;
}
&__user-name {
font-weight: 500;
font-size: 16px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
text-decoration: underline;
} }
} }
&__user-status {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #666;
margin-top: 2px;
}
&__typing {
color: $primary;
font-weight: 500;
}
&__typing-dots {
color: $primary;
animation: blink 1.4s infinite;
}
&__avatar-placeholder { &__avatar-placeholder {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(255, 255, 255, 0.2); background: $primary;
color: white; color: white;
font-weight: 600; font-weight: 600;
border-radius: 50%;
}
&__user-name {
font-weight: 600;
font-size: 16px;
cursor: pointer;
}
&__user-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
opacity: 0.9;
}
&__typing {
font-style: italic;
}
&__typing-dots {
animation: blink 1.4s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
} }
// Ride Banner // Ride Banner
&__ride-banner { &__ride-banner {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
padding: 10px 16px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 12px 16px;
background: #fff3cd;
border-top: 1px solid #ffc107;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
&:hover { &:hover {
background: rgba(255, 255, 255, 0.25); background: #ffe69c;
} }
} }
@@ -73,201 +92,205 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
flex: 1;
min-width: 0;
} }
&__ride-banner-text { &__ride-banner-text {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px;
min-width: 0;
} }
&__ride-banner-route { &__ride-banner-route {
font-weight: 600; font-weight: 500;
font-size: 14px; font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
&__ride-banner-date { &__ride-banner-date {
font-size: 12px; font-size: 12px;
opacity: 0.85; color: #666;
} }
// Messages // Messages Area
&__messages { &__messages {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 16px; padding: 16px;
padding-bottom: 80px; display: flex;
position: relative; flex-direction: column;
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23667eea' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); gap: 8px;
// Calcola altezza considerando header + input + tab
// Si adatta automaticamente grazie a flex: 1
} }
&__load-more { &__load-more {
display: flex; text-align: center;
justify-content: center;
padding: 8px; padding: 8px;
} }
&__date-separator { &__date-separator {
display: flex; text-align: center;
justify-content: center;
margin: 16px 0; margin: 16px 0;
span { span {
background: rgba(102, 126, 234, 0.1); display: inline-block;
color: #667eea; padding: 4px 12px;
padding: 6px 16px; background: rgba(255, 255, 255, 0.9);
border-radius: 20px; border-radius: 12px;
font-size: 12px; font-size: 12px;
font-weight: 500; color: #666;
text-transform: capitalize; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
} }
} }
// Typing indicator // Typing Indicator
&__typing-indicator { &__typing-indicator {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
gap: 8px; gap: 8px;
margin-top: 8px; padding: 8px 0;
} }
&__typing-bubble { &__typing-bubble {
background: white; background: white;
border-radius: 18px;
padding: 12px 16px; padding: 12px 16px;
border-radius: 18px;
display: flex; display: flex;
gap: 4px; gap: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
} }
&__typing-dot { &__typing-dot {
width: 8px; width: 8px;
height: 8px; height: 8px;
background: #bbb;
border-radius: 50%; border-radius: 50%;
animation: typingBounce 1.4s infinite ease-in-out; background: #999;
animation: typing 1.4s infinite;
&:nth-child(1) { animation-delay: 0s; } &:nth-child(2) {
&:nth-child(2) { animation-delay: 0.2s; } animation-delay: 0.2s;
&:nth-child(3) { animation-delay: 0.4s; } }
&:nth-child(3) {
animation-delay: 0.4s;
}
} }
@keyframes typingBounce { // Scroll Button
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-6px); }
}
// Scroll button
&__scroll-btn { &__scroll-btn {
position: fixed; position: fixed;
bottom: 90px; // Posiziona sopra l'input area e la tab fissa
bottom: calc(70px + 40px); // input (70px) + tab (40px)
right: 20px; right: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 10; z-index: 50;
} }
// Reply preview // Reply Preview
&__reply-preview { &__reply-preview {
background: white; background: #f0f0f0;
border-top: 1px solid #e0e0e0;
padding: 12px 16px; padding: 12px 16px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
border-top: 1px solid #ddd;
gap: 12px;
} }
&__reply-content { &__reply-content {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
color: #667eea; flex: 1;
min-width: 0;
> div {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
} }
&__reply-author { &__reply-author {
font-weight: 600; font-weight: 500;
font-size: 13px; font-size: 13px;
display: block; color: $primary;
} }
&__reply-text { &__reply-text {
font-size: 13px; font-size: 13px;
color: #666; color: #666;
display: block;
max-width: 250px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
// Input area // Input Area
&__input-area { &__input-area {
position: fixed; position: sticky;
bottom: 0; bottom: 0;
left: 0;
right: 0;
background: white; background: white;
padding: 12px 16px; padding: 12px 16px;
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
gap: 8px; gap: 8px;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
z-index: 100; z-index: 10;
} }
&__input-wrapper { &__input-wrapper {
flex: 1; flex: 1;
min-width: 0;
} }
&__input { &__input {
:deep(.q-field__control) { .q-field__control {
border-radius: 24px; border-radius: 20px;
background: #f5f5f5;
min-height: 44px;
&::before {
border-color: transparent;
}
}
:deep(.q-field__native) {
padding: 8px 0;
} }
} }
// Emoji picker // Emoji Picker
&__emoji-picker { &__emoji-picker {
background: white; background: white;
padding: 12px; padding: 12px;
border-radius: 12px; border-radius: 8px;
display: grid; display: grid;
grid-template-columns: repeat(6, 1fr); grid-template-columns: repeat(6, 1fr);
gap: 8px; gap: 8px;
max-width: 280px; max-width: 300px;
} }
&__emoji { &__emoji {
font-size: 24px; font-size: 24px;
padding: 8px;
cursor: pointer; cursor: pointer;
border-radius: 8px; padding: 4px;
text-align: center; border-radius: 4px;
transition: background 0.2s; transition: background 0.2s;
text-align: center;
&:hover { &:hover {
background: #f0f0f0; background: #f0f0f0;
} }
} }
// Attach menu // Attachment Menu
&__attach-menu { &__attach-menu {
border-radius: 20px 20px 0 0; width: 100%;
padding: 24px; max-width: 500px;
margin: 0 auto;
} }
&__attach-grid { &__attach-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 20px; gap: 16px;
text-align: center; padding: 24px;
} }
&__attach-item { &__attach-item {
@@ -277,7 +300,7 @@
gap: 8px; gap: 8px;
cursor: pointer; cursor: pointer;
padding: 12px; padding: 12px;
border-radius: 12px; border-radius: 8px;
transition: background 0.2s; transition: background 0.2s;
&:hover { &:hover {
@@ -286,128 +309,98 @@
span { span {
font-size: 12px; font-size: 12px;
color: #666; text-align: center;
}
.q-avatar {
width: 56px;
height: 56px;
} }
} }
// Profile card // Profile Card
&__profile-card { &__profile-card {
width: 320px; width: 100%;
max-width: 90vw; max-width: 400px;
height: 100%; height: 100%;
border-radius: 0;
} }
&__profile-header { &__profile-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex;
color: white; flex-direction: column;
text-align: center; align-items: center;
padding: 24px; gap: 12px;
position: relative; padding-top: 24px;
.q-btn { .q-btn {
position: absolute; align-self: flex-end;
top: 12px;
left: 12px;
color: white;
}
.q-avatar {
margin-bottom: 16px;
border: 4px solid rgba(255, 255, 255, 0.3);
span {
background: rgba(255, 255, 255, 0.2);
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
} }
h4 { h4 {
margin: 0 0 4px; margin: 0;
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
} }
p { p {
margin: 0; margin: 0;
opacity: 0.85; color: #666;
font-size: 14px;
} }
} }
&__profile-actions { &__profile-actions {
display: flex; display: flex;
justify-content: center; gap: 12px;
gap: 16px; margin-top: 12px;
margin-top: 20px; }
.q-btn { // Animations
background: rgba(255, 255, 255, 0.2); @keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
} }
30% {
transform: translateY(-8px);
}
}
@keyframes blink {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
// Transitions
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 0.3s, opacity 0.3s;
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(100%);
opacity: 0;
} }
} }
// Transitions // Responsive
.fade-enter-active, @media (max-width: 600px) {
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.3s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(100%);
opacity: 0;
}
// Dark mode
.body--dark {
.chat-page { .chat-page {
background: #121212; &__user-name {
font-size: 15px;
&__messages {
background-color: #1a1a2e;
} }
&__date-separator span { &__attach-grid {
background: rgba(102, 126, 234, 0.2); grid-template-columns: repeat(4, 1fr);
}
&__typing-bubble {
background: #2d2d44;
}
&__input-area {
background: #1e1e30;
}
&__input {
:deep(.q-field__control) {
background: #2d2d44;
}
}
&__reply-preview {
background: #1e1e30;
border-color: #333;
} }
} }
} }

View File

@@ -1,12 +1,20 @@
// ChatPage.ts // ChatPage.ts
import { defineComponent, ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'; import {
defineComponent,
ref,
computed,
onMounted,
onUnmounted,
nextTick,
watch,
} from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useChat } from '../composables/useChat'; import { useChat } from '../composables/useChat';
import { useRealtimeChat } from '../composables/useRealtimeChat'; import { useRealtimeChat } from '../composables/useRealtimeChat';
import { useAuth } from '@/composables/useAuth'; import { useAuth } from '../composables/useAuth';
import MessageBubble from '../components/chat/MessageBubble.vue'; import MessageBubble from '../components/chat/MessageBubble.vue';
import type { Message, User, RideInfo } from '../types/trasporti.types'; import type { Message, RideInfo } from '../types/trasporti.types';
import { debounce } from 'quasar'; import { debounce } from 'quasar';
interface MessageGroup { interface MessageGroup {
@@ -18,7 +26,7 @@ export default defineComponent({
name: 'ChatPage', name: 'ChatPage',
components: { components: {
MessageBubble MessageBubble,
}, },
setup() { setup() {
@@ -38,14 +46,12 @@ export default defineComponent({
deleteMessage: deleteMsg, deleteMessage: deleteMsg,
onlineUsers, onlineUsers,
typingUsers, typingUsers,
toggleMuteChat toggleMuteChat,
startPolling, // AGGIUNGI
stopPolling, // AGGIUNGI
} = useChat(); } = useChat();
const { const { subscribeToChat, unsubscribeFromChat, sendTyping } = useRealtimeChat();
subscribeToChat,
unsubscribeFromChat,
sendTyping
} = useRealtimeChat();
// Refs // Refs
const messagesContainer = ref<HTMLElement>(); const messagesContainer = ref<HTMLElement>();
@@ -66,15 +72,28 @@ export default defineComponent({
const isMuted = ref(false); const isMuted = ref(false);
const lastSeen = ref<Date | null>(null); const lastSeen = ref<Date | null>(null);
const commonEmojis = ['😊', '😂', '❤️', '👍', '🙏', '😍', '🎉', '🚗', '📍', '✅', '❌', '⏰']; const commonEmojis = [
'😊',
'😂',
'❤️',
'👍',
'🙏',
'😍',
'🎉',
'🚗',
'📍',
'✅',
'❌',
'⏰',
];
// Computed // Computed
const chatId = computed(() => route.params.id as string); const chatId = computed(() => route.params.id as string);
const currentUserId = computed(() => currentUser.value?._id); const currentUserId = computed(() => currentUser.value?._id);
const otherUser = computed((): User | undefined => { const otherUser = computed((): any | undefined => {
if (!currentChat.value?.participants) return undefined; if (!currentChat.value?.participants) return undefined;
return currentChat.value.participants.find(p => p._id !== currentUserId.value); return currentChat.value.participants.find((p) => p._id !== currentUserId.value);
}); });
const rideInfo = computed((): RideInfo | undefined => { const rideInfo = computed((): RideInfo | undefined => {
@@ -93,14 +112,14 @@ export default defineComponent({
const groups: MessageGroup[] = []; const groups: MessageGroup[] = [];
let currentDate = ''; let currentDate = '';
messages.value.forEach(message => { messages.value.forEach((message) => {
const messageDate = formatDateHeader(new Date(message.createdAt)); const messageDate = formatDateHeader(new Date(message.createdAt));
if (messageDate !== currentDate) { if (messageDate !== currentDate) {
currentDate = messageDate; currentDate = messageDate;
groups.push({ groups.push({
date: messageDate, date: messageDate,
messages: [message] messages: [message],
}); });
} else { } else {
groups[groups.length - 1].messages.push(message); groups[groups.length - 1].messages.push(message);
@@ -133,7 +152,7 @@ export default defineComponent({
return date.toLocaleDateString('it-IT', { return date.toLocaleDateString('it-IT', {
weekday: 'long', weekday: 'long',
day: 'numeric', day: 'numeric',
month: 'long' month: 'long',
}); });
} }
}; };
@@ -158,7 +177,7 @@ export default defineComponent({
day: 'numeric', day: 'numeric',
month: 'short', month: 'short',
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit',
}); });
}; };
@@ -173,7 +192,7 @@ export default defineComponent({
if (messagesContainer.value) { if (messagesContainer.value) {
messagesContainer.value.scrollTo({ messagesContainer.value.scrollTo({
top: messagesContainer.value.scrollHeight, top: messagesContainer.value.scrollHeight,
behavior: smooth ? 'smooth' : 'auto' behavior: smooth ? 'smooth' : 'auto',
}); });
newMessagesCount.value = 0; newMessagesCount.value = 0;
} }
@@ -203,7 +222,7 @@ export default defineComponent({
try { try {
const olderMessages = await loadMessages(chatId.value, { const olderMessages = await loadMessages(chatId.value, {
before: messages.value[0]?.createdAt, before: messages.value[0]?.createdAt,
limit: 30 limit: 30,
}); });
if (olderMessages.length < 30) { if (olderMessages.length < 30) {
@@ -233,7 +252,7 @@ export default defineComponent({
await sendMessageApi(chatId.value, { await sendMessageApi(chatId.value, {
content, content,
type: 'text', type: 'text',
replyTo: replyToId replyTo: replyToId,
}); });
messageText.value = ''; messageText.value = '';
@@ -242,7 +261,7 @@ export default defineComponent({
} catch (error) { } catch (error) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: 'Errore nell\'invio del messaggio' message: "Errore nell'invio del messaggio",
}); });
} finally { } finally {
sending.value = false; sending.value = false;
@@ -268,14 +287,14 @@ export default defineComponent({
$q.dialog({ $q.dialog({
title: 'Elimina messaggio', title: 'Elimina messaggio',
message: 'Eliminare questo messaggio?', message: 'Eliminare questo messaggio?',
cancel: true cancel: true,
}).onOk(async () => { }).onOk(async () => {
try { try {
await deleteMsg(chatId.value, messageId); await deleteMsg(chatId.value, messageId);
} catch (error) { } catch (error) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: 'Errore nell\'eliminazione' message: "Errore nell'eliminazione",
}); });
} }
}); });
@@ -287,14 +306,14 @@ export default defineComponent({
} else { } else {
$q.notify({ $q.notify({
type: 'warning', type: 'warning',
message: 'Numero di telefono non disponibile' message: 'Numero di telefono non disponibile',
}); });
} }
}; };
const viewRide = () => { const viewRide = () => {
if (rideInfo.value?.rideId) { if (rideInfo.value?.rideId) {
router.push(`/trasporti/viaggio/${rideInfo.value.rideId}`); router.push(`/trasporti/ride/${rideInfo.value.rideId}`);
} }
}; };
@@ -312,12 +331,12 @@ export default defineComponent({
$q.notify({ $q.notify({
type: 'info', type: 'info',
message: isMuted.value ? 'Notifiche silenziate' : 'Notifiche attivate', message: isMuted.value ? 'Notifiche silenziate' : 'Notifiche attivate',
icon: isMuted.value ? 'notifications_off' : 'notifications' icon: isMuted.value ? 'notifications_off' : 'notifications',
}); });
} catch (error) { } catch (error) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: 'Errore' message: 'Errore',
}); });
} }
}; };
@@ -327,7 +346,7 @@ export default defineComponent({
title: 'Elimina conversazione', title: 'Elimina conversazione',
message: 'Sei sicuro? Questa azione non è reversibile.', message: 'Sei sicuro? Questa azione non è reversibile.',
cancel: true, cancel: true,
persistent: true persistent: true,
}).onOk(async () => { }).onOk(async () => {
try { try {
// TODO: Implementa eliminazione chat // TODO: Implementa eliminazione chat
@@ -335,7 +354,7 @@ export default defineComponent({
} catch (error) { } catch (error) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: 'Errore nell\'eliminazione' message: "Errore nell'eliminazione",
}); });
} }
}); });
@@ -345,7 +364,7 @@ export default defineComponent({
$q.dialog({ $q.dialog({
title: 'Blocca utente', title: 'Blocca utente',
message: `Bloccare ${otherUser.value?.name}? Non potrete più scambiarvi messaggi.`, message: `Bloccare ${otherUser.value?.name}? Non potrete più scambiarvi messaggi.`,
cancel: true cancel: true,
}).onOk(() => { }).onOk(() => {
// TODO: Implementa blocco // TODO: Implementa blocco
showUserProfile.value = false; showUserProfile.value = false;
@@ -378,20 +397,23 @@ export default defineComponent({
}; };
// Watch for new messages // Watch for new messages
watch(() => messages.value.length, (newLen, oldLen) => { watch(
if (newLen > oldLen) { () => messages.value.length,
const lastMessage = messages.value[messages.value.length - 1]; (newLen, oldLen) => {
if (newLen > oldLen) {
const lastMessage = messages.value[messages.value.length - 1];
if (lastMessage.senderId === currentUserId.value) { if (lastMessage.senderId === currentUserId.value) {
scrollToBottom(); scrollToBottom();
} else if (showScrollButton.value) { } else if (showScrollButton.value) {
newMessagesCount.value++; newMessagesCount.value++;
} else { } else {
scrollToBottom(); scrollToBottom();
markAsRead(chatId.value); markAsRead(chatId.value);
}
} }
} }
}); );
// Lifecycle // Lifecycle
onMounted(async () => { onMounted(async () => {
@@ -399,11 +421,24 @@ export default defineComponent({
await loadMessages(chatId.value, { limit: 50 }); await loadMessages(chatId.value, { limit: 50 });
scrollToBottom(false); scrollToBottom(false);
markAsRead(chatId.value); markAsRead(chatId.value);
subscribeToChat(chatId.value); // AVVIA POLLING
startPolling(chatId.value);
// subscribeToChat(chatId.value);
}); });
// Aggiungi questa funzione nel setup()
const getIsOwn = (message: Message): boolean => {
const senderId =
typeof message.senderId === 'object'
? (message.senderId as any)._id
: message.senderId;
return senderId === currentUserId.value;
};
onUnmounted(() => { onUnmounted(() => {
unsubscribeFromChat(chatId.value); stopPolling();
//unsubscribeFromChat(chatId.value);
}); });
return { return {
@@ -460,7 +495,8 @@ export default defineComponent({
attachDocument, attachDocument,
shareLocation, shareLocation,
sendRideRequest, sendRideRequest,
startVoiceMessage startVoiceMessage,
getIsOwn,
}; };
} },
}); });

View File

@@ -1,24 +1,38 @@
<!-- ChatPage.vue -->
<template> <template>
<q-page class="chat-page"> <q-page class="chat-page">
<!-- Header --> <!-- Header Locale (NON q-header) -->
<q-header class="chat-page__header" elevated> <div class="chat-page__header">
<q-toolbar> <div class="chat-page__toolbar">
<q-btn flat round icon="arrow_back" @click="goBack" /> <q-btn
flat
round
icon="arrow_back"
@click="goBack"
/>
<q-avatar size="42px" class="q-ml-sm" @click="showUserProfile = true"> <q-avatar
size="42px"
class="q-ml-sm"
@click="showUserProfile = true"
>
<img <img
v-if="otherUser?.profile?.img" v-if="otherUser?.profile?.img"
:src="otherUser.profile.img" :src="otherUser.profile.img"
:alt="otherUser.name" :alt="otherUser.name"
/> />
<div v-else class="chat-page__avatar-placeholder"> <div
v-else
class="chat-page__avatar-placeholder"
>
{{ getInitials(otherUser) }} {{ getInitials(otherUser) }}
</div> </div>
</q-avatar> </q-avatar>
<q-toolbar-title class="q-ml-sm"> <div class="chat-page__toolbar-title">
<div class="chat-page__user-name" @click="showUserProfile = true"> <div
class="chat-page__user-name"
@click="showUserProfile = true"
>
{{ otherUser?.name }} {{ otherUser?.surname }} {{ otherUser?.name }} {{ otherUser?.surname }}
</div> </div>
<div class="chat-page__user-status"> <div class="chat-page__user-status">
@@ -27,27 +41,49 @@
<span class="chat-page__typing-dots">...</span> <span class="chat-page__typing-dots">...</span>
</template> </template>
<template v-else-if="isOnline"> <template v-else-if="isOnline">
<q-icon name="circle" size="8px" color="positive" /> <q-icon
name="circle"
size="8px"
color="positive"
/>
<span>Online</span> <span>Online</span>
</template> </template>
<template v-else-if="lastSeen"> <template v-else-if="lastSeen">
<span>Ultimo accesso {{ formatLastSeen(lastSeen) }}</span> <span>Ultimo accesso {{ formatLastSeen(lastSeen) }}</span>
</template> </template>
</div> </div>
</q-toolbar-title> </div>
<q-btn flat round icon="phone" @click="callUser" /> <q-btn
<q-btn flat round icon="more_vert"> flat
round
icon="phone"
@click="callUser"
/>
<q-btn
flat
round
icon="more_vert"
>
<q-menu> <q-menu>
<q-list style="min-width: 200px"> <q-list style="min-width: 200px">
<q-item clickable v-close-popup @click="showUserProfile = true"> <q-item
clickable
v-close-popup
@click="showUserProfile = true"
>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="person" /> <q-icon name="person" />
</q-item-section> </q-item-section>
<q-item-section>Profilo utente</q-item-section> <q-item-section>Profilo utente</q-item-section>
</q-item> </q-item>
<q-item v-if="rideInfo" clickable v-close-popup @click="viewRide"> <q-item
v-if="rideInfo"
clickable
v-close-popup
@click="viewRide"
>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="directions_car" /> <q-icon name="directions_car" />
</q-item-section> </q-item-section>
@@ -56,35 +92,57 @@
<q-separator /> <q-separator />
<q-item clickable v-close-popup @click="searchInChat = true"> <q-item
clickable
v-close-popup
@click="searchInChat = true"
>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="search" /> <q-icon name="search" />
</q-item-section> </q-item-section>
<q-item-section>Cerca</q-item-section> <q-item-section>Cerca</q-item-section>
</q-item> </q-item>
<q-item clickable v-close-popup @click="toggleMute"> <q-item
clickable
v-close-popup
@click="toggleMute"
>
<q-item-section avatar> <q-item-section avatar>
<q-icon :name="isMuted ? 'notifications' : 'notifications_off'" /> <q-icon :name="isMuted ? 'notifications' : 'notifications_off'" />
</q-item-section> </q-item-section>
<q-item-section>{{ isMuted ? 'Attiva notifiche' : 'Silenzia' }}</q-item-section> <q-item-section>{{
isMuted ? 'Attiva notifiche' : 'Silenzia'
}}</q-item-section>
</q-item> </q-item>
<q-separator /> <q-separator />
<q-item clickable v-close-popup class="text-negative" @click="deleteConversation"> <q-item
clickable
v-close-popup
class="text-negative"
@click="deleteConversation"
>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="delete" color="negative" /> <q-icon
name="delete"
color="negative"
/>
</q-item-section> </q-item-section>
<q-item-section>Elimina conversazione</q-item-section> <q-item-section>Elimina conversazione</q-item-section>
</q-item> </q-item>
</q-list> </q-list>
</q-menu> </q-menu>
</q-btn> </q-btn>
</q-toolbar> </div>
<!-- Ride Info Banner --> <!-- Ride Info Banner -->
<div v-if="rideInfo" class="chat-page__ride-banner" @click="viewRide"> <div
v-if="rideInfo"
class="chat-page__ride-banner"
@click="viewRide"
>
<div class="chat-page__ride-banner-content"> <div class="chat-page__ride-banner-content">
<q-icon <q-icon
:name="rideInfo.type === 'offer' ? 'directions_car' : 'hail'" :name="rideInfo.type === 'offer' ? 'directions_car' : 'hail'"
@@ -102,7 +160,7 @@
</div> </div>
<q-icon name="chevron_right" /> <q-icon name="chevron_right" />
</div> </div>
</q-header> </div>
<!-- Messages Area --> <!-- Messages Area -->
<div <div
@@ -111,7 +169,10 @@
@scroll="onScroll" @scroll="onScroll"
> >
<!-- Load More --> <!-- Load More -->
<div v-if="hasMoreMessages" class="chat-page__load-more"> <div
v-if="hasMoreMessages"
class="chat-page__load-more"
>
<q-btn <q-btn
flat flat
round round
@@ -122,7 +183,10 @@
</div> </div>
<!-- Date separators and messages --> <!-- Date separators and messages -->
<template v-for="(group, index) in groupedMessages" :key="index"> <template
v-for="(group, index) in groupedMessages"
:key="index"
>
<div class="chat-page__date-separator"> <div class="chat-page__date-separator">
<span>{{ group.date }}</span> <span>{{ group.date }}</span>
</div> </div>
@@ -131,19 +195,25 @@
v-for="message in group.messages" v-for="message in group.messages"
:key="message._id" :key="message._id"
:message="message" :message="message"
:is-mine="message.senderId === currentUserId" :is-own="getIsOwn(message)"
:show-avatar="shouldShowAvatar(message, group.messages)" :show-avatar="shouldShowAvatar(message, group.messages)"
:sender="message.senderId === currentUserId ? currentUser : otherUser" :sender="getIsOwn(message) ? currentUser : otherUser"
@reply="replyTo = message" @reply="replyTo = message"
@react="onReact" @reaction-click="onReact"
@delete="onDeleteMessage" @delete="onDeleteMessage"
/> />
</template> </template>
<!-- Typing indicator --> <!-- Typing indicator -->
<div v-if="isTyping" class="chat-page__typing-indicator"> <div
v-if="isTyping"
class="chat-page__typing-indicator"
>
<q-avatar size="32px"> <q-avatar size="32px">
<img v-if="otherUser?.profile?.img" :src="otherUser.profile.img" /> <img
v-if="otherUser?.profile?.img"
:src="otherUser.profile.img"
/>
<span v-else>{{ getInitials(otherUser) }}</span> <span v-else>{{ getInitials(otherUser) }}</span>
</q-avatar> </q-avatar>
<div class="chat-page__typing-bubble"> <div class="chat-page__typing-bubble">
@@ -165,7 +235,11 @@
size="md" size="md"
@click="scrollToBottom" @click="scrollToBottom"
> >
<q-badge v-if="newMessagesCount > 0" color="primary" floating> <q-badge
v-if="newMessagesCount > 0"
color="primary"
floating
>
{{ newMessagesCount }} {{ newMessagesCount }}
</q-badge> </q-badge>
</q-btn> </q-btn>
@@ -174,9 +248,15 @@
<!-- Reply Preview --> <!-- Reply Preview -->
<transition name="slide-up"> <transition name="slide-up">
<div v-if="replyTo" class="chat-page__reply-preview"> <div
v-if="replyTo"
class="chat-page__reply-preview"
>
<div class="chat-page__reply-content"> <div class="chat-page__reply-content">
<q-icon name="reply" size="20px" /> <q-icon
name="reply"
size="20px"
/>
<div> <div>
<span class="chat-page__reply-author"> <span class="chat-page__reply-author">
{{ replyTo.senderId === currentUserId ? 'Tu' : otherUser?.name }} {{ replyTo.senderId === currentUserId ? 'Tu' : otherUser?.name }}
@@ -184,7 +264,13 @@
<span class="chat-page__reply-text">{{ replyTo.content }}</span> <span class="chat-page__reply-text">{{ replyTo.content }}</span>
</div> </div>
</div> </div>
<q-btn flat round size="sm" icon="close" @click="replyTo = null" /> <q-btn
flat
round
size="sm"
icon="close"
@click="replyTo = null"
/>
</div> </div>
</transition> </transition>
@@ -211,7 +297,13 @@
@update:model-value="onTyping" @update:model-value="onTyping"
> >
<template #append> <template #append>
<q-btn flat round dense icon="mood" @click="showEmoji = !showEmoji"> <q-btn
flat
round
dense
icon="mood"
@click="showEmoji = !showEmoji"
>
<q-popup-proxy <q-popup-proxy
v-model="showEmoji" v-model="showEmoji"
:offset="[0, 10]" :offset="[0, 10]"
@@ -243,47 +335,104 @@
/> />
</div> </div>
<!-- Attachment Menu --> <!-- Resto dei dialog... -->
<q-dialog v-model="showAttachMenu" position="bottom"> <q-dialog
v-model="showAttachMenu"
position="bottom"
>
<q-card class="chat-page__attach-menu"> <q-card class="chat-page__attach-menu">
<div class="chat-page__attach-grid"> <div class="chat-page__attach-grid">
<div class="chat-page__attach-item" @click="attachImage"> <div
<q-avatar color="purple" text-color="white" icon="image" /> class="chat-page__attach-item"
@click="attachImage"
>
<q-avatar
color="purple"
text-color="white"
icon="image"
/>
<span>Foto</span> <span>Foto</span>
</div> </div>
<div class="chat-page__attach-item" @click="attachDocument"> <div
<q-avatar color="blue" text-color="white" icon="description" /> class="chat-page__attach-item"
@click="attachDocument"
>
<q-avatar
color="blue"
text-color="white"
icon="description"
/>
<span>Documento</span> <span>Documento</span>
</div> </div>
<div class="chat-page__attach-item" @click="shareLocation"> <div
<q-avatar color="green" text-color="white" icon="location_on" /> class="chat-page__attach-item"
@click="shareLocation"
>
<q-avatar
color="green"
text-color="white"
icon="location_on"
/>
<span>Posizione</span> <span>Posizione</span>
</div> </div>
<div v-if="rideInfo" class="chat-page__attach-item" @click="sendRideRequest"> <div
<q-avatar color="orange" text-color="white" icon="directions_car" /> v-if="rideInfo"
class="chat-page__attach-item"
@click="sendRideRequest"
>
<q-avatar
color="orange"
text-color="white"
icon="directions_car"
/>
<span>Richiedi passaggio</span> <span>Richiedi passaggio</span>
</div> </div>
</div> </div>
</q-card> </q-card>
</q-dialog> </q-dialog>
<!-- User Profile Dialog --> <q-dialog
<q-dialog v-model="showUserProfile" position="right" full-height> v-model="showUserProfile"
position="right"
full-height
>
<q-card class="chat-page__profile-card"> <q-card class="chat-page__profile-card">
<q-card-section class="chat-page__profile-header"> <q-card-section class="chat-page__profile-header">
<q-btn flat round icon="close" @click="showUserProfile = false" /> <q-btn
flat
round
icon="close"
@click="showUserProfile = false"
/>
<q-avatar size="100px"> <q-avatar size="100px">
<img v-if="otherUser?.profile?.img" :src="otherUser.profile.img" /> <img
<span v-else class="text-h4">{{ getInitials(otherUser) }}</span> v-if="otherUser?.profile?.img"
:src="otherUser.profile.img"
/>
<span
v-else
class="text-h4"
>{{ getInitials(otherUser) }}</span
>
</q-avatar> </q-avatar>
<h4>{{ otherUser?.name }} {{ otherUser?.surname }}</h4> <h4>{{ otherUser?.name }} {{ otherUser?.surname }}</h4>
<p>@{{ otherUser?.username }}</p> <p>@{{ otherUser?.username }}</p>
<div class="chat-page__profile-actions"> <div class="chat-page__profile-actions">
<q-btn round color="primary" icon="directions_car" @click="viewDriverProfile"> <q-btn
round
color="primary"
icon="directions_car"
@click="viewDriverProfile"
>
<q-tooltip>Profilo guida</q-tooltip> <q-tooltip>Profilo guida</q-tooltip>
</q-btn> </q-btn>
<q-btn round color="secondary" icon="phone" @click="callUser"> <q-btn
round
color="secondary"
icon="phone"
@click="callUser"
>
<q-tooltip>Chiama</q-tooltip> <q-tooltip>Chiama</q-tooltip>
</q-btn> </q-btn>
</div> </div>
@@ -314,13 +463,18 @@
<q-item v-if="otherUser?.profile?.driverProfile"> <q-item v-if="otherUser?.profile?.driverProfile">
<q-item-section avatar> <q-item-section avatar>
<q-icon name="star" color="amber" /> <q-icon
name="star"
color="amber"
/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label caption>Valutazione guida</q-item-label> <q-item-label caption>Valutazione guida</q-item-label>
<q-item-label> <q-item-label>
{{ otherUser.profile.driverProfile.rating?.toFixed(1) || 'N/D' }} {{ otherUser.profile.driverProfile.rating?.toFixed(1) || 'N/D' }}
<span class="text-grey"> · {{ otherUser.profile.driverProfile.totalRides || 0 }} viaggi</span> <span class="text-grey">
· {{ otherUser.profile.driverProfile.totalRides || 0 }} viaggi</span
>
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>

View File

@@ -73,14 +73,14 @@ export default defineComponent({
}; };
const goToRide = (rideId: string) => { const goToRide = (rideId: string) => {
router.push(`/trasporti/viaggio/${rideId}`); router.push(`/trasporti/ride/${rideId}`);
}; };
const contactUser = async () => { const contactUser = async () => {
try { try {
const response = await getOrCreateDirectChat(userId.value); const response = await getOrCreateDirectChat(userId.value);
if (response?.data?.data) { if (response?.data) {
router.push(`/trasporti/chat/${response.data.data._id}`); router.push(`/trasporti/chat/${response.data._id}`);
} }
} catch (error: any) { } catch (error: any) {
$q.notify({ type: 'negative', message: error.data?.message || error.message }); $q.notify({ type: 'negative', message: error.data?.message || error.message });

View File

@@ -101,7 +101,7 @@ export default defineComponent({
}; };
const goToRide = (rideId: string) => { const goToRide = (rideId: string) => {
router.push(`/trasporti/viaggio/${rideId}`); router.push(`/trasporti/ride/${rideId}`);
}; };
const goToProfile = (userId: string) => { const goToProfile = (userId: string) => {
@@ -109,7 +109,7 @@ export default defineComponent({
}; };
const editRide = (rideId: string) => { const editRide = (rideId: string) => {
router.push(`/trasporti/viaggio/${rideId}/modifica`); router.push(`/trasporti/ride/${rideId}/modifica`);
}; };
const cancelRide = async (ride: Ride) => { const cancelRide = async (ride: Ride) => {

View File

@@ -360,9 +360,9 @@ export default defineComponent({
try { try {
const [statsRes, receivedRes, givenRes] = await Promise.all([ const [statsRes, receivedRes, givenRes] = await Promise.all([
Api.SendReq('/api/trasporti/feedback/stats', 'GET'), Api.SendReqWithData('/api/trasporti/feedback/stats', 'GET'),
Api.SendReq('/api/trasporti/feedback/received', 'GET'), Api.SendReqWithData('/api/trasporti/feedback/received', 'GET'),
Api.SendReq('/api/trasporti/feedback/given', 'GET') Api.SendReqWithData('/api/trasporti/feedback/given', 'GET')
]); ]);
if (statsRes.success) { if (statsRes.success) {
@@ -396,7 +396,7 @@ export default defineComponent({
? '/api/trasporti/feedback/received' ? '/api/trasporti/feedback/received'
: '/api/trasporti/feedback/given'; : '/api/trasporti/feedback/given';
const response = await Api.SendReq(`${endpoint}?page=${currentPage.value}`, 'GET'); const response = await Api.SendReqWithData(`${endpoint}?page=${currentPage.value}`, 'GET');
if (response.success) { if (response.success) {
const newFeedbacks = response.data.feedbacks || []; const newFeedbacks = response.data.feedbacks || [];

View File

@@ -367,7 +367,7 @@ export default defineComponent({
const endpoint = responseAction.value === 'accept' const endpoint = responseAction.value === 'accept'
? `/api/trasporti/richieste/${selectedRequest.value._id}/accept` ? `/api/trasporti/richieste/${selectedRequest.value._id}/accept`
: `/api/trasporti/richieste/${selectedRequest.value._id}/reject`; : `/api/trasporti/richieste/${selectedRequest.value._id}/reject`;
const response = await Api.SendReq(endpoint, 'PUT', { message: responseMessage.value }); const response = await Api.SendReqWithData(endpoint, 'PUT', { message: responseMessage.value });
if (response.success) { if (response.success) {
const index = receivedRequests.value.findIndex(r => r._id === selectedRequest.value?._id); const index = receivedRequests.value.findIndex(r => r._id === selectedRequest.value?._id);
if (index !== -1) { if (index !== -1) {
@@ -390,7 +390,7 @@ export default defineComponent({
const cancelRequest = async (request: RideRequest) => { const cancelRequest = async (request: RideRequest) => {
$q.dialog({ title: 'Annulla richiesta', message: 'Sei sicuro?', cancel: true }).onOk(async () => { $q.dialog({ title: 'Annulla richiesta', message: 'Sei sicuro?', cancel: true }).onOk(async () => {
try { try {
const response = await Api.SendReq(`/api/trasporti/richieste/${request._id}/cancel`, 'PUT'); const response = await Api.SendReqWithData(`/api/trasporti/richieste/${request._id}/cancel`, 'PUT');
if (response.success) { if (response.success) {
const index = sentRequests.value.findIndex(r => r._id === request._id); const index = sentRequests.value.findIndex(r => r._id === request._id);
if (index !== -1) sentRequests.value[index].status = 'cancelled'; if (index !== -1) sentRequests.value[index].status = 'cancelled';
@@ -406,9 +406,9 @@ export default defineComponent({
loading.value = true; loading.value = true;
try { try {
const [statsRes, receivedRes, sentRes] = await Promise.all([ const [statsRes, receivedRes, sentRes] = await Promise.all([
Api.SendReq('/api/trasporti/richieste/stats', 'GET'), Api.SendReqWithData('/api/trasporti/richieste/stats', 'GET'),
Api.SendReq('/api/trasporti/richieste/received', 'GET'), Api.SendReqWithData('/api/trasporti/richieste/received', 'GET'),
Api.SendReq('/api/trasporti/richieste/sent', 'GET') Api.SendReqWithData('/api/trasporti/richieste/sent', 'GET')
]); ]);
if (statsRes.success) stats.value = statsRes.data; if (statsRes.success) stats.value = statsRes.data;
if (receivedRes.success) { if (receivedRes.success) {
@@ -428,7 +428,7 @@ export default defineComponent({
currentPage.value++; currentPage.value++;
try { try {
const endpoint = activeTab.value === 'received' ? '/api/trasporti/richieste/received' : '/api/trasporti/richieste/sent'; const endpoint = activeTab.value === 'received' ? '/api/trasporti/richieste/received' : '/api/trasporti/richieste/sent';
const response = await Api.SendReq(`${endpoint}?page=${currentPage.value}`, 'GET'); const response = await Api.SendReqWithData(`${endpoint}?page=${currentPage.value}`, 'GET');
if (response.success) { if (response.success) {
const newRequests = response.data.requests || []; const newRequests = response.data.requests || [];
if (activeTab.value === 'received') receivedRequests.value.push(...newRequests); if (activeTab.value === 'received') receivedRequests.value.push(...newRequests);

View File

@@ -292,8 +292,8 @@ export default defineComponent({
if (isEditing.value) { if (isEditing.value) {
try { try {
const response = await fetchRide(rideId.value!); const response = await fetchRide(rideId.value!);
if (response?.data?.data) { if (response?.data) {
const ride = response.data.data; const ride = response.data;
formData.type = ride.type; formData.type = ride.type;
formData.departure = ride.departure; formData.departure = ride.departure;

View File

@@ -198,7 +198,7 @@ export default defineComponent({
}; };
const editRide = () => { const editRide = () => {
router.push(`/trasporti/viaggio/${rideId.value}/modifica`); router.push(`/trasporti/ride/${rideId.value}/modifica`);
}; };
const shareRide = async () => { const shareRide = async () => {
@@ -280,8 +280,8 @@ export default defineComponent({
const contactDriver = async () => { const contactDriver = async () => {
try { try {
const response = await getOrCreateDirectChat(driverId.value, rideId.value); const response = await getOrCreateDirectChat(driverId.value, rideId.value);
if (response?.data?.data) { if (response?.data) {
router.push(`/trasporti/chat/${response.data.data._id}`); router.push(`/trasporti/chat/${response.data._id}`);
} }
} catch (error: any) { } catch (error: any) {
$q.notify({ type: 'negative', message: error.data?.message || error.message }); $q.notify({ type: 'negative', message: error.data?.message || error.message });
@@ -292,8 +292,8 @@ export default defineComponent({
const userId = typeof user === 'string' ? user : user._id; const userId = typeof user === 'string' ? user : user._id;
try { try {
const response = await getOrCreateDirectChat(userId, rideId.value); const response = await getOrCreateDirectChat(userId, rideId.value);
if (response?.data?.data) { if (response?.data) {
router.push(`/trasporti/chat/${response.data.data._id}`); router.push(`/trasporti/chat/${response.data._id}`);
} }
} catch (error: any) { } catch (error: any) {
$q.notify({ type: 'negative', message: error.data?.message || error.message }); $q.notify({ type: 'negative', message: error.data?.message || error.message });

View File

@@ -180,7 +180,7 @@ export default defineComponent({
}; };
const goToRide = (rideId: string) => { const goToRide = (rideId: string) => {
router.push(`/trasporti/viaggio/${rideId}`); router.push(`/trasporti/ride/${rideId}`);
}; };
const goToProfile = (userId: string) => { const goToProfile = (userId: string) => {
@@ -218,8 +218,8 @@ export default defineComponent({
try { try {
const userId = typeof ride.userId === 'string' ? ride.userId : ride.userId._id; const userId = typeof ride.userId === 'string' ? ride.userId : ride.userId._id;
const response = await getOrCreateDirectChat(userId, ride._id); const response = await getOrCreateDirectChat(userId, ride._id);
if (response?.data?.data) { if (response?.data) {
router.push(`/trasporti/chat/${response.data.data._id}`); router.push(`/trasporti/chat/${response.data._id}`);
} }
} catch (error: any) { } catch (error: any) {
$q.notify({ type: 'negative', message: error.data?.message || error.message }); $q.notify({ type: 'negative', message: error.data?.message || error.message });

View File

@@ -81,7 +81,7 @@ export default defineComponent({
}; };
const goToDetail = (rideId: string) => { const goToDetail = (rideId: string) => {
router.push(`/trasporti/viaggio/${rideId}`); router.push(`/trasporti/ride/${rideId}`);
}; };
const goToDriverProfile = (userId: string) => { const goToDriverProfile = (userId: string) => {
@@ -135,8 +135,8 @@ export default defineComponent({
const userId = typeof ride.userId === 'string' ? ride.userId : ride.userId._id; const userId = typeof ride.userId === 'string' ? ride.userId : ride.userId._id;
const response = await getOrCreateDirectChat(userId, ride._id); const response = await getOrCreateDirectChat(userId, ride._id);
if (response?.data?.data) { if (response?.data) {
router.push(`/trasporti/chat/${response.data.data._id}`); router.push(`/trasporti/chat/${response.data._id}`);
} }
} catch (error: any) { } catch (error: any) {
$q.notify({ $q.notify({

View File

@@ -536,8 +536,8 @@ export default defineComponent({
loading.value = true; loading.value = true;
try { try {
const [settingsRes, vehiclesRes] = await Promise.all([ const [settingsRes, vehiclesRes] = await Promise.all([
Api.SendReq('/api/trasporti/settings', 'GET'), Api.SendReqWithData('/api/trasporti/settings', 'GET'),
Api.SendReq('/api/trasporti/veicoli', 'GET') Api.SendReqWithData('/api/trasporti/driver/vehicles', 'GET')
]); ]);
if (settingsRes.success && settingsRes.data) { if (settingsRes.success && settingsRes.data) {
@@ -557,7 +557,7 @@ export default defineComponent({
const saveSettings = async () => { const saveSettings = async () => {
saving.value = true; saving.value = true;
try { try {
const response = await Api.SendReq('/api/trasporti/settings', 'PUT', settings.value); const response = await Api.SendReqWithData('/api/trasporti/settings', 'PUT', settings.value);
if (response.success) { if (response.success) {
$q.notify({ type: 'positive', message: 'Impostazioni salvate', timeout: 1500 }); $q.notify({ type: 'positive', message: 'Impostazioni salvate', timeout: 1500 });
} }

View File

@@ -12,18 +12,30 @@
/> />
<div> <div>
<h1>{{ isEdit ? 'Modifica Veicolo' : 'Nuovo Veicolo' }}</h1> <h1>{{ isEdit ? 'Modifica Veicolo' : 'Nuovo Veicolo' }}</h1>
<p>{{ isEdit ? 'Aggiorna i dati del tuo veicolo' : 'Aggiungi un nuovo mezzo' }}</p> <p>
{{ isEdit ? 'Aggiorna i dati del tuo veicolo' : 'Aggiungi un nuovo mezzo' }}
</p>
</div> </div>
</div> </div>
<!-- Loading --> <!-- Loading -->
<div v-if="loading" class="vehicle-edit-page__loading"> <div
<q-spinner-dots size="50px" color="primary" /> v-if="loading"
class="vehicle-edit-page__loading"
>
<q-spinner-dots
size="50px"
color="primary"
/>
<p>Caricamento dati...</p> <p>Caricamento dati...</p>
</div> </div>
<!-- Form --> <!-- Form -->
<q-form v-else @submit="onSubmit" class="vehicle-edit-page__form"> <q-form
v-else
@submit="onSubmit"
class="vehicle-edit-page__form"
>
<!-- Photos Section --> <!-- Photos Section -->
<div class="vehicle-edit-page__section"> <div class="vehicle-edit-page__section">
<h3 class="vehicle-edit-page__section-title"> <h3 class="vehicle-edit-page__section-title">
@@ -32,12 +44,16 @@
</h3> </h3>
<div class="vehicle-edit-page__photos"> <div class="vehicle-edit-page__photos">
<!-- Existing Photos -->
<div <div
v-for="(photo, index) in form.photos" v-for="(photo, index) in form.photos"
:key="index" :key="index"
class="vehicle-edit-page__photo" class="vehicle-edit-page__photo"
> >
<q-img :src="photo" :ratio="1" /> <q-img
:src="photo"
:ratio="1"
/>
<q-btn <q-btn
round round
flat flat
@@ -47,19 +63,29 @@
class="vehicle-edit-page__photo-remove" class="vehicle-edit-page__photo-remove"
@click="removePhoto(index)" @click="removePhoto(index)"
/> />
<div v-if="index === 0" class="vehicle-edit-page__photo-badge">
<q-icon name="star" size="12px" />
Principale
</div>
</div> </div>
<!-- Add Photo Button -->
<div <div
v-if="form.photos.length < 4" v-if="form.photos.length < 5"
class="vehicle-edit-page__photo-add" class="vehicle-edit-page__photo-add"
@click="addPhoto" @click="addPhoto"
> >
<q-icon name="add_a_photo" size="32px" color="grey-5" /> <q-icon
name="add_a_photo"
size="32px"
color="grey-5"
/>
<span>Aggiungi foto</span> <span>Aggiungi foto</span>
</div> </div>
</div> </div>
<p class="vehicle-edit-page__photo-hint"> <p class="vehicle-edit-page__photo-hint">
Puoi aggiungere fino a 4 foto del tuo veicolo <q-icon name="info" size="14px" />
Puoi aggiungere fino a 5 foto. La prima sarà quella principale.
</p> </p>
</div> </div>
@@ -76,10 +102,15 @@
v-for="vType in vehicleTypes" v-for="vType in vehicleTypes"
:key="vType.value" :key="vType.value"
class="vehicle-edit-page__type-option" class="vehicle-edit-page__type-option"
:class="{ 'vehicle-edit-page__type-option--active': form.type === vType.value }" :class="{
'vehicle-edit-page__type-option--active': form.type === vType.value,
}"
@click="form.type = vType.value" @click="form.type = vType.value"
> >
<q-icon :name="vType.icon" size="28px" /> <q-icon
:name="vType.icon"
size="28px"
/>
<span>{{ vType.label }}</span> <span>{{ vType.label }}</span>
</div> </div>
</div> </div>
@@ -90,7 +121,7 @@
v-model="form.brand" v-model="form.brand"
label="Marca *" label="Marca *"
outlined outlined
:rules="[val => !!val || 'Campo obbligatorio']" :rules="[(val) => !!val || 'Campo obbligatorio']"
class="vehicle-edit-page__input" class="vehicle-edit-page__input"
> >
<template #prepend> <template #prepend>
@@ -102,7 +133,7 @@
v-model="form.model" v-model="form.model"
label="Modello *" label="Modello *"
outlined outlined
:rules="[val => !!val || 'Campo obbligatorio']" :rules="[(val) => !!val || 'Campo obbligatorio']"
class="vehicle-edit-page__input" class="vehicle-edit-page__input"
> >
<template #prepend> <template #prepend>
@@ -158,13 +189,8 @@
v-model="form.plate" v-model="form.plate"
label="Targa *" label="Targa *"
outlined outlined
:rules="[
val => !!val || 'Campo obbligatorio',
val => /^[A-Z]{2}[0-9]{3}[A-Z]{2}$/i.test(val) || 'Formato targa non valido (es. AB123CD)'
]"
hint="Formato: AB123CD"
class="vehicle-edit-page__input--full" class="vehicle-edit-page__input--full"
@update:model-value="val => form.plate = val.toUpperCase()" @update:model-value="(val) => (form.plate = val.toUpperCase())"
> >
<template #prepend> <template #prepend>
<q-icon name="pin" /> <q-icon name="pin" />
@@ -191,7 +217,10 @@
@click="form.seats = Math.max(1, form.seats - 1)" @click="form.seats = Math.max(1, form.seats - 1)"
/> />
<div class="vehicle-edit-page__seats-value"> <div class="vehicle-edit-page__seats-value">
<q-icon name="event_seat" size="24px" /> <q-icon
name="event_seat"
size="24px"
/>
<span>{{ form.seats }}</span> <span>{{ form.seats }}</span>
</div> </div>
<q-btn <q-btn
@@ -202,9 +231,7 @@
@click="form.seats = Math.min(8, form.seats + 1)" @click="form.seats = Math.min(8, form.seats + 1)"
/> />
</div> </div>
<p class="vehicle-edit-page__seats-hint"> <p class="vehicle-edit-page__seats-hint">Escluso il conducente</p>
Escluso il conducente
</p>
</div> </div>
<!-- Fuel Type --> <!-- Fuel Type -->
@@ -215,10 +242,15 @@
v-for="fuel in fuelTypes" v-for="fuel in fuelTypes"
:key="fuel.value" :key="fuel.value"
class="vehicle-edit-page__fuel-option" class="vehicle-edit-page__fuel-option"
:class="{ 'vehicle-edit-page__fuel-option--active': form.fuelType === fuel.value }" :class="{
'vehicle-edit-page__fuel-option--active': form.fuelType === fuel.value,
}"
@click="form.fuelType = fuel.value" @click="form.fuelType = fuel.value"
> >
<q-icon :name="fuel.icon" size="20px" /> <q-icon
:name="fuel.icon"
size="20px"
/>
<span>{{ fuel.label }}</span> <span>{{ fuel.label }}</span>
</div> </div>
</div> </div>
@@ -243,7 +275,10 @@
> >
<template #default> <template #default>
<div class="vehicle-edit-page__feature-content"> <div class="vehicle-edit-page__feature-content">
<q-icon :name="feature.icon" size="20px" /> <q-icon
:name="feature.icon"
size="20px"
/>
<span>{{ feature.label }}</span> <span>{{ feature.label }}</span>
</div> </div>
</template> </template>
@@ -271,7 +306,10 @@
<!-- Default Toggle --> <!-- Default Toggle -->
<div class="vehicle-edit-page__section"> <div class="vehicle-edit-page__section">
<q-item tag="label" class="vehicle-edit-page__toggle"> <q-item
tag="label"
class="vehicle-edit-page__toggle"
>
<q-item-section> <q-item-section>
<q-item-label>Imposta come predefinito</q-item-label> <q-item-label>Imposta come predefinito</q-item-label>
<q-item-label caption> <q-item-label caption>
@@ -279,7 +317,10 @@
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
<q-item-section side> <q-item-section side>
<q-toggle v-model="form.isDefault" color="primary" /> <q-toggle
v-model="form.isDefault"
color="primary"
/>
</q-item-section> </q-item-section>
</q-item> </q-item>
</div> </div>
@@ -303,13 +344,14 @@
</div> </div>
</q-form> </q-form>
<!-- Hidden File Input --> <!-- Hidden File Input for Multiple Photos -->
<input <input
ref="fileInput" ref="fileInput"
type="file" type="file"
accept="image/*" accept="image/*"
multiple
style="display: none" style="display: none"
@change="onPhotoSelected" @change="onPhotosSelected"
/> />
</q-page> </q-page>
</template> </template>
@@ -347,6 +389,7 @@ export default defineComponent({
// State // State
const loading = ref(false); const loading = ref(false);
const saving = ref(false); const saving = ref(false);
const uploadingPhotos = ref(false);
const isEdit = computed(() => !!route.params.vehicleId); const isEdit = computed(() => !!route.params.vehicleId);
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
@@ -362,7 +405,7 @@ export default defineComponent({
features: [], features: [],
photos: [], photos: [],
notes: '', notes: '',
isDefault: false isDefault: false,
}); });
// Options // Options
@@ -370,7 +413,7 @@ export default defineComponent({
{ value: 'car', label: 'Auto', icon: 'directions_car' }, { value: 'car', label: 'Auto', icon: 'directions_car' },
{ value: 'suv', label: 'SUV', icon: 'local_shipping' }, { value: 'suv', label: 'SUV', icon: 'local_shipping' },
{ value: 'van', label: 'Van', icon: 'airport_shuttle' }, { value: 'van', label: 'Van', icon: 'airport_shuttle' },
{ value: 'motorcycle', label: 'Moto', icon: 'two_wheeler' } { value: 'motorcycle', label: 'Moto', icon: 'two_wheeler' },
]; ];
const fuelTypes = [ const fuelTypes = [
@@ -379,7 +422,7 @@ export default defineComponent({
{ value: 'hybrid', label: 'Ibrido', icon: 'eco' }, { value: 'hybrid', label: 'Ibrido', icon: 'eco' },
{ value: 'electric', label: 'Elettrico', icon: 'bolt' }, { value: 'electric', label: 'Elettrico', icon: 'bolt' },
{ value: 'lpg', label: 'GPL', icon: 'propane_tank' }, { value: 'lpg', label: 'GPL', icon: 'propane_tank' },
{ value: 'methane', label: 'Metano', icon: 'propane' } { value: 'methane', label: 'Metano', icon: 'propane' },
]; ];
const colorOptions = [ const colorOptions = [
@@ -393,7 +436,7 @@ export default defineComponent({
{ value: 'giallo', label: 'Giallo', hex: '#fdd835' }, { value: 'giallo', label: 'Giallo', hex: '#fdd835' },
{ value: 'arancione', label: 'Arancione', hex: '#fb8c00' }, { value: 'arancione', label: 'Arancione', hex: '#fb8c00' },
{ value: 'marrone', label: 'Marrone', hex: '#795548' }, { value: 'marrone', label: 'Marrone', hex: '#795548' },
{ value: 'beige', label: 'Beige', hex: '#d7ccc8' } { value: 'beige', label: 'Beige', hex: '#d7ccc8' },
]; ];
const availableFeatures = [ const availableFeatures = [
@@ -406,73 +449,143 @@ export default defineComponent({
{ value: 'animali', label: 'Animali ammessi', icon: 'pets' }, { value: 'animali', label: 'Animali ammessi', icon: 'pets' },
{ value: 'wifi', label: 'WiFi', icon: 'wifi' }, { value: 'wifi', label: 'WiFi', icon: 'wifi' },
{ value: 'fumatori', label: 'Si può fumare', icon: 'smoking_rooms' }, { value: 'fumatori', label: 'Si può fumare', icon: 'smoking_rooms' },
{ value: 'no_fumatori', label: 'Vietato fumare', icon: 'smoke_free' } { value: 'no_fumatori', label: 'Vietato fumare', icon: 'smoke_free' },
]; ];
// Methods // Methods
const confirmCancel = () => { const confirmCancel = () => {
$q.dialog({ $q.dialog({
title: 'Annulla', title: 'Annulla',
message: 'Sei sicuro di voler annullare? Le modifiche non salvate andranno perse.', message:
'Sei sicuro di voler annullare? Le modifiche non salvate andranno perse.',
cancel: true, cancel: true,
persistent: true persistent: true,
}).onOk(() => { }).onOk(() => {
router.back(); router.back();
}); });
}; };
const addPhoto = () => { const addPhoto = () => {
if (form.value.photos.length >= 5) {
$q.notify({
type: 'warning',
message: 'Puoi caricare massimo 5 foto',
});
return;
}
fileInput.value?.click(); fileInput.value?.click();
}; };
const onPhotoSelected = async (event: Event) => { const onPhotosSelected = async (event: Event) => {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
if (!input.files?.length) return; if (!input.files?.length) return;
const file = input.files[0]; const files = Array.from(input.files);
const remainingSlots = 5 - form.value.photos.length;
// Validate file if (files.length > remainingSlots) {
if (!file.type.startsWith('image/')) {
$q.notify({ $q.notify({
type: 'negative', type: 'warning',
message: 'Seleziona un file immagine valido' message: `Puoi caricare solo ${remainingSlots} foto in più`,
}); });
return; files.splice(remainingSlots);
} }
if (file.size > 5 * 1024 * 1024) { // Validate all files
$q.notify({ for (const file of files) {
type: 'negative', if (!file.type.startsWith('image/')) {
message: 'L\'immagine non può superare i 5MB' $q.notify({
}); type: 'negative',
return; message: `${file.name} non è un'immagine valida`,
});
return;
}
if (file.size > 5 * 1024 * 1024) {
$q.notify({
type: 'negative',
message: `${file.name} supera i 5MB`,
});
return;
}
} }
// Upload // Upload files
uploadingPhotos.value = true;
$q.loading.show({ message: `Caricamento di ${files.length} foto...` });
try { try {
$q.loading.show({ message: 'Caricamento foto...' });
const formData = new FormData(); const formData = new FormData();
formData.append('photo', file); files.forEach(file => {
formData.append('photos', file);
});
const response = await Api.SendReq('/api/trasporti/upload/vehicle-photo', 'POST', formData); const response = await Api.SendReqWithData(
'/api/trasporti/upload/vehicle-photos',
'postFormData',
false,
false,
true,
0,
0,
formData
);
if (response.success) { if (response.success && response.data.urls) {
form.value.photos.push(response.data.url); form.value.photos.push(...response.data.urls);
$q.notify({
type: 'positive',
message: `${files.length} foto caricate con successo`,
icon: 'check_circle',
});
} }
} catch (error: any) { } catch (error: any) {
console.error('Errore upload foto:', error);
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: error.data?.message || error.messageessage || error.message || 'Errore nel caricamento della foto' message:
error.data?.message ||
error.message ||
'Errore nel caricamento delle foto',
}); });
} finally { } finally {
uploadingPhotos.value = false;
$q.loading.hide(); $q.loading.hide();
input.value = ''; input.value = '';
} }
}; };
const removePhoto = (index: number) => { const removePhoto = async (index: number) => {
form.value.photos.splice(index, 1); $q.dialog({
title: 'Rimuovi foto',
message: 'Vuoi rimuovere questa foto?',
cancel: true,
persistent: true,
}).onOk(async () => {
const photoUrl = form.value.photos[index];
// Remove from array
form.value.photos.splice(index, 1);
// If vehicle is being edited, also delete from server
if (isEdit.value) {
try {
await Api.SendReqWithData(
'/api/trasporti/upload/vehicle-photo',
'DELETE',
{ photoUrl }
);
} catch (error) {
console.error('Errore eliminazione foto dal server:', error);
}
}
$q.notify({
type: 'positive',
message: 'Foto rimossa',
icon: 'delete',
});
});
}; };
const loadVehicle = async () => { const loadVehicle = async () => {
@@ -481,13 +594,13 @@ export default defineComponent({
loading.value = true; loading.value = true;
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/veicoli/${route.params.vehicleId}`, `/api/trasporti/driver/vehicles/${route.params.vehicleId}`,
'GET' 'GET'
); );
if (response.success && response.data?.data) { if (response.success && response.data) {
const vehicle = response.data.data; const vehicle = response.data;
form.value = { form.value = {
type: vehicle.type || 'car', type: vehicle.type || 'car',
brand: vehicle.brand || '', brand: vehicle.brand || '',
@@ -500,13 +613,14 @@ export default defineComponent({
features: vehicle.features || [], features: vehicle.features || [],
photos: vehicle.photos || [], photos: vehicle.photos || [],
notes: vehicle.notes || '', notes: vehicle.notes || '',
isDefault: vehicle.isDefault || false isDefault: vehicle.isDefault || false,
}; };
} }
} catch (error: any) { } catch (error: any) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: error.data?.message || error.message || 'Errore nel caricamento del veicolo' message:
error.data?.message || error.message || 'Errore nel caricamento del veicolo',
}); });
router.back(); router.back();
} finally { } finally {
@@ -515,16 +629,27 @@ export default defineComponent({
}; };
const onSubmit = async () => { const onSubmit = async () => {
// Validate required fields
if (!form.value.brand || !form.value.model) {
$q.notify({
type: 'warning',
message: 'Compila tutti i campi obbligatori',
});
return;
}
saving.value = true; saving.value = true;
try { try {
const endpoint = isEdit.value const endpoint = isEdit.value
? `/api/trasporti/veicoli/${route.params.vehicleId}` ? `/api/trasporti/driver/vehicles/${route.params.vehicleId}`
: '/api/trasporti/veicoli'; : '/api/trasporti/driver/vehicles';
const method = isEdit.value ? 'PUT' : 'POST'; const method = isEdit.value ? 'PUT' : 'POST';
const response = await Api.SendReq(endpoint, method, form.value); console.log('--- dati da salvare', form.value);
const response = await Api.SendReqWithData(endpoint, method, form.value);
if (response.success) { if (response.success) {
$q.notify({ $q.notify({
@@ -532,7 +657,7 @@ export default defineComponent({
message: isEdit.value message: isEdit.value
? 'Veicolo aggiornato con successo' ? 'Veicolo aggiornato con successo'
: 'Veicolo aggiunto con successo', : 'Veicolo aggiunto con successo',
icon: 'check_circle' icon: 'check_circle',
}); });
router.push('/trasporti/veicoli'); router.push('/trasporti/veicoli');
@@ -542,7 +667,8 @@ export default defineComponent({
} catch (error: any) { } catch (error: any) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: error.data?.message || error.message || 'Errore durante il salvataggio' message:
error.data?.message || error.message || 'Errore durante il salvataggio',
}); });
} finally { } finally {
saving.value = false; saving.value = false;
@@ -559,6 +685,7 @@ export default defineComponent({
fileInput, fileInput,
loading, loading,
saving, saving,
uploadingPhotos,
isEdit, isEdit,
currentYear, currentYear,
form, form,
@@ -568,11 +695,11 @@ export default defineComponent({
availableFeatures, availableFeatures,
confirmCancel, confirmCancel,
addPhoto, addPhoto,
onPhotoSelected, onPhotosSelected,
removePhoto, removePhoto,
onSubmit onSubmit,
}; };
} },
}); });
</script> </script>
@@ -648,7 +775,7 @@ export default defineComponent({
// Photos // Photos
&__photos { &__photos {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 12px; gap: 12px;
} }
@@ -667,7 +794,23 @@ export default defineComponent({
position: absolute; position: absolute;
top: 4px; top: 4px;
right: 4px; right: 4px;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
}
&__photo-badge {
position: absolute;
bottom: 4px;
left: 4px;
background: #ffc107;
color: #333;
padding: 2px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
} }
&__photo-add { &__photo-add {
@@ -690,14 +833,21 @@ export default defineComponent({
span { span {
font-size: 11px; font-size: 11px;
color: #888; color: #888;
text-align: center;
} }
} }
&__photo-hint { &__photo-hint {
display: flex;
align-items: center;
gap: 6px;
margin: 12px 0 0; margin: 12px 0 0;
font-size: 12px; font-size: 12px;
color: #888; color: #888;
text-align: center;
.q-icon {
color: #667eea;
}
} }
// Type Selector // Type Selector

View File

@@ -266,7 +266,8 @@ export default defineComponent({
// Methods // Methods
const goBack = () => { const goBack = () => {
router.back(); // router.back();
router.push('/trasporti/dashboard');
}; };
const addVehicle = () => { const addVehicle = () => {
@@ -352,8 +353,8 @@ export default defineComponent({
const setDefault = async (vehicle: Vehicle) => { const setDefault = async (vehicle: Vehicle) => {
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/veicoli/${vehicle._id}/default`, `/api/trasporti/driver/vehicles/${vehicle._id}/default`,
'PUT' 'PUT'
); );
@@ -388,8 +389,8 @@ export default defineComponent({
deleting.value = true; deleting.value = true;
try { try {
const response = await Api.SendReq( const response = await Api.SendReqWithData(
`/api/trasporti/veicoli/${vehicleToDelete.value._id}`, `/api/trasporti/driver/vehicles/${vehicleToDelete.value._id}`,
'DELETE' 'DELETE'
); );
@@ -420,10 +421,10 @@ export default defineComponent({
loading.value = true; loading.value = true;
try { try {
const response = await Api.SendReq('/api/trasporti/veicoli', 'GET'); const response = await Api.SendReqWithData('/api/trasporti/driver/vehicles', 'GET');
if (response.success) { if (response.success) {
vehicles.value = response.data?.data || []; vehicles.value = response.data || [];
} }
} catch (error: any) { } catch (error: any) {
$q.notify({ $q.notify({

View File

@@ -52,6 +52,7 @@ export interface Vehicle {
features?: VehicleFeature[]; features?: VehicleFeature[];
isDefault?: boolean; isDefault?: boolean;
isVerified?: boolean; isVerified?: boolean;
photos?: string[];
} }
// ============================================================ // ============================================================
@@ -131,6 +132,11 @@ export interface UserBasic {
name?: string; name?: string;
surname?: string; surname?: string;
email?: string; email?: string;
profile?: {
img?: string;
cell?: string;
Biografia?: string;
};
} }
export interface UserWithProfile extends UserBasic { export interface UserWithProfile extends UserBasic {
@@ -138,6 +144,7 @@ export interface UserWithProfile extends UserBasic {
img?: string; img?: string;
Biografia?: string; Biografia?: string;
Cell?: string; Cell?: string;
cell?: string;
cellVerified?: boolean; cellVerified?: boolean;
driverProfile?: DriverProfile; driverProfile?: DriverProfile;
preferences?: UserPreferences; preferences?: UserPreferences;
@@ -277,6 +284,16 @@ export interface RideRequestContribution {
notes?: string; notes?: string;
} }
export interface RideInfo {
rideId?: string;
departure: string | Location;
destination: string | Location;
departureDate: Date | string;
type?: RideType;
availableSeats?: number;
currentPassengers?: number;
}
export interface RideRequest { export interface RideRequest {
_id: string; _id: string;
idapp: string; idapp: string;
@@ -307,9 +324,11 @@ export interface RideRequest {
feedbackGiven: boolean; feedbackGiven: boolean;
createdAt: Date | string; createdAt: Date | string;
updatedAt: Date | string; updatedAt: Date | string;
// Virtuals // Virtuals & Extra fields
canCancel?: boolean; canCancel?: boolean;
isPending?: boolean; isPending?: boolean;
rideInfo?: RideInfo;
userData?: UserBasic;
} }
// ============================================================ // ============================================================
@@ -321,7 +340,7 @@ export type MessageType = 'text' | 'ride_share' | 'location' | 'image' | 'voice'
export interface LastMessage { export interface LastMessage {
text: string; text: string;
senderId: string; senderId: string | UserBasic;
timestamp: Date | string; timestamp: Date | string;
type: MessageType; type: MessageType;
} }
@@ -335,14 +354,16 @@ export interface Chat {
type: ChatType; type: ChatType;
title?: string; title?: string;
lastMessage?: LastMessage; lastMessage?: LastMessage;
unreadCount?: Map<string, number> | Record<string, number>; unreadCount?: Map<string, number> | Record<string, number> | number;
isActive: boolean; isActive: boolean;
mutedBy?: string[]; mutedBy?: string[];
blockedBy?: string[]; blockedBy?: string[];
deletedBy?: string[];
createdAt: Date | string; createdAt: Date | string;
updatedAt: Date | string; updatedAt: Date | string;
// Extra per UI // Extra per UI
otherParticipant?: UserBasic; otherParticipant?: UserBasic;
rideInfo?: RideInfo;
} }
export interface MessageMetadata { export interface MessageMetadata {
@@ -376,6 +397,7 @@ export interface Message {
chatId: string; chatId: string;
senderId: string | UserBasic; senderId: string | UserBasic;
text?: string; text?: string;
content?: string; // Alias di text
type: MessageType; type: MessageType;
metadata?: MessageMetadata; metadata?: MessageMetadata;
readBy?: MessageReadBy[]; readBy?: MessageReadBy[];
@@ -386,6 +408,7 @@ export interface Message {
deletedAt?: Date | string; deletedAt?: Date | string;
reactions?: MessageReaction[]; reactions?: MessageReaction[];
createdAt: Date | string; createdAt: Date | string;
updatedAt?: Date | string;
} }
// ============================================================ // ============================================================
@@ -675,17 +698,22 @@ export interface RideRequestCounts {
pending: number; pending: number;
accepted: number; accepted: number;
rejected: number; rejected: number;
cancelled: number;
expired: number;
completed: number;
} }
export interface RequestsReceivedResponse { export interface RequestsReceivedResponse {
success: boolean; success: boolean;
data: RideRequest[]; data: {
counts: RideRequestCounts; requests: RideRequest[];
pagination: { counts: RideRequestCounts;
page: number; pagination: {
limit: number; page: number;
total: number; limit: number;
pages: number; total: number;
pages: number;
};
}; };
} }

View File

@@ -19,7 +19,8 @@ async function sendRequest(
const actions = { const actions = {
get: () => Api.get(url, mydata, responsedata), get: () => Api.get(url, mydata, responsedata),
post: () => Api.post(url, mydata, responsedata, options), post: () => Api.post(url, mydata, responsedata, options),
postformdata: () => Api.postFormData(url, myformdata, responsedata, options), // ✅ Aggiunto options postformdata: () => Api.postFormData(url, myformdata, responsedata, options),
putformdata: () => Api.putFormData(url, myformdata, responsedata, options),
delete: () => Api.Delete(url, mydata, responsedata), delete: () => Api.Delete(url, mydata, responsedata),
put: () => Api.put(url, mydata, responsedata), put: () => Api.put(url, mydata, responsedata),
patch: () => Api.patch(url, mydata, responsedata), patch: () => Api.patch(url, mydata, responsedata),

View File

@@ -130,7 +130,7 @@ async function Request(
if (tools.isDebug()) if (tools.isDebug())
console.log('Axios Request', path, type, tools.notshowPwd(payload)); console.log('Axios Request', path, type, tools.notshowPwd(payload));
const isFormData = type === 'postFormData'; const isFormData = type === 'postFormData' || type === 'putFormData';
let config: AxiosRequestConfig = { let config: AxiosRequestConfig = {
baseURL, baseURL,
timeout: 60000, timeout: 60000,
@@ -222,6 +222,8 @@ async function Request(
});*/ });*/
} else if (type === 'postFormData') { } else if (type === 'postFormData') {
response = await axiosInstance.post(path, payload, config); response = await axiosInstance.post(path, payload, config);
} else if (type === 'putFormData') {
response = await axiosInstance.put(path, payload, config);
} else { } else {
throw new Error(`Unsupported request type: ${type}`); throw new Error(`Unsupported request type: ${type}`);
} }

View File

@@ -56,6 +56,13 @@ export const Api = {
return await Request('postFormData', path, payload, responsedata, options); return await Request('postFormData', path, payload, responsedata, options);
}, },
async putFormData(path: string, payload?: any, responsedata?: any, options?: any) {
const globalStore = useGlobalStore();
globalStore.connData.downloading_server = 1;
globalStore.connData.uploading_server = 1;
return await Request('putFormData', path, payload, responsedata, options);
},
async get(path: string, payload?: any, responsedata?: any) { async get(path: string, payload?: any, responsedata?: any) {
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
globalStore.connData.downloading_server = 1; globalStore.connData.downloading_server = 1;
@@ -336,6 +343,34 @@ export const Api = {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
}, },
async SendReqWithData(
url: string,
method: string,
mydata?: any,
setAuthToken = false,
evitaloop = false,
retryCount = 1,
retryDelay = 5000,
myformdata: any = null,
responsedata: any = null,
options: any = null
) {
const ret = await this.SendReq(
url,
method,
mydata,
setAuthToken,
evitaloop,
retryCount,
retryDelay,
myformdata,
responsedata,
options
);
return ret?.data ? ret.data : ret;
},
// Funzione che gestisce la chiamata con retry // Funzione che gestisce la chiamata con retry
async SendReq( async SendReq(
url: string, url: string,