- Trasporti- Passo 2
This commit is contained in:
@@ -4,10 +4,15 @@
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
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 {
|
||||
flex-direction: row-reverse;
|
||||
align-self: flex-end;
|
||||
|
||||
.message-bubble__bubble {
|
||||
background: linear-gradient(135deg, var(--q-primary), var(--q-primary-dark, #1565c0));
|
||||
@@ -19,6 +24,10 @@
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.message-bubble__edited {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.message-bubble__footer {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -26,54 +35,72 @@
|
||||
.message-bubble__reactions {
|
||||
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 {
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
&__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);
|
||||
align-self: center;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
// Avatar
|
||||
&__avatar {
|
||||
background: linear-gradient(135deg, var(--q-secondary), var(--q-primary));
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
background: #e0e0e0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// Content wrapper
|
||||
&__content {
|
||||
max-width: 70%;
|
||||
min-width: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// Sender name
|
||||
&__sender {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--q-primary);
|
||||
margin-bottom: 4px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
// Reply preview
|
||||
&__reply {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 4px;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 12px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
margin-bottom: 4px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
@@ -84,13 +111,14 @@
|
||||
width: 3px;
|
||||
background: var(--q-primary);
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__reply-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__reply-sender {
|
||||
@@ -101,63 +129,76 @@
|
||||
|
||||
&__reply-text {
|
||||
font-size: 12px;
|
||||
color: var(--q-grey-7);
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// Bubble principale
|
||||
&__bubble {
|
||||
background: #f0f0f0;
|
||||
border-radius: 18px 18px 18px 4px;
|
||||
background: white;
|
||||
padding: 10px 14px;
|
||||
border-radius: 18px 18px 18px 4px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
// Testo messaggio
|
||||
&__text {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
color: var(--q-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Messaggio eliminato
|
||||
&__deleted {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
color: var(--q-grey-6);
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
// Footer
|
||||
&__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 11px;
|
||||
color: var(--q-grey-6);
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&__edited {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
color: var(--q-grey-5);
|
||||
}
|
||||
|
||||
&__status {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
// Reactions
|
||||
&__reactions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -168,43 +209,61 @@
|
||||
&__reaction {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px 8px;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Menu button
|
||||
&__menu-btn {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
transition: opacity 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover &__menu-btn {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 8px;
|
||||
background: rgba(var(--q-primary-rgb), 0.08);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 4px;
|
||||
padding: 4px 0;
|
||||
|
||||
&--success {
|
||||
background: rgba(var(--q-positive-rgb), 0.08);
|
||||
.message-bubble__special-title {
|
||||
color: var(--q-positive);
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
background: rgba(var(--q-negative-rgb), 0.08);
|
||||
.message-bubble__special-title {
|
||||
color: var(--q-negative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,28 +279,30 @@
|
||||
}
|
||||
|
||||
&__special-text {
|
||||
font-size: 13px;
|
||||
color: var(--q-grey-8);
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
// Location message
|
||||
&__location {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
&__location-preview {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
|
||||
height: 100px;
|
||||
background: #e8f5e9;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// Ride share
|
||||
&__ride-share {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -250,50 +311,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode
|
||||
.body--dark {
|
||||
// Responsive
|
||||
@media (max-width: 600px) {
|
||||
.message-bubble {
|
||||
&__system {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
max-width: 90%;
|
||||
padding: 0 12px;
|
||||
|
||||
&__bubble {
|
||||
background: #2d2d2d;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
&--own .message-bubble__bubble {
|
||||
background: linear-gradient(135deg, var(--q-primary), #1565c0);
|
||||
&__text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__reply {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
|
||||
&: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%;
|
||||
&__menu-btn {
|
||||
opacity: 1; // Sempre visibile su mobile
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,14 +29,24 @@ export default defineComponent({
|
||||
replyTo: {
|
||||
type: Object as PropType<Message | 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 }) {
|
||||
// Sender info
|
||||
const sender = computed(() => {
|
||||
// Sender info - USA LA PROP sender SE DISPONIBILE
|
||||
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') {
|
||||
return props.message.senderId as UserBasic;
|
||||
}
|
||||
@@ -44,14 +54,14 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const senderName = computed(() => {
|
||||
if (sender.value?.name) {
|
||||
return `${sender.value.name} ${sender.value.surname?.[0] || ''}`.trim();
|
||||
if (senderData.value?.name) {
|
||||
return `${senderData.value.name} ${senderData.value.surname?.[0] || ''}`.trim();
|
||||
}
|
||||
return sender.value?.username || 'Utente';
|
||||
return senderData.value?.username || 'Utente';
|
||||
});
|
||||
|
||||
const senderImg = computed(() => {
|
||||
return (sender.value as any)?.profile?.img;
|
||||
return (senderData.value as any)?.profile?.img;
|
||||
});
|
||||
|
||||
const senderInitials = computed(() => {
|
||||
@@ -148,7 +158,7 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
return {
|
||||
sender,
|
||||
senderData,
|
||||
senderName,
|
||||
senderImg,
|
||||
senderInitials,
|
||||
|
||||
@@ -238,9 +238,9 @@ export default defineComponent({
|
||||
// Methods
|
||||
const loadRecentTripsFromServer = async () => {
|
||||
try {
|
||||
const response = await Api.SendReq('/api/trasporti/cities/recent', 'GET');
|
||||
if (response.success && response.data?.data?.cities) {
|
||||
serverRecentTrips.value = response.data.data.cities;
|
||||
const response = await Api.SendReqWithData('/api/trasporti/cities/recent', 'GET');
|
||||
if (response.success && response.data?.cities) {
|
||||
serverRecentTrips.value = response.data.cities;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading recent trips:', error);
|
||||
|
||||
@@ -72,10 +72,10 @@ export default defineComponent({
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await Api.SendReq('/api/trasporti/widget/data', 'GET', {});
|
||||
const response = await Api.SendReqWithData('/api/trasporti/widget/data', 'GET', {});
|
||||
|
||||
if (response.success) {
|
||||
const data: WidgetData = response.data.data;
|
||||
const data: WidgetData = response.data;
|
||||
|
||||
stats.value = data.stats || { offers: 0, requests: 0, matches: 0 };
|
||||
recentRides.value = data.recentRides || [];
|
||||
@@ -95,10 +95,10 @@ export default defineComponent({
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const response = await Api.SendReq('/api/trasporti/stats/summary', 'GET');
|
||||
const response = await Api.SendReqWithData('/api/trasporti/stats/summary', 'GET');
|
||||
|
||||
if (response.success) {
|
||||
stats.value = response.data.data;
|
||||
stats.value = response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Errore caricamento stats:', error);
|
||||
|
||||
16
src/modules/trasporti/composables/useAuth.ts
Normal file
16
src/modules/trasporti/composables/useAuth.ts
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { Api } from '@api';
|
||||
import type { Chat, Message } from '../types/trasporti.types';
|
||||
import { tools } from 'app/src/store/Modules/tools';
|
||||
|
||||
// ============================================================
|
||||
// STATE
|
||||
@@ -21,13 +22,27 @@ const error = ref<string | null>(null);
|
||||
const onlineUsers = 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
|
||||
// ============================================================
|
||||
|
||||
export function useChat() {
|
||||
// ID app per trasporti
|
||||
const IDAPP = 'trasporti';
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// COMPUTED
|
||||
@@ -64,16 +79,16 @@ export function useChat() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
`/api/trasporti/chats?idapp=${IDAPP}&page=${page}&limit=${limit}`,
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/chats?page=${page}&limit=${limit}`,
|
||||
'GET'
|
||||
);
|
||||
|
||||
if (response.success && response.data.data) {
|
||||
chats.value = response.data.data;
|
||||
if (response.success && response.data) {
|
||||
chats.value = response.data;
|
||||
|
||||
// Calcola unread totale
|
||||
totalUnreadCount.value = response.data.data.reduce(
|
||||
totalUnreadCount.value = response.data.reduce(
|
||||
(sum: number, chat: any) => sum + (chat.unreadCount || 0),
|
||||
0
|
||||
);
|
||||
@@ -96,21 +111,16 @@ export function useChat() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
'/api/trasporti/chats/direct',
|
||||
'POST',
|
||||
{
|
||||
idapp: IDAPP,
|
||||
otherUserId,
|
||||
rideId
|
||||
}
|
||||
);
|
||||
const response = await Api.SendReqWithData('/api/trasporti/chats/direct', 'POST', {
|
||||
otherUserId,
|
||||
rideId,
|
||||
});
|
||||
|
||||
if (response.success && response.data.data) {
|
||||
currentChat.value = response.data.data;
|
||||
if (response.success && response.data) {
|
||||
currentChat.value = response.data;
|
||||
|
||||
// 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) {
|
||||
chats.value.unshift(currentChat.value);
|
||||
}
|
||||
@@ -140,13 +150,10 @@ export function useChat() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
`/api/trasporti/chats/${chatId}`,
|
||||
'GET'
|
||||
);
|
||||
const response = await Api.SendReqWithData(`/api/trasporti/chats/${chatId}`, 'GET');
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
currentChat.value = response.data?.data;
|
||||
if (response.success && response.data) {
|
||||
currentChat.value = response.data;
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -168,48 +175,86 @@ export function useChat() {
|
||||
const response = await fetchMessages(chatId, options);
|
||||
return response.data || [];
|
||||
};
|
||||
const fetchMessages = async (
|
||||
chatId: string,
|
||||
options: FetchMessagesOptions = {}
|
||||
) => {
|
||||
try {
|
||||
loadingMessages.value = true;
|
||||
error.value = null;
|
||||
|
||||
/**
|
||||
* 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: tools.getIdApp() });
|
||||
|
||||
const params = new URLSearchParams({ idapp: IDAPP });
|
||||
if (options?.before) params.append('before', options.before);
|
||||
if (options?.after) params.append('after', options.after);
|
||||
if (options?.limit) params.append('limit', options.limit.toString());
|
||||
|
||||
const response = await Api.SendReq(
|
||||
`/api/trasporti/chats/${chatId}/messages?${params.toString()}`,
|
||||
'GET'
|
||||
);
|
||||
|
||||
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;
|
||||
// ✅ Determina i parametri in base al tipo di caricamento
|
||||
if (options.loadOlder && messages.value.length > 0) {
|
||||
// Carica messaggi più vecchi del primo messaggio attuale
|
||||
const oldestMessage = messages.value[0];
|
||||
params.append('before', oldestMessage.createdAt);
|
||||
} else if (options.loadNewer && messages.value.length > 0) {
|
||||
// Carica messaggi più recenti dell'ultimo messaggio attuale
|
||||
const newestMessage = messages.value[messages.value.length - 1];
|
||||
params.append('after', newestMessage.createdAt);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
@@ -231,20 +276,19 @@ export function useChat() {
|
||||
// Supporta sia content che text
|
||||
const messageText = payload.content || payload.text || '';
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/chats/${chatId}/messages`,
|
||||
'POST',
|
||||
{
|
||||
idapp: IDAPP,
|
||||
text: messageText,
|
||||
type: payload.type || 'text',
|
||||
metadata: payload.metadata,
|
||||
replyTo: payload.replyTo
|
||||
replyTo: payload.replyTo,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
const newMessage = response.data?.data;
|
||||
if (response.success && response.data) {
|
||||
const newMessage = response.data;
|
||||
messages.value.push(newMessage);
|
||||
|
||||
// Aggiorna lastMessage nella chat
|
||||
@@ -253,7 +297,7 @@ export function useChat() {
|
||||
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
error.value = err.message || 'Errore nell\'invio del messaggio';
|
||||
error.value = err.message || "Errore nell'invio del messaggio";
|
||||
throw err;
|
||||
} finally {
|
||||
sending.value = false;
|
||||
@@ -264,13 +308,13 @@ export function useChat() {
|
||||
* Aggiorna lastMessage nella chat locale
|
||||
*/
|
||||
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) {
|
||||
chats.value[chatIndex].lastMessage = {
|
||||
text: message.text || '',
|
||||
senderId: message.senderId as any,
|
||||
timestamp: message.createdAt,
|
||||
type: message.type || 'text'
|
||||
type: message.type || 'text',
|
||||
};
|
||||
chats.value[chatIndex].updatedAt = message.createdAt;
|
||||
}
|
||||
@@ -280,7 +324,7 @@ export function useChat() {
|
||||
text: message.text || '',
|
||||
senderId: message.senderId as any,
|
||||
timestamp: message.createdAt,
|
||||
type: message.type || 'text'
|
||||
type: message.type || 'text',
|
||||
};
|
||||
currentChat.value.updatedAt = message.createdAt;
|
||||
}
|
||||
@@ -291,14 +335,14 @@ export function useChat() {
|
||||
*/
|
||||
const markAsRead = async (chatId: string) => {
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/chats/${chatId}/read`,
|
||||
'PUT'
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
// Aggiorna contatore locale
|
||||
const chatIndex = chats.value.findIndex(c => c._id === chatId);
|
||||
const chatIndex = chats.value.findIndex((c) => c._id === chatId);
|
||||
if (chatIndex !== -1) {
|
||||
const unread = chats.value[chatIndex].unreadCount || 0;
|
||||
totalUnreadCount.value = Math.max(0, totalUnreadCount.value - unread);
|
||||
@@ -321,13 +365,13 @@ export function useChat() {
|
||||
*/
|
||||
const fetchUnreadCount = async () => {
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/chats/unread/count?idapp=${IDAPP}`,
|
||||
'GET'
|
||||
);
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
totalUnreadCount.value = response.data?.data.total || 0;
|
||||
if (response.success && response.data) {
|
||||
totalUnreadCount.value = response.data.total || 0;
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -341,13 +385,13 @@ export function useChat() {
|
||||
*/
|
||||
const deleteMessage = async (chatId: string, messageId: string) => {
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/chats/${chatId}/messages/${messageId}`,
|
||||
'DELETE'
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
const index = messages.value.findIndex(m => m._id === messageId);
|
||||
const index = messages.value.findIndex((m) => m._id === messageId);
|
||||
if (index !== -1) {
|
||||
messages.value[index].isDeleted = true;
|
||||
messages.value[index].text = '[Messaggio eliminato]';
|
||||
@@ -356,7 +400,7 @@ export function useChat() {
|
||||
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
error.value = err.message || 'Errore nell\'eliminazione del messaggio';
|
||||
error.value = err.message || "Errore nell'eliminazione del messaggio";
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
@@ -366,7 +410,7 @@ export function useChat() {
|
||||
*/
|
||||
const toggleBlockChat = async (chatId: string, block: boolean) => {
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/chats/${chatId}/block`,
|
||||
'PUT',
|
||||
{ block }
|
||||
@@ -384,7 +428,7 @@ export function useChat() {
|
||||
*/
|
||||
const toggleMuteChat = async (chatId: string, mute: boolean) => {
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/chats/${chatId}/mute`,
|
||||
'PUT',
|
||||
{ mute }
|
||||
@@ -473,6 +517,64 @@ export function useChat() {
|
||||
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
|
||||
// ------------------------------------------------------------
|
||||
@@ -489,12 +591,16 @@ export function useChat() {
|
||||
error,
|
||||
onlineUsers,
|
||||
typingUsers,
|
||||
isPolling, // AGGIUNGI
|
||||
|
||||
|
||||
// Computed
|
||||
hasChats,
|
||||
hasUnread,
|
||||
sortedChats,
|
||||
sortedMessages,
|
||||
hasOlderMessages, // AGGIUNGI
|
||||
hasNewerMessages, // AGGIUNGI
|
||||
|
||||
// API Methods
|
||||
fetchChats,
|
||||
@@ -509,6 +615,14 @@ export function useChat() {
|
||||
toggleBlockChat,
|
||||
toggleMuteChat,
|
||||
deleteMessage,
|
||||
loadOlderMessages, // AGGIUNGI
|
||||
loadNewerMessages, // AGGIUNGI
|
||||
addNewMessage, // AGGIUNGI
|
||||
|
||||
// Polling
|
||||
startPolling, // AGGIUNGI
|
||||
stopPolling, // AGGIUNGI
|
||||
|
||||
|
||||
// Real-time (placeholder)
|
||||
sendTyping,
|
||||
@@ -519,6 +633,6 @@ export function useChat() {
|
||||
formatMessageTime,
|
||||
openChat,
|
||||
clearState,
|
||||
closeCurrentChat
|
||||
closeCurrentChat,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,13 +55,13 @@ export function useCitySuggestions() {
|
||||
lastQuery.value = query;
|
||||
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/cities/suggestions?q=${encodeURIComponent(query)}`,
|
||||
'GET'
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
suggestions.value = response.data?.data.suggestions || [];
|
||||
suggestions.value = response.data.suggestions || [];
|
||||
} else {
|
||||
error.value = response.message || 'Errore nel caricamento dei suggerimenti';
|
||||
suggestions.value = [];
|
||||
|
||||
@@ -68,13 +68,13 @@ export function useContribTypes() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
'/api/trasporti/contrib-types',
|
||||
'GET'
|
||||
) as ApiResponse<ContribType[]>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
contribTypes.value = response.data.data;
|
||||
if (response.success && response.data) {
|
||||
contribTypes.value = response.data;
|
||||
fetched = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ import type {
|
||||
DriverProfile,
|
||||
Vehicle,
|
||||
UserPreferences,
|
||||
DriverPublicProfile,
|
||||
ApiResponse
|
||||
} from '../types';
|
||||
DriverPublicProfile
|
||||
} from '../types/trasporti.types';
|
||||
|
||||
// ============================================================
|
||||
// STATE
|
||||
@@ -53,18 +52,18 @@ export function useDriverProfile() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
`/api/trasporti/driver/${userId}`,
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/driver/user/${userId}`,
|
||||
'GET'
|
||||
) as ApiResponse<DriverPublicProfile>;
|
||||
);
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
driverProfile.value = response.data.data;
|
||||
if (response.success && response.data) {
|
||||
driverProfile.value = response.data;
|
||||
}
|
||||
|
||||
return response;
|
||||
} 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;
|
||||
} finally {
|
||||
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 {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
'/api/trasporti/driver/profile',
|
||||
'PUT',
|
||||
{ driverProfile: profileData }
|
||||
) as ApiResponse<{ driverProfile: DriverProfile; preferences: UserPreferences }>;
|
||||
{
|
||||
idapp: 'trasporti',
|
||||
...data
|
||||
}
|
||||
);
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
myDriverProfile.value = response.data?.data.driverProfile;
|
||||
myPreferences.value = response.data?.data.preferences;
|
||||
if (response.success && response.data) {
|
||||
// Il backend ritorna user.profile che contiene driverProfile e preferences
|
||||
if (response.data.driverProfile) {
|
||||
myDriverProfile.value = response.data.driverProfile;
|
||||
}
|
||||
if (response.data.preferences) {
|
||||
myPreferences.value = response.data.preferences;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
} 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;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -100,53 +110,33 @@ export function useDriverProfile() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Aggiorna le mie preferenze
|
||||
* Aggiorna solo le preferenze
|
||||
*/
|
||||
const updatePreferences = async (preferences: Partial<UserPreferences>) => {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
return await updateDriverProfile({ preferences });
|
||||
};
|
||||
|
||||
/**
|
||||
* Aggiungi veicolo
|
||||
*/
|
||||
const addVehicle = async (vehicle: Omit<Vehicle, '_id'>) => {
|
||||
const addVehicle = async (vehicleData: Omit<Vehicle, '_id'>) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
'/api/trasporti/driver/vehicles',
|
||||
'POST',
|
||||
{ vehicle }
|
||||
) as ApiResponse<Vehicle[]>;
|
||||
{ vehicle: vehicleData }
|
||||
);
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
myVehicles.value = response.data.data;
|
||||
if (response.success && response.data) {
|
||||
myVehicles.value = response.data;
|
||||
}
|
||||
|
||||
return response;
|
||||
} 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;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -156,24 +146,24 @@ export function useDriverProfile() {
|
||||
/**
|
||||
* Aggiorna veicolo
|
||||
*/
|
||||
const updateVehicle = async (vehicleId: string, vehicle: Partial<Vehicle>) => {
|
||||
const updateVehicle = async (vehicleId: string, vehicleData: Partial<Vehicle>) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/driver/vehicles/${vehicleId}`,
|
||||
'PUT',
|
||||
{ vehicle }
|
||||
) as ApiResponse<Vehicle[]>;
|
||||
{ vehicle: vehicleData }
|
||||
);
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
myVehicles.value = response.data.data;
|
||||
if (response.success && response.data) {
|
||||
myVehicles.value = response.data;
|
||||
}
|
||||
|
||||
return response;
|
||||
} 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;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -188,10 +178,10 @@ export function useDriverProfile() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/driver/vehicles/${vehicleId}`,
|
||||
'DELETE'
|
||||
) as ApiResponse<void>;
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
myVehicles.value = myVehicles.value.filter(v => v._id !== vehicleId);
|
||||
@@ -199,7 +189,7 @@ export function useDriverProfile() {
|
||||
|
||||
return response;
|
||||
} 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;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -214,10 +204,10 @@ export function useDriverProfile() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/driver/vehicles/${vehicleId}/default`,
|
||||
'POST'
|
||||
) as ApiResponse<void>;
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
// Aggiorna localmente
|
||||
@@ -229,7 +219,7 @@ export function useDriverProfile() {
|
||||
|
||||
return response;
|
||||
} 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;
|
||||
} finally {
|
||||
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) => {
|
||||
if (user?.profile?.driverProfile) {
|
||||
|
||||
@@ -59,14 +59,14 @@ export function useFeedback() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
'/api/trasporti/feedback',
|
||||
'POST',
|
||||
feedbackData
|
||||
) as ApiResponse<Feedback>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
myGivenFeedback.value.unshift(response.data?.data);
|
||||
if (response.success && response.data) {
|
||||
myGivenFeedback.value.unshift(response.data);
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -91,7 +91,7 @@ export function useFeedback() {
|
||||
if (options?.page) queryParams.append('page', options.page.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()}`,
|
||||
'GET'
|
||||
) as ApiResponse<{
|
||||
@@ -100,10 +100,10 @@ export function useFeedback() {
|
||||
distribution: RatingDistribution[];
|
||||
}>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
feedbacks.value = response.data?.data.feedbacks;
|
||||
currentUserStats.value = response.data?.data.stats;
|
||||
ratingDistribution.value = response.data?.data.distribution;
|
||||
if (response.success && response.data) {
|
||||
feedbacks.value = response.data.feedbacks;
|
||||
currentUserStats.value = response.data.stats;
|
||||
ratingDistribution.value = response.data.distribution;
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -120,7 +120,7 @@ export function useFeedback() {
|
||||
*/
|
||||
const fetchUserStats = async (userId: string) => {
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/feedback/user/${userId}/stats`,
|
||||
'GET'
|
||||
) as ApiResponse<{
|
||||
@@ -129,8 +129,8 @@ export function useFeedback() {
|
||||
commonTags: { _id: FeedbackTag; count: number }[];
|
||||
}>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
currentUserStats.value = response.data?.data.stats;
|
||||
if (response.success && response.data) {
|
||||
currentUserStats.value = response.data.stats;
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -148,13 +148,13 @@ export function useFeedback() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/feedback/ride/${rideId}`,
|
||||
'GET'
|
||||
) as ApiResponse<Feedback[]>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
feedbacks.value = response.data.data;
|
||||
if (response.success && response.data) {
|
||||
feedbacks.value = response.data;
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -171,12 +171,12 @@ export function useFeedback() {
|
||||
*/
|
||||
const canLeaveFeedback = async (rideId: string, toUserId: string) => {
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/feedback/can-leave/${rideId}/${toUserId}`,
|
||||
'GET'
|
||||
) as ApiResponse<{ canLeave: boolean; reason?: string }>;
|
||||
|
||||
return response.data?.data;
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
console.error('Errore verifica feedback:', err);
|
||||
return { canLeave: false, reason: 'Errore nella verifica' };
|
||||
@@ -194,14 +194,14 @@ export function useFeedback() {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (role) queryParams.append('role', role);
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/feedback/my/received?${queryParams.toString()}`,
|
||||
'GET'
|
||||
) as ApiResponse<{ feedbacks: Feedback[]; stats: FeedbackStats }>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
myReceivedFeedback.value = response.data?.data.feedbacks;
|
||||
currentUserStats.value = response.data?.data.stats;
|
||||
if (response.success && response.data) {
|
||||
myReceivedFeedback.value = response.data.feedbacks;
|
||||
currentUserStats.value = response.data.stats;
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -221,13 +221,13 @@ export function useFeedback() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
'/api/trasporti/feedback/my/given',
|
||||
'GET'
|
||||
) as PaginatedResponse<Feedback>;
|
||||
|
||||
if (response.success) {
|
||||
myGivenFeedback.value = response.data.data;
|
||||
myGivenFeedback.value = response.data;
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -247,17 +247,17 @@ export function useFeedback() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/feedback/${feedbackId}/response`,
|
||||
'POST',
|
||||
{ text }
|
||||
) as ApiResponse<Feedback>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
if (response.success && response.data) {
|
||||
// Aggiorna nella lista
|
||||
const index = myReceivedFeedback.value.findIndex(f => f._id === feedbackId);
|
||||
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) => {
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/feedback/${feedbackId}/report`,
|
||||
'POST',
|
||||
{ reason }
|
||||
@@ -293,15 +293,15 @@ export function useFeedback() {
|
||||
*/
|
||||
const markAsHelpful = async (feedbackId: string) => {
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/feedback/${feedbackId}/helpful`,
|
||||
'POST'
|
||||
) as ApiResponse<{ helpfulCount: number }>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
if (response.success && response.data) {
|
||||
const feedback = feedbacks.value.find(f => f._id === feedbackId);
|
||||
if (feedback && feedback.helpful) {
|
||||
feedback.helpful.count = response.data?.data.helpfulCount;
|
||||
feedback.helpful.count = response.data.helpfulCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ export function useRealtimeChat() {
|
||||
|
||||
// Simula utenti online (in produzione questi dati verrebbero dal server)
|
||||
if (currentChat.value?.participants) {
|
||||
currentChat.value.participants.forEach(participant => {
|
||||
currentChat.value.participants.forEach((participant: any) => {
|
||||
if (participant._id) {
|
||||
simulateUserOnline(participant._id);
|
||||
}
|
||||
@@ -127,7 +127,7 @@ export function useRealtimeChat() {
|
||||
console.log(`Typing in chat: ${chatId}`);
|
||||
|
||||
// In produzione:
|
||||
// await Api.SendReq(`/api/trasporti/chats/${chatId}/typing`, 'POST');
|
||||
// await Api.SendReqWithData(`/api/trasporti/chats/${chatId}/typing`, 'POST');
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -69,15 +69,15 @@ export function useRideRequests() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
'/api/trasporti/requests',
|
||||
'POST',
|
||||
requestData,
|
||||
false, true
|
||||
) as ApiResponse<{ request: RideRequest; chatId: string }>;
|
||||
|
||||
if (response.success && response.data.data) {
|
||||
sentRequests.value.unshift(response.data.data.request);
|
||||
if (response.success && response.data) {
|
||||
sentRequests.value.unshift(response.data.request);
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -102,15 +102,15 @@ export function useRideRequests() {
|
||||
queryParams.append('page', pagination.page.toString());
|
||||
queryParams.append('limit', pagination.limit.toString());
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/requests/received?${queryParams.toString()}`,
|
||||
'GET'
|
||||
) as RequestsReceivedResponse;
|
||||
|
||||
if (response.success) {
|
||||
receivedRequests.value = response.data.data;
|
||||
requestCounts.value = response.data?.data.counts;
|
||||
Object.assign(pagination, response.data?.data.pagination);
|
||||
receivedRequests.value = response.data.requests;
|
||||
requestCounts.value = response.data.counts;
|
||||
Object.assign(pagination, response.data.pagination);
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -135,13 +135,13 @@ export function useRideRequests() {
|
||||
queryParams.append('page', pagination.page.toString());
|
||||
queryParams.append('limit', pagination.limit.toString());
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/requests/sent?${queryParams.toString()}`,
|
||||
'GET'
|
||||
) as PaginatedResponse<RideRequest>;
|
||||
|
||||
if (response.success) {
|
||||
sentRequests.value = response.data.data;
|
||||
sentRequests.value = response.data;
|
||||
Object.assign(pagination, response?.data.pagination);
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ export function useRideRequests() {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (status) queryParams.append('status', status);
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/requests/ride/${rideId}?${queryParams.toString()}`,
|
||||
'GET'
|
||||
) as ApiResponse<RideRequest[]>;
|
||||
@@ -187,13 +187,13 @@ export function useRideRequests() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/requests/${requestId}`,
|
||||
'GET'
|
||||
) as ApiResponse<RideRequest>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
currentRequest.value = response.data.data;
|
||||
if (response.success && response.data) {
|
||||
currentRequest.value = response.data;
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -213,15 +213,15 @@ export function useRideRequests() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/requests/${requestId}/accept`,
|
||||
'POST',
|
||||
{ responseMessage }
|
||||
) as ApiResponse<RideRequest>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
if (response.success && response.data) {
|
||||
// Aggiorna nella lista
|
||||
updateRequestInList(requestId, response.data?.data);
|
||||
updateRequestInList(requestId, response.data);
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -241,14 +241,14 @@ export function useRideRequests() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/requests/${requestId}/reject`,
|
||||
'POST',
|
||||
{ responseMessage }
|
||||
) as ApiResponse<RideRequest>;
|
||||
|
||||
if (response.success && response.dat?.data) {
|
||||
updateRequestInList(requestId, response.data?.data);
|
||||
updateRequestInList(requestId, response.data);
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -268,14 +268,14 @@ export function useRideRequests() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = await Api.SendReq(
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/requests/${requestId}/cancel`,
|
||||
'POST',
|
||||
{ reason }
|
||||
) as ApiResponse<RideRequest>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
updateRequestInList(requestId, response.data?.data);
|
||||
if (response.success && response.data) {
|
||||
updateRequestInList(requestId, response.data);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -116,14 +116,14 @@ export function useRides() {
|
||||
if (filters.passingThrough)
|
||||
queryParams.append('passingThrough', filters.passingThrough);
|
||||
|
||||
const response = (await Api.SendReq(
|
||||
const response = (await Api.SendReqWithData(
|
||||
`/api/trasporti/rides?${queryParams.toString()}`,
|
||||
'GET'
|
||||
)) as PaginatedResponse<Ride>;
|
||||
|
||||
if (response.success) {
|
||||
// ✅ 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) {
|
||||
rides.value = newRides;
|
||||
@@ -166,14 +166,14 @@ export function useRides() {
|
||||
queryParams.append('page', pagination.page.toString());
|
||||
queryParams.append('limit', pagination.limit.toString());
|
||||
|
||||
const response = (await Api.SendReq(
|
||||
const response = (await Api.SendReqWithData(
|
||||
`/api/trasporti/rides/search?${queryParams.toString()}`,
|
||||
'GET'
|
||||
)) as PaginatedResponse<Ride>;
|
||||
|
||||
if (response.success) {
|
||||
// ✅ 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) {
|
||||
Object.assign(pagination, response?.data.pagination);
|
||||
@@ -198,13 +198,13 @@ export function useRides() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = (await Api.SendReq(
|
||||
const response = (await Api.SendReqWithData(
|
||||
`/api/trasporti/rides/${rideId}`,
|
||||
'GET'
|
||||
)) as ApiResponse<Ride>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
currentRide.value = response.data.data;
|
||||
if (response.success && response.data) {
|
||||
currentRide.value = response.data;
|
||||
} else {
|
||||
throw new Error('Viaggio non trovato');
|
||||
}
|
||||
@@ -226,16 +226,16 @@ export function useRides() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = (await Api.SendReq(
|
||||
const response = (await Api.SendReqWithData(
|
||||
'/api/trasporti/rides',
|
||||
'POST',
|
||||
rideData
|
||||
)) as ApiResponse<Ride>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
if (response.success && response.data) {
|
||||
// Aggiungi in testa alla lista
|
||||
rides.value.unshift(response.data?.data);
|
||||
currentRide.value = response.data.data;
|
||||
rides.value.unshift(response.data);
|
||||
currentRide.value = response.data;
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -255,20 +255,20 @@ export function useRides() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = (await Api.SendReq(
|
||||
const response = (await Api.SendReqWithData(
|
||||
`/api/trasporti/rides/${rideId}`,
|
||||
'PUT',
|
||||
updateData
|
||||
)) as ApiResponse<Ride>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
if (response.success && response.data) {
|
||||
// Aggiorna nella lista
|
||||
const index = rides.value.findIndex((r) => r._id === rideId);
|
||||
if (index !== -1) {
|
||||
rides.value[index] = response.data.data;
|
||||
rides.value[index] = response.data;
|
||||
}
|
||||
if (currentRide.value?._id === rideId) {
|
||||
currentRide.value = response.data.data;
|
||||
currentRide.value = response.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ export function useRides() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = (await Api.SendReq(`/api/trasporti/rides/${rideId}`, 'DELETE', {
|
||||
const response = (await Api.SendReqWithData(`/api/trasporti/rides/${rideId}`, 'DELETE', {
|
||||
reason,
|
||||
})) as ApiResponse<void>;
|
||||
|
||||
@@ -318,18 +318,18 @@ export function useRides() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const response = (await Api.SendReq(
|
||||
const response = (await Api.SendReqWithData(
|
||||
`/api/trasporti/rides/${rideId}/complete`,
|
||||
'POST'
|
||||
)) as ApiResponse<Ride>;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
if (response.success && response.data) {
|
||||
const index = rides.value.findIndex((r) => r._id === rideId);
|
||||
if (index !== -1) {
|
||||
rides.value[index] = response.data.data;
|
||||
rides.value[index] = response.data;
|
||||
}
|
||||
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('limit', pagination.limit.toString());
|
||||
|
||||
const response = (await Api.SendReq(
|
||||
const response = (await Api.SendReqWithData(
|
||||
`/api/trasporti/rides/my?${queryParams.toString()}`,
|
||||
'GET'
|
||||
)) as MyRidesResponse;
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
myRides.all = response.data.data.all;
|
||||
myRides.upcoming = response.data.data.upcoming;
|
||||
myRides.past = response.data.data.past;
|
||||
if (response.success && response.data) {
|
||||
myRides.all = response.data.all;
|
||||
myRides.upcoming = response.data.upcoming;
|
||||
myRides.past = response.data.past;
|
||||
Object.assign(pagination, response?.data.pagination);
|
||||
}
|
||||
|
||||
@@ -387,13 +387,13 @@ export function useRides() {
|
||||
*/
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = (await Api.SendReq(
|
||||
const response = (await Api.SendReqWithData(
|
||||
'/api/trasporti/rides/stats',
|
||||
'GET'
|
||||
)) as ApiResponse<RidesStatsResponse>;
|
||||
|
||||
if (response.success && response.data) {
|
||||
stats.value = response.data.data;
|
||||
stats.value = response.data;
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -1,142 +1,88 @@
|
||||
// ChatListPage.scss
|
||||
.chat-list-page {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
min-height: 100vh;
|
||||
|
||||
// Header
|
||||
&__header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 24px 20px;
|
||||
border-radius: 0 0 24px 24px;
|
||||
margin-bottom: 8px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&__title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 40px;
|
||||
opacity: 0.9;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
letter-spacing: -0.5px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
margin: 4px 0 0;
|
||||
opacity: 0.85;
|
||||
margin: 0;
|
||||
color: $grey-6;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__search {
|
||||
:deep(.q-field__control) {
|
||||
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);
|
||||
}
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
// Tabs
|
||||
&__tabs {
|
||||
background: white;
|
||||
margin: 0 12px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
|
||||
:deep(.q-tab) {
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&__tab-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
// Content
|
||||
&__content {
|
||||
padding: 16px 12px;
|
||||
min-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
&__loading {
|
||||
&__loading,
|
||||
&__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
|
||||
p {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
padding: 60px 24px;
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 20px 0 8px;
|
||||
margin: 16px 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
margin-bottom: 24px;
|
||||
margin: 0;
|
||||
color: $grey-6;
|
||||
}
|
||||
}
|
||||
|
||||
// List
|
||||
&__list {
|
||||
background: transparent;
|
||||
background: white;
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.2s ease;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
&:active {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
&--unread {
|
||||
border-left: 4px solid var(--q-primary);
|
||||
background: linear-gradient(90deg, rgba(102, 126, 234, 0.05) 0%, white 100%);
|
||||
}
|
||||
|
||||
:deep(.q-item) {
|
||||
padding: 16px;
|
||||
background: rgba($primary, 0.02);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,26 +91,19 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 24px;
|
||||
height: 100%;
|
||||
padding: 0 20px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
.q-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&--archive {
|
||||
background: #2196f3;
|
||||
background: $orange;
|
||||
}
|
||||
|
||||
&--delete {
|
||||
background: #f44336;
|
||||
background: $negative;
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar
|
||||
&__avatar-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
@@ -175,125 +114,80 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: $primary;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&__online-dot {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #4caf50;
|
||||
border: 3px solid white;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: $positive;
|
||||
border: 2px solid white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__ride-badge {
|
||||
padding: 3px;
|
||||
min-height: 18px;
|
||||
min-width: 18px;
|
||||
.q-badge__content {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
&__name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #1a1a2e;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&__ride-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
margin-top: 2px;
|
||||
color: $primary;
|
||||
font-size: 12px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&__last-message {
|
||||
color: #666;
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: $grey-7;
|
||||
font-size: 14px;
|
||||
|
||||
&--unread {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: $grey-9;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// Meta
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
color: $grey-6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__unread-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Load more
|
||||
&__load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
// Search dialog
|
||||
&__search-dialog {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
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;
|
||||
}
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// ChatListPage.ts
|
||||
import { defineComponent, ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { defineComponent, ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useChat } from '../composables/useChat';
|
||||
import { useAuth } from '@/composables/useAuth'; // Il tuo composable auth esistente
|
||||
import type { Chat, User, Message } from '../types/trasporti.types';
|
||||
import { debounce } from 'quasar';
|
||||
import { useAuth } from '../composables/useAuth';
|
||||
import { Api } from '@api';
|
||||
import type { Chat, User, Message } from '../types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ChatListPage',
|
||||
@@ -17,12 +17,12 @@ export default defineComponent({
|
||||
const {
|
||||
chats,
|
||||
loading,
|
||||
loadChats,
|
||||
archiveChat,
|
||||
fetchChats,
|
||||
getOrCreateDirectChat,
|
||||
toggleMuteChat,
|
||||
deleteChat,
|
||||
createChat,
|
||||
searchUsers: searchUsersApi,
|
||||
onlineUsers
|
||||
onlineUsers,
|
||||
totalUnreadCount
|
||||
} = useChat();
|
||||
|
||||
// State
|
||||
@@ -31,18 +31,19 @@ export default defineComponent({
|
||||
const loadingMore = ref(false);
|
||||
const hasMore = ref(true);
|
||||
const page = ref(1);
|
||||
|
||||
// ✅ User search
|
||||
const showUserSearch = ref(false);
|
||||
const showGroupCreate = ref(false);
|
||||
const userSearchQuery = ref('');
|
||||
const searchedUsers = ref<User[]>([]);
|
||||
const searchingUsers = ref(false);
|
||||
|
||||
// ✅ Group chat
|
||||
const showGroupCreate = ref(false);
|
||||
|
||||
// Computed
|
||||
const currentUserId = computed(() => currentUser.value?._id);
|
||||
|
||||
const unreadCount = computed(() => {
|
||||
return chats.value.reduce((total, chat) => total + (chat.unreadCount || 0), 0);
|
||||
});
|
||||
const unreadCount = computed(() => totalUnreadCount.value);
|
||||
|
||||
const filteredChats = computed(() => {
|
||||
let result = [...chats.value];
|
||||
@@ -50,7 +51,7 @@ export default defineComponent({
|
||||
// Filter by tab
|
||||
switch (activeTab.value) {
|
||||
case 'unread':
|
||||
result = result.filter(chat => chat.unreadCount > 0);
|
||||
result = result.filter(chat => (chat.unreadCount || 0) > 0);
|
||||
break;
|
||||
case 'rides':
|
||||
result = result.filter(chat => chat.rideId);
|
||||
@@ -69,23 +70,38 @@ export default defineComponent({
|
||||
const otherUser = getOtherParticipant(chat);
|
||||
const fullName = `${otherUser?.name || ''} ${otherUser?.surname || ''}`.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) ||
|
||||
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) => {
|
||||
// Pinned chats first
|
||||
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 dateB = new Date(b.lastMessage?.createdAt || b.updatedAt).getTime();
|
||||
const dateA = new Date(a.lastMessage?.timestamp || a.updatedAt).getTime();
|
||||
const dateB = new Date(b.lastMessage?.timestamp || b.updatedAt).getTime();
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
@@ -97,7 +113,7 @@ export default defineComponent({
|
||||
switch (activeTab.value) {
|
||||
case 'unread': return 'mark_email_read';
|
||||
case 'rides': return 'no_transfer';
|
||||
case 'archived': return 'inventory_2';
|
||||
case 'archived': return 'unarchive';
|
||||
default: return 'forum';
|
||||
}
|
||||
});
|
||||
@@ -117,21 +133,25 @@ export default defineComponent({
|
||||
switch (activeTab.value) {
|
||||
case 'unread': return 'Non hai messaggi da leggere';
|
||||
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';
|
||||
}
|
||||
});
|
||||
|
||||
// Methods
|
||||
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 => {
|
||||
if (!user) return '?';
|
||||
const name = user.name || '';
|
||||
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 => {
|
||||
@@ -164,22 +184,37 @@ export default defineComponent({
|
||||
}
|
||||
};
|
||||
|
||||
const getMessagePreview = (message?: Message): string => {
|
||||
if (!message) return 'Nessun messaggio';
|
||||
// ✅ Fixed: Riceve lastMessage invece di chat
|
||||
const getMessagePreview = (lastMessage?: Message | null): string => {
|
||||
if (!lastMessage) return 'Nessun messaggio';
|
||||
|
||||
if (message.type === 'image') return '📷 Foto';
|
||||
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';
|
||||
const msgType = lastMessage.type || 'text';
|
||||
|
||||
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 => {
|
||||
if (!message) return '';
|
||||
if (message.read) return 'done_all';
|
||||
if (message.delivered) return 'done_all';
|
||||
// ✅ Fixed: Riceve lastMessage invece di chat
|
||||
const getMessageStatusIcon = (lastMessage?: Message | null): string => {
|
||||
if (!lastMessage) return '';
|
||||
|
||||
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';
|
||||
};
|
||||
|
||||
@@ -187,14 +222,36 @@ export default defineComponent({
|
||||
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) => {
|
||||
try {
|
||||
await archiveChat(chat._id, !chat.archived);
|
||||
// TODO: Implementa nel backend
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: chat.archived ? 'Chat ripristinata' : 'Chat archiviata',
|
||||
icon: 'archive'
|
||||
message: 'Conversazione archiviata'
|
||||
});
|
||||
await fetchChats();
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
@@ -203,20 +260,28 @@ export default defineComponent({
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ Fixed: Delete chat
|
||||
const onDeleteChat = async (chat: Chat) => {
|
||||
$q.dialog({
|
||||
title: 'Elimina conversazione',
|
||||
message: 'Sei sicuro di voler eliminare questa conversazione? L\'azione non è reversibile.',
|
||||
cancel: true,
|
||||
message: 'Sei sicuro di voler eliminare questa conversazione?',
|
||||
cancel: {
|
||||
label: 'Annulla',
|
||||
flat: true
|
||||
},
|
||||
ok: {
|
||||
label: 'Elimina',
|
||||
color: 'negative'
|
||||
},
|
||||
persistent: true
|
||||
}).onOk(async () => {
|
||||
try {
|
||||
await deleteChat(chat._id);
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Conversazione eliminata',
|
||||
icon: 'delete'
|
||||
message: 'Conversazione eliminata'
|
||||
});
|
||||
await fetchChats();
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
@@ -226,38 +291,45 @@ export default defineComponent({
|
||||
});
|
||||
};
|
||||
|
||||
const loadMore = async () => {
|
||||
loadingMore.value = true;
|
||||
try {
|
||||
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) {
|
||||
// ✅ Added: Search users
|
||||
const searchUsers = async () => {
|
||||
if (!userSearchQuery.value || userSearchQuery.value.length < 2) {
|
||||
searchedUsers.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
searchingUsers.value = true;
|
||||
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 {
|
||||
searchingUsers.value = false;
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// ✅ Added: Start chat with user
|
||||
const startChatWith = async (user: User) => {
|
||||
try {
|
||||
const chat = await createChat([user._id]);
|
||||
const chat = await getOrCreateDirectChat(user._id);
|
||||
showUserSearch.value = false;
|
||||
router.push(`/trasporti/chat/${chat._id}`);
|
||||
userSearchQuery.value = '';
|
||||
searchedUsers.value = [];
|
||||
|
||||
if (chat) {
|
||||
router.push(`/trasporti/chat/${chat._id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
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
|
||||
onMounted(() => {
|
||||
loadChats({ page: 1, limit: 20 });
|
||||
onMounted(async () => {
|
||||
await fetchChats(1, 20);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -300,9 +392,11 @@ export default defineComponent({
|
||||
getMessagePreview,
|
||||
getMessageStatusIcon,
|
||||
openChat,
|
||||
onMuteChat,
|
||||
onArchiveChat,
|
||||
onDeleteChat,
|
||||
loadMore,
|
||||
startNewChat,
|
||||
searchUsers,
|
||||
startChatWith
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- Header -->
|
||||
<div class="chat-list-page__header">
|
||||
<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>
|
||||
<h1 class="chat-list-page__title">Messaggi</h1>
|
||||
<p class="chat-list-page__subtitle">Le tue conversazioni</p>
|
||||
@@ -17,12 +17,12 @@
|
||||
placeholder="Cerca conversazione..."
|
||||
outlined
|
||||
dense
|
||||
class="chat-list-page__search"
|
||||
class="chat-list-page__search q-mt-md"
|
||||
>
|
||||
<template #prepend>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
<template v-if="searchQuery" #append>
|
||||
<template v-if="searchQuery" v-slot:append>
|
||||
<q-icon name="close" class="cursor-pointer" @click="searchQuery = ''" />
|
||||
</template>
|
||||
</q-input>
|
||||
@@ -35,13 +35,19 @@
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
align="justify"
|
||||
dense
|
||||
>
|
||||
<q-tab name="all" label="Tutte" icon="inbox" />
|
||||
<q-tab name="unread" icon="mark_email_unread">
|
||||
<template #default>
|
||||
<template v-slot:default>
|
||||
<div class="chat-list-page__tab-content">
|
||||
<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>
|
||||
</template>
|
||||
</q-tab>
|
||||
@@ -52,16 +58,16 @@
|
||||
<!-- Content -->
|
||||
<div class="chat-list-page__content">
|
||||
<!-- 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" />
|
||||
<p>Caricamento conversazioni...</p>
|
||||
<p class="text-grey q-mt-md">Caricamento conversazioni...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="filteredChats.length === 0" class="chat-list-page__empty">
|
||||
<q-icon :name="emptyStateIcon" size="80px" color="grey-4" />
|
||||
<h3>{{ emptyStateTitle }}</h3>
|
||||
<p>{{ emptyStateMessage }}</p>
|
||||
<h3 class="q-mt-md q-mb-sm">{{ emptyStateTitle }}</h3>
|
||||
<p class="text-grey">{{ emptyStateMessage }}</p>
|
||||
<q-btn
|
||||
v-if="activeTab === 'all' && !searchQuery"
|
||||
color="primary"
|
||||
@@ -69,6 +75,7 @@
|
||||
label="Esplora viaggi"
|
||||
rounded
|
||||
unelevated
|
||||
class="q-mt-md"
|
||||
to="/trasporti"
|
||||
/>
|
||||
</div>
|
||||
@@ -79,24 +86,27 @@
|
||||
v-for="chat in filteredChats"
|
||||
:key="chat._id"
|
||||
class="chat-list-page__item"
|
||||
:class="{ 'chat-list-page__item--unread': chat.unreadCount > 0 }"
|
||||
@left="onArchiveChat(chat)"
|
||||
@right="onDeleteChat(chat)"
|
||||
:class="{ 'chat-list-page__item--unread': (chat.unreadCount || 0) > 0 }"
|
||||
@left="(details) => { details.reset(); onArchiveChat(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">
|
||||
<q-icon name="archive" />
|
||||
<span>Archivia</span>
|
||||
<q-icon name="archive" size="24px" />
|
||||
<span class="text-caption q-mt-xs">Archivia</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<!-- Right Slide Action: Delete -->
|
||||
<template v-slot:right>
|
||||
<div class="chat-list-page__slide-action chat-list-page__slide-action--delete">
|
||||
<q-icon name="delete" />
|
||||
<span>Elimina</span>
|
||||
<q-icon name="delete" size="24px" />
|
||||
<span class="text-caption q-mt-xs">Elimina</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Chat Item -->
|
||||
<q-item clickable @click="openChat(chat)">
|
||||
<!-- Avatar -->
|
||||
<q-item-section avatar>
|
||||
@@ -105,7 +115,7 @@
|
||||
<img
|
||||
v-if="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">
|
||||
{{ getInitials(getOtherParticipant(chat)) }}
|
||||
@@ -120,27 +130,53 @@
|
||||
|
||||
<!-- Ride type badge -->
|
||||
<q-badge
|
||||
v-if="chat.rideInfo"
|
||||
:color="chat.rideInfo.type === 'offer' ? 'positive' : 'negative'"
|
||||
v-if="chat.rideId && typeof chat.rideId === 'object' && chat.rideId.type"
|
||||
:color="chat.rideId.type === 'offer' ? 'positive' : 'warning'"
|
||||
floating
|
||||
rounded
|
||||
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>
|
||||
</div>
|
||||
</q-item-section>
|
||||
|
||||
<!-- Content -->
|
||||
<q-item-section>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Ride info -->
|
||||
<q-item-label v-if="chat.rideInfo" caption class="chat-list-page__ride-info">
|
||||
<q-icon name="place" size="14px" />
|
||||
{{ chat.rideInfo.departure }} → {{ chat.rideInfo.destination }}
|
||||
<q-item-label
|
||||
v-if="chat.rideId && typeof chat.rideId === 'object'"
|
||||
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>
|
||||
|
||||
<!-- Last message -->
|
||||
@@ -148,38 +184,46 @@
|
||||
caption
|
||||
lines="1"
|
||||
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
|
||||
v-if="chat.lastMessage?.senderId === currentUserId"
|
||||
v-if="chat.lastMessage && chat.lastMessage.senderId === currentUserId"
|
||||
:name="getMessageStatusIcon(chat.lastMessage)"
|
||||
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) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<!-- Right side -->
|
||||
<q-item-section side>
|
||||
<!-- Right side: Time & Badge -->
|
||||
<q-item-section side top>
|
||||
<div class="chat-list-page__meta">
|
||||
<!-- 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>
|
||||
|
||||
<!-- Unread badge -->
|
||||
<q-badge
|
||||
v-if="chat.unreadCount > 0"
|
||||
v-if="(chat.unreadCount || 0) > 0"
|
||||
color="primary"
|
||||
:label="chat.unreadCount > 99 ? '99+' : chat.unreadCount"
|
||||
rounded
|
||||
class="chat-list-page__unread-badge"
|
||||
class="chat-list-page__unread-badge q-mt-xs"
|
||||
/>
|
||||
|
||||
<!-- Muted icon -->
|
||||
<q-icon
|
||||
v-else-if="chat.pinned"
|
||||
name="push_pin"
|
||||
v-else-if="chat.mutedBy && chat.mutedBy.includes(currentUserId)"
|
||||
name="notifications_off"
|
||||
size="18px"
|
||||
color="grey"
|
||||
class="q-mt-xs"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
@@ -188,11 +232,12 @@
|
||||
</q-list>
|
||||
|
||||
<!-- 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
|
||||
flat
|
||||
color="primary"
|
||||
label="Carica altre conversazioni"
|
||||
icon="expand_more"
|
||||
:loading="loadingMore"
|
||||
@click="loadMore"
|
||||
/>
|
||||
@@ -211,12 +256,16 @@
|
||||
color="secondary"
|
||||
icon="person_search"
|
||||
label="Cerca utente"
|
||||
external-label
|
||||
label-position="left"
|
||||
@click="showUserSearch = true"
|
||||
/>
|
||||
<q-fab-action
|
||||
color="accent"
|
||||
icon="group"
|
||||
label="Nuovo gruppo"
|
||||
external-label
|
||||
label-position="left"
|
||||
@click="showGroupCreate = true"
|
||||
/>
|
||||
</q-fab>
|
||||
@@ -224,9 +273,11 @@
|
||||
|
||||
<!-- User Search Dialog -->
|
||||
<q-dialog v-model="showUserSearch" position="top">
|
||||
<q-card class="chat-list-page__search-dialog">
|
||||
<q-card-section>
|
||||
<q-card class="chat-list-page__search-dialog" style="width: 100%; max-width: 500px;">
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<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>
|
||||
@@ -234,40 +285,91 @@
|
||||
v-model="userSearchQuery"
|
||||
placeholder="Cerca per nome o username..."
|
||||
outlined
|
||||
dense
|
||||
autofocus
|
||||
@update:model-value="searchUsers"
|
||||
>
|
||||
<template #prepend>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
<template v-if="userSearchQuery" v-slot:append>
|
||||
<q-icon
|
||||
name="close"
|
||||
class="cursor-pointer"
|
||||
@click="userSearchQuery = ''; searchedUsers = []"
|
||||
/>
|
||||
</template>
|
||||
</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
|
||||
v-for="user in searchedUsers"
|
||||
:key="user._id"
|
||||
clickable
|
||||
v-ripple
|
||||
@click="startChatWith(user)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<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-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ user.name }} {{ user.surname }}</q-item-label>
|
||||
<q-item-label caption>@{{ user.username }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="chevron_right" color="grey" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<div v-else-if="userSearchQuery && !searchingUsers" class="text-center q-pa-md text-grey">
|
||||
Nessun utente trovato
|
||||
<!-- No results -->
|
||||
<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>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,71 +1,90 @@
|
||||
// ChatPage.scss
|
||||
.chat-page {
|
||||
height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #f0f2f5;
|
||||
background: #e5ddd5;
|
||||
// Padding per la tab fissa in basso (40px)
|
||||
padding-bottom: 30px;
|
||||
|
||||
// Header
|
||||
// Header locale (NON q-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) {
|
||||
min-height: 64px;
|
||||
&__toolbar {
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: $primary;
|
||||
color: white;
|
||||
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 {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 10px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #fff3cd;
|
||||
border-top: 1px solid #ffc107;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
background: #ffe69c;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,201 +92,205 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__ride-banner-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__ride-banner-route {
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__ride-banner-date {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// Messages
|
||||
// Messages Area
|
||||
&__messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
padding-bottom: 80px;
|
||||
position: relative;
|
||||
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");
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
// Calcola altezza considerando header + input + tab
|
||||
// Si adatta automaticamente grazie a flex: 1
|
||||
}
|
||||
|
||||
&__load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&__date-separator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
|
||||
span {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #667eea;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
color: #666;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Typing indicator
|
||||
// Typing Indicator
|
||||
&__typing-indicator {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
&__typing-bubble {
|
||||
background: white;
|
||||
border-radius: 18px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
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 {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #bbb;
|
||||
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) { animation-delay: 0.2s; }
|
||||
&:nth-child(3) { animation-delay: 0.4s; }
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typingBounce {
|
||||
0%, 60%, 100% { transform: translateY(0); }
|
||||
30% { transform: translateY(-6px); }
|
||||
}
|
||||
|
||||
// Scroll button
|
||||
// Scroll Button
|
||||
&__scroll-btn {
|
||||
position: fixed;
|
||||
bottom: 90px;
|
||||
// Posiziona sopra l'input area e la tab fissa
|
||||
bottom: calc(70px + 40px); // input (70px) + tab (40px)
|
||||
right: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
// Reply preview
|
||||
// Reply Preview
|
||||
&__reply-preview {
|
||||
background: white;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background: #f0f0f0;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #ddd;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__reply-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #667eea;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__reply-author {
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
&__reply-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
display: block;
|
||||
max-width: 250px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// Input area
|
||||
// Input Area
|
||||
&__input-area {
|
||||
position: fixed;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
|
||||
z-index: 100;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&__input-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__input {
|
||||
:deep(.q-field__control) {
|
||||
border-radius: 24px;
|
||||
background: #f5f5f5;
|
||||
min-height: 44px;
|
||||
|
||||
&::before {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.q-field__native) {
|
||||
padding: 8px 0;
|
||||
.q-field__control {
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// Emoji picker
|
||||
// Emoji Picker
|
||||
&__emoji-picker {
|
||||
background: white;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 8px;
|
||||
max-width: 280px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
&__emoji {
|
||||
font-size: 24px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
// Attach menu
|
||||
// Attachment Menu
|
||||
&__attach-menu {
|
||||
border-radius: 20px 20px 0 0;
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__attach-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
text-align: center;
|
||||
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
&__attach-item {
|
||||
@@ -277,7 +300,7 @@
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
@@ -286,128 +309,98 @@
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.q-avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Profile card
|
||||
// Profile Card
|
||||
&__profile-card {
|
||||
width: 320px;
|
||||
max-width: 90vw;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&__profile-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-top: 24px;
|
||||
|
||||
.q-btn {
|
||||
position: absolute;
|
||||
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%;
|
||||
}
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 4px;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
opacity: 0.85;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__profile-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.q-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
// Animations
|
||||
@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
|
||||
.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: all 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-up-enter-from,
|
||||
.slide-up-leave-to {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// Dark mode
|
||||
.body--dark {
|
||||
// Responsive
|
||||
@media (max-width: 600px) {
|
||||
.chat-page {
|
||||
background: #121212;
|
||||
|
||||
&__messages {
|
||||
background-color: #1a1a2e;
|
||||
&__user-name {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&__date-separator span {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
&__typing-bubble {
|
||||
background: #2d2d44;
|
||||
}
|
||||
|
||||
&__input-area {
|
||||
background: #1e1e30;
|
||||
}
|
||||
|
||||
&__input {
|
||||
:deep(.q-field__control) {
|
||||
background: #2d2d44;
|
||||
}
|
||||
}
|
||||
|
||||
&__reply-preview {
|
||||
background: #1e1e30;
|
||||
border-color: #333;
|
||||
&__attach-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
// 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 { useQuasar } from 'quasar';
|
||||
import { useChat } from '../composables/useChat';
|
||||
import { useRealtimeChat } from '../composables/useRealtimeChat';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { useAuth } from '../composables/useAuth';
|
||||
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';
|
||||
|
||||
interface MessageGroup {
|
||||
@@ -18,7 +26,7 @@ export default defineComponent({
|
||||
name: 'ChatPage',
|
||||
|
||||
components: {
|
||||
MessageBubble
|
||||
MessageBubble,
|
||||
},
|
||||
|
||||
setup() {
|
||||
@@ -38,14 +46,12 @@ export default defineComponent({
|
||||
deleteMessage: deleteMsg,
|
||||
onlineUsers,
|
||||
typingUsers,
|
||||
toggleMuteChat
|
||||
toggleMuteChat,
|
||||
startPolling, // AGGIUNGI
|
||||
stopPolling, // AGGIUNGI
|
||||
} = useChat();
|
||||
|
||||
const {
|
||||
subscribeToChat,
|
||||
unsubscribeFromChat,
|
||||
sendTyping
|
||||
} = useRealtimeChat();
|
||||
const { subscribeToChat, unsubscribeFromChat, sendTyping } = useRealtimeChat();
|
||||
|
||||
// Refs
|
||||
const messagesContainer = ref<HTMLElement>();
|
||||
@@ -66,15 +72,28 @@ export default defineComponent({
|
||||
const isMuted = ref(false);
|
||||
const lastSeen = ref<Date | null>(null);
|
||||
|
||||
const commonEmojis = ['😊', '😂', '❤️', '👍', '🙏', '😍', '🎉', '🚗', '📍', '✅', '❌', '⏰'];
|
||||
const commonEmojis = [
|
||||
'😊',
|
||||
'😂',
|
||||
'❤️',
|
||||
'👍',
|
||||
'🙏',
|
||||
'😍',
|
||||
'🎉',
|
||||
'🚗',
|
||||
'📍',
|
||||
'✅',
|
||||
'❌',
|
||||
'⏰',
|
||||
];
|
||||
|
||||
// Computed
|
||||
const chatId = computed(() => route.params.id as string);
|
||||
const currentUserId = computed(() => currentUser.value?._id);
|
||||
|
||||
const otherUser = computed((): User | undefined => {
|
||||
const otherUser = computed((): any | 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 => {
|
||||
@@ -93,14 +112,14 @@ export default defineComponent({
|
||||
const groups: MessageGroup[] = [];
|
||||
let currentDate = '';
|
||||
|
||||
messages.value.forEach(message => {
|
||||
messages.value.forEach((message) => {
|
||||
const messageDate = formatDateHeader(new Date(message.createdAt));
|
||||
|
||||
if (messageDate !== currentDate) {
|
||||
currentDate = messageDate;
|
||||
groups.push({
|
||||
date: messageDate,
|
||||
messages: [message]
|
||||
messages: [message],
|
||||
});
|
||||
} else {
|
||||
groups[groups.length - 1].messages.push(message);
|
||||
@@ -133,7 +152,7 @@ export default defineComponent({
|
||||
return date.toLocaleDateString('it-IT', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long'
|
||||
month: 'long',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -158,7 +177,7 @@ export default defineComponent({
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -173,7 +192,7 @@ export default defineComponent({
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTo({
|
||||
top: messagesContainer.value.scrollHeight,
|
||||
behavior: smooth ? 'smooth' : 'auto'
|
||||
behavior: smooth ? 'smooth' : 'auto',
|
||||
});
|
||||
newMessagesCount.value = 0;
|
||||
}
|
||||
@@ -203,7 +222,7 @@ export default defineComponent({
|
||||
try {
|
||||
const olderMessages = await loadMessages(chatId.value, {
|
||||
before: messages.value[0]?.createdAt,
|
||||
limit: 30
|
||||
limit: 30,
|
||||
});
|
||||
|
||||
if (olderMessages.length < 30) {
|
||||
@@ -233,7 +252,7 @@ export default defineComponent({
|
||||
await sendMessageApi(chatId.value, {
|
||||
content,
|
||||
type: 'text',
|
||||
replyTo: replyToId
|
||||
replyTo: replyToId,
|
||||
});
|
||||
|
||||
messageText.value = '';
|
||||
@@ -242,7 +261,7 @@ export default defineComponent({
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Errore nell\'invio del messaggio'
|
||||
message: "Errore nell'invio del messaggio",
|
||||
});
|
||||
} finally {
|
||||
sending.value = false;
|
||||
@@ -268,14 +287,14 @@ export default defineComponent({
|
||||
$q.dialog({
|
||||
title: 'Elimina messaggio',
|
||||
message: 'Eliminare questo messaggio?',
|
||||
cancel: true
|
||||
cancel: true,
|
||||
}).onOk(async () => {
|
||||
try {
|
||||
await deleteMsg(chatId.value, messageId);
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Errore nell\'eliminazione'
|
||||
message: "Errore nell'eliminazione",
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -287,14 +306,14 @@ export default defineComponent({
|
||||
} else {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Numero di telefono non disponibile'
|
||||
message: 'Numero di telefono non disponibile',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const viewRide = () => {
|
||||
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({
|
||||
type: 'info',
|
||||
message: isMuted.value ? 'Notifiche silenziate' : 'Notifiche attivate',
|
||||
icon: isMuted.value ? 'notifications_off' : 'notifications'
|
||||
icon: isMuted.value ? 'notifications_off' : 'notifications',
|
||||
});
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Errore'
|
||||
message: 'Errore',
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -327,7 +346,7 @@ export default defineComponent({
|
||||
title: 'Elimina conversazione',
|
||||
message: 'Sei sicuro? Questa azione non è reversibile.',
|
||||
cancel: true,
|
||||
persistent: true
|
||||
persistent: true,
|
||||
}).onOk(async () => {
|
||||
try {
|
||||
// TODO: Implementa eliminazione chat
|
||||
@@ -335,7 +354,7 @@ export default defineComponent({
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Errore nell\'eliminazione'
|
||||
message: "Errore nell'eliminazione",
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -345,7 +364,7 @@ export default defineComponent({
|
||||
$q.dialog({
|
||||
title: 'Blocca utente',
|
||||
message: `Bloccare ${otherUser.value?.name}? Non potrete più scambiarvi messaggi.`,
|
||||
cancel: true
|
||||
cancel: true,
|
||||
}).onOk(() => {
|
||||
// TODO: Implementa blocco
|
||||
showUserProfile.value = false;
|
||||
@@ -378,20 +397,23 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
// Watch for new messages
|
||||
watch(() => messages.value.length, (newLen, oldLen) => {
|
||||
if (newLen > oldLen) {
|
||||
const lastMessage = messages.value[messages.value.length - 1];
|
||||
watch(
|
||||
() => messages.value.length,
|
||||
(newLen, oldLen) => {
|
||||
if (newLen > oldLen) {
|
||||
const lastMessage = messages.value[messages.value.length - 1];
|
||||
|
||||
if (lastMessage.senderId === currentUserId.value) {
|
||||
scrollToBottom();
|
||||
} else if (showScrollButton.value) {
|
||||
newMessagesCount.value++;
|
||||
} else {
|
||||
scrollToBottom();
|
||||
markAsRead(chatId.value);
|
||||
if (lastMessage.senderId === currentUserId.value) {
|
||||
scrollToBottom();
|
||||
} else if (showScrollButton.value) {
|
||||
newMessagesCount.value++;
|
||||
} else {
|
||||
scrollToBottom();
|
||||
markAsRead(chatId.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
@@ -399,11 +421,24 @@ export default defineComponent({
|
||||
await loadMessages(chatId.value, { limit: 50 });
|
||||
scrollToBottom(false);
|
||||
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(() => {
|
||||
unsubscribeFromChat(chatId.value);
|
||||
stopPolling();
|
||||
|
||||
//unsubscribeFromChat(chatId.value);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -460,7 +495,8 @@ export default defineComponent({
|
||||
attachDocument,
|
||||
shareLocation,
|
||||
sendRideRequest,
|
||||
startVoiceMessage
|
||||
startVoiceMessage,
|
||||
getIsOwn,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
<!-- ChatPage.vue -->
|
||||
<template>
|
||||
<q-page class="chat-page">
|
||||
<!-- Header -->
|
||||
<q-header class="chat-page__header" elevated>
|
||||
<q-toolbar>
|
||||
<q-btn flat round icon="arrow_back" @click="goBack" />
|
||||
<!-- Header Locale (NON q-header) -->
|
||||
<div class="chat-page__header">
|
||||
<div class="chat-page__toolbar">
|
||||
<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
|
||||
v-if="otherUser?.profile?.img"
|
||||
:src="otherUser.profile.img"
|
||||
:alt="otherUser.name"
|
||||
/>
|
||||
<div v-else class="chat-page__avatar-placeholder">
|
||||
<div
|
||||
v-else
|
||||
class="chat-page__avatar-placeholder"
|
||||
>
|
||||
{{ getInitials(otherUser) }}
|
||||
</div>
|
||||
</q-avatar>
|
||||
|
||||
<q-toolbar-title class="q-ml-sm">
|
||||
<div class="chat-page__user-name" @click="showUserProfile = true">
|
||||
<div class="chat-page__toolbar-title">
|
||||
<div
|
||||
class="chat-page__user-name"
|
||||
@click="showUserProfile = true"
|
||||
>
|
||||
{{ otherUser?.name }} {{ otherUser?.surname }}
|
||||
</div>
|
||||
<div class="chat-page__user-status">
|
||||
@@ -27,27 +41,49 @@
|
||||
<span class="chat-page__typing-dots">...</span>
|
||||
</template>
|
||||
<template v-else-if="isOnline">
|
||||
<q-icon name="circle" size="8px" color="positive" />
|
||||
<q-icon
|
||||
name="circle"
|
||||
size="8px"
|
||||
color="positive"
|
||||
/>
|
||||
<span>Online</span>
|
||||
</template>
|
||||
<template v-else-if="lastSeen">
|
||||
<span>Ultimo accesso {{ formatLastSeen(lastSeen) }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</q-toolbar-title>
|
||||
</div>
|
||||
|
||||
<q-btn flat round icon="phone" @click="callUser" />
|
||||
<q-btn flat round icon="more_vert">
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
icon="phone"
|
||||
@click="callUser"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
icon="more_vert"
|
||||
>
|
||||
<q-menu>
|
||||
<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-icon name="person" />
|
||||
</q-item-section>
|
||||
<q-item-section>Profilo utente</q-item-section>
|
||||
</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-icon name="directions_car" />
|
||||
</q-item-section>
|
||||
@@ -56,35 +92,57 @@
|
||||
|
||||
<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-icon name="search" />
|
||||
</q-item-section>
|
||||
<q-item-section>Cerca</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item clickable v-close-popup @click="toggleMute">
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
@click="toggleMute"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon :name="isMuted ? 'notifications' : 'notifications_off'" />
|
||||
</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-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-icon name="delete" color="negative" />
|
||||
<q-icon
|
||||
name="delete"
|
||||
color="negative"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>Elimina conversazione</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</q-toolbar>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<q-icon
|
||||
:name="rideInfo.type === 'offer' ? 'directions_car' : 'hail'"
|
||||
@@ -102,7 +160,7 @@
|
||||
</div>
|
||||
<q-icon name="chevron_right" />
|
||||
</div>
|
||||
</q-header>
|
||||
</div>
|
||||
|
||||
<!-- Messages Area -->
|
||||
<div
|
||||
@@ -111,7 +169,10 @@
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<!-- Load More -->
|
||||
<div v-if="hasMoreMessages" class="chat-page__load-more">
|
||||
<div
|
||||
v-if="hasMoreMessages"
|
||||
class="chat-page__load-more"
|
||||
>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
@@ -122,7 +183,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<span>{{ group.date }}</span>
|
||||
</div>
|
||||
@@ -131,19 +195,25 @@
|
||||
v-for="message in group.messages"
|
||||
:key="message._id"
|
||||
:message="message"
|
||||
:is-mine="message.senderId === currentUserId"
|
||||
:is-own="getIsOwn(message)"
|
||||
:show-avatar="shouldShowAvatar(message, group.messages)"
|
||||
:sender="message.senderId === currentUserId ? currentUser : otherUser"
|
||||
:sender="getIsOwn(message) ? currentUser : otherUser"
|
||||
@reply="replyTo = message"
|
||||
@react="onReact"
|
||||
@reaction-click="onReact"
|
||||
@delete="onDeleteMessage"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Typing indicator -->
|
||||
<div v-if="isTyping" class="chat-page__typing-indicator">
|
||||
<div
|
||||
v-if="isTyping"
|
||||
class="chat-page__typing-indicator"
|
||||
>
|
||||
<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>
|
||||
</q-avatar>
|
||||
<div class="chat-page__typing-bubble">
|
||||
@@ -165,7 +235,11 @@
|
||||
size="md"
|
||||
@click="scrollToBottom"
|
||||
>
|
||||
<q-badge v-if="newMessagesCount > 0" color="primary" floating>
|
||||
<q-badge
|
||||
v-if="newMessagesCount > 0"
|
||||
color="primary"
|
||||
floating
|
||||
>
|
||||
{{ newMessagesCount }}
|
||||
</q-badge>
|
||||
</q-btn>
|
||||
@@ -174,9 +248,15 @@
|
||||
|
||||
<!-- Reply Preview -->
|
||||
<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">
|
||||
<q-icon name="reply" size="20px" />
|
||||
<q-icon
|
||||
name="reply"
|
||||
size="20px"
|
||||
/>
|
||||
<div>
|
||||
<span class="chat-page__reply-author">
|
||||
{{ replyTo.senderId === currentUserId ? 'Tu' : otherUser?.name }}
|
||||
@@ -184,7 +264,13 @@
|
||||
<span class="chat-page__reply-text">{{ replyTo.content }}</span>
|
||||
</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>
|
||||
</transition>
|
||||
|
||||
@@ -211,7 +297,13 @@
|
||||
@update:model-value="onTyping"
|
||||
>
|
||||
<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
|
||||
v-model="showEmoji"
|
||||
:offset="[0, 10]"
|
||||
@@ -243,47 +335,104 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Attachment Menu -->
|
||||
<q-dialog v-model="showAttachMenu" position="bottom">
|
||||
<!-- Resto dei dialog... -->
|
||||
<q-dialog
|
||||
v-model="showAttachMenu"
|
||||
position="bottom"
|
||||
>
|
||||
<q-card class="chat-page__attach-menu">
|
||||
<div class="chat-page__attach-grid">
|
||||
<div class="chat-page__attach-item" @click="attachImage">
|
||||
<q-avatar color="purple" text-color="white" icon="image" />
|
||||
<div
|
||||
class="chat-page__attach-item"
|
||||
@click="attachImage"
|
||||
>
|
||||
<q-avatar
|
||||
color="purple"
|
||||
text-color="white"
|
||||
icon="image"
|
||||
/>
|
||||
<span>Foto</span>
|
||||
</div>
|
||||
<div class="chat-page__attach-item" @click="attachDocument">
|
||||
<q-avatar color="blue" text-color="white" icon="description" />
|
||||
<div
|
||||
class="chat-page__attach-item"
|
||||
@click="attachDocument"
|
||||
>
|
||||
<q-avatar
|
||||
color="blue"
|
||||
text-color="white"
|
||||
icon="description"
|
||||
/>
|
||||
<span>Documento</span>
|
||||
</div>
|
||||
<div class="chat-page__attach-item" @click="shareLocation">
|
||||
<q-avatar color="green" text-color="white" icon="location_on" />
|
||||
<div
|
||||
class="chat-page__attach-item"
|
||||
@click="shareLocation"
|
||||
>
|
||||
<q-avatar
|
||||
color="green"
|
||||
text-color="white"
|
||||
icon="location_on"
|
||||
/>
|
||||
<span>Posizione</span>
|
||||
</div>
|
||||
<div v-if="rideInfo" class="chat-page__attach-item" @click="sendRideRequest">
|
||||
<q-avatar color="orange" text-color="white" icon="directions_car" />
|
||||
<div
|
||||
v-if="rideInfo"
|
||||
class="chat-page__attach-item"
|
||||
@click="sendRideRequest"
|
||||
>
|
||||
<q-avatar
|
||||
color="orange"
|
||||
text-color="white"
|
||||
icon="directions_car"
|
||||
/>
|
||||
<span>Richiedi passaggio</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- User Profile Dialog -->
|
||||
<q-dialog v-model="showUserProfile" position="right" full-height>
|
||||
<q-dialog
|
||||
v-model="showUserProfile"
|
||||
position="right"
|
||||
full-height
|
||||
>
|
||||
<q-card class="chat-page__profile-card">
|
||||
<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">
|
||||
<img v-if="otherUser?.profile?.img" :src="otherUser.profile.img" />
|
||||
<span v-else class="text-h4">{{ getInitials(otherUser) }}</span>
|
||||
<img
|
||||
v-if="otherUser?.profile?.img"
|
||||
:src="otherUser.profile.img"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="text-h4"
|
||||
>{{ getInitials(otherUser) }}</span
|
||||
>
|
||||
</q-avatar>
|
||||
<h4>{{ otherUser?.name }} {{ otherUser?.surname }}</h4>
|
||||
<p>@{{ otherUser?.username }}</p>
|
||||
|
||||
<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-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-btn>
|
||||
</div>
|
||||
@@ -314,13 +463,18 @@
|
||||
|
||||
<q-item v-if="otherUser?.profile?.driverProfile">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="star" color="amber" />
|
||||
<q-icon
|
||||
name="star"
|
||||
color="amber"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>Valutazione guida</q-item-label>
|
||||
<q-item-label>
|
||||
{{ 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-section>
|
||||
</q-item>
|
||||
|
||||
@@ -73,14 +73,14 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const goToRide = (rideId: string) => {
|
||||
router.push(`/trasporti/viaggio/${rideId}`);
|
||||
router.push(`/trasporti/ride/${rideId}`);
|
||||
};
|
||||
|
||||
const contactUser = async () => {
|
||||
try {
|
||||
const response = await getOrCreateDirectChat(userId.value);
|
||||
if (response?.data?.data) {
|
||||
router.push(`/trasporti/chat/${response.data.data._id}`);
|
||||
if (response?.data) {
|
||||
router.push(`/trasporti/chat/${response.data._id}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
$q.notify({ type: 'negative', message: error.data?.message || error.message });
|
||||
|
||||
@@ -101,7 +101,7 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const goToRide = (rideId: string) => {
|
||||
router.push(`/trasporti/viaggio/${rideId}`);
|
||||
router.push(`/trasporti/ride/${rideId}`);
|
||||
};
|
||||
|
||||
const goToProfile = (userId: string) => {
|
||||
@@ -109,7 +109,7 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const editRide = (rideId: string) => {
|
||||
router.push(`/trasporti/viaggio/${rideId}/modifica`);
|
||||
router.push(`/trasporti/ride/${rideId}/modifica`);
|
||||
};
|
||||
|
||||
const cancelRide = async (ride: Ride) => {
|
||||
|
||||
@@ -360,9 +360,9 @@ export default defineComponent({
|
||||
|
||||
try {
|
||||
const [statsRes, receivedRes, givenRes] = await Promise.all([
|
||||
Api.SendReq('/api/trasporti/feedback/stats', 'GET'),
|
||||
Api.SendReq('/api/trasporti/feedback/received', 'GET'),
|
||||
Api.SendReq('/api/trasporti/feedback/given', 'GET')
|
||||
Api.SendReqWithData('/api/trasporti/feedback/stats', 'GET'),
|
||||
Api.SendReqWithData('/api/trasporti/feedback/received', 'GET'),
|
||||
Api.SendReqWithData('/api/trasporti/feedback/given', 'GET')
|
||||
]);
|
||||
|
||||
if (statsRes.success) {
|
||||
@@ -396,7 +396,7 @@ export default defineComponent({
|
||||
? '/api/trasporti/feedback/received'
|
||||
: '/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) {
|
||||
const newFeedbacks = response.data.feedbacks || [];
|
||||
|
||||
@@ -367,7 +367,7 @@ export default defineComponent({
|
||||
const endpoint = responseAction.value === 'accept'
|
||||
? `/api/trasporti/richieste/${selectedRequest.value._id}/accept`
|
||||
: `/api/trasporti/richieste/${selectedRequest.value._id}/reject`;
|
||||
const response = await Api.SendReq(endpoint, 'PUT', { message: responseMessage.value });
|
||||
const response = await Api.SendReqWithData(endpoint, 'PUT', { message: responseMessage.value });
|
||||
if (response.success) {
|
||||
const index = receivedRequests.value.findIndex(r => r._id === selectedRequest.value?._id);
|
||||
if (index !== -1) {
|
||||
@@ -390,7 +390,7 @@ export default defineComponent({
|
||||
const cancelRequest = async (request: RideRequest) => {
|
||||
$q.dialog({ title: 'Annulla richiesta', message: 'Sei sicuro?', cancel: true }).onOk(async () => {
|
||||
try {
|
||||
const response = await Api.SendReq(`/api/trasporti/richieste/${request._id}/cancel`, 'PUT');
|
||||
const response = await Api.SendReqWithData(`/api/trasporti/richieste/${request._id}/cancel`, 'PUT');
|
||||
if (response.success) {
|
||||
const index = sentRequests.value.findIndex(r => r._id === request._id);
|
||||
if (index !== -1) sentRequests.value[index].status = 'cancelled';
|
||||
@@ -406,9 +406,9 @@ export default defineComponent({
|
||||
loading.value = true;
|
||||
try {
|
||||
const [statsRes, receivedRes, sentRes] = await Promise.all([
|
||||
Api.SendReq('/api/trasporti/richieste/stats', 'GET'),
|
||||
Api.SendReq('/api/trasporti/richieste/received', 'GET'),
|
||||
Api.SendReq('/api/trasporti/richieste/sent', 'GET')
|
||||
Api.SendReqWithData('/api/trasporti/richieste/stats', 'GET'),
|
||||
Api.SendReqWithData('/api/trasporti/richieste/received', 'GET'),
|
||||
Api.SendReqWithData('/api/trasporti/richieste/sent', 'GET')
|
||||
]);
|
||||
if (statsRes.success) stats.value = statsRes.data;
|
||||
if (receivedRes.success) {
|
||||
@@ -428,7 +428,7 @@ export default defineComponent({
|
||||
currentPage.value++;
|
||||
try {
|
||||
const endpoint = activeTab.value === 'received' ? '/api/trasporti/richieste/received' : '/api/trasporti/richieste/sent';
|
||||
const response = await Api.SendReq(`${endpoint}?page=${currentPage.value}`, 'GET');
|
||||
const response = await Api.SendReqWithData(`${endpoint}?page=${currentPage.value}`, 'GET');
|
||||
if (response.success) {
|
||||
const newRequests = response.data.requests || [];
|
||||
if (activeTab.value === 'received') receivedRequests.value.push(...newRequests);
|
||||
|
||||
@@ -292,8 +292,8 @@ export default defineComponent({
|
||||
if (isEditing.value) {
|
||||
try {
|
||||
const response = await fetchRide(rideId.value!);
|
||||
if (response?.data?.data) {
|
||||
const ride = response.data.data;
|
||||
if (response?.data) {
|
||||
const ride = response.data;
|
||||
|
||||
formData.type = ride.type;
|
||||
formData.departure = ride.departure;
|
||||
|
||||
@@ -198,7 +198,7 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const editRide = () => {
|
||||
router.push(`/trasporti/viaggio/${rideId.value}/modifica`);
|
||||
router.push(`/trasporti/ride/${rideId.value}/modifica`);
|
||||
};
|
||||
|
||||
const shareRide = async () => {
|
||||
@@ -280,8 +280,8 @@ export default defineComponent({
|
||||
const contactDriver = async () => {
|
||||
try {
|
||||
const response = await getOrCreateDirectChat(driverId.value, rideId.value);
|
||||
if (response?.data?.data) {
|
||||
router.push(`/trasporti/chat/${response.data.data._id}`);
|
||||
if (response?.data) {
|
||||
router.push(`/trasporti/chat/${response.data._id}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
$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;
|
||||
try {
|
||||
const response = await getOrCreateDirectChat(userId, rideId.value);
|
||||
if (response?.data?.data) {
|
||||
router.push(`/trasporti/chat/${response.data.data._id}`);
|
||||
if (response?.data) {
|
||||
router.push(`/trasporti/chat/${response.data._id}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
$q.notify({ type: 'negative', message: error.data?.message || error.message });
|
||||
|
||||
@@ -180,7 +180,7 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const goToRide = (rideId: string) => {
|
||||
router.push(`/trasporti/viaggio/${rideId}`);
|
||||
router.push(`/trasporti/ride/${rideId}`);
|
||||
};
|
||||
|
||||
const goToProfile = (userId: string) => {
|
||||
@@ -218,8 +218,8 @@ export default defineComponent({
|
||||
try {
|
||||
const userId = typeof ride.userId === 'string' ? ride.userId : ride.userId._id;
|
||||
const response = await getOrCreateDirectChat(userId, ride._id);
|
||||
if (response?.data?.data) {
|
||||
router.push(`/trasporti/chat/${response.data.data._id}`);
|
||||
if (response?.data) {
|
||||
router.push(`/trasporti/chat/${response.data._id}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
$q.notify({ type: 'negative', message: error.data?.message || error.message });
|
||||
|
||||
@@ -81,7 +81,7 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const goToDetail = (rideId: string) => {
|
||||
router.push(`/trasporti/viaggio/${rideId}`);
|
||||
router.push(`/trasporti/ride/${rideId}`);
|
||||
};
|
||||
|
||||
const goToDriverProfile = (userId: string) => {
|
||||
@@ -135,8 +135,8 @@ export default defineComponent({
|
||||
const userId = typeof ride.userId === 'string' ? ride.userId : ride.userId._id;
|
||||
const response = await getOrCreateDirectChat(userId, ride._id);
|
||||
|
||||
if (response?.data?.data) {
|
||||
router.push(`/trasporti/chat/${response.data.data._id}`);
|
||||
if (response?.data) {
|
||||
router.push(`/trasporti/chat/${response.data._id}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
|
||||
@@ -536,8 +536,8 @@ export default defineComponent({
|
||||
loading.value = true;
|
||||
try {
|
||||
const [settingsRes, vehiclesRes] = await Promise.all([
|
||||
Api.SendReq('/api/trasporti/settings', 'GET'),
|
||||
Api.SendReq('/api/trasporti/veicoli', 'GET')
|
||||
Api.SendReqWithData('/api/trasporti/settings', 'GET'),
|
||||
Api.SendReqWithData('/api/trasporti/driver/vehicles', 'GET')
|
||||
]);
|
||||
|
||||
if (settingsRes.success && settingsRes.data) {
|
||||
@@ -557,7 +557,7 @@ export default defineComponent({
|
||||
const saveSettings = async () => {
|
||||
saving.value = true;
|
||||
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) {
|
||||
$q.notify({ type: 'positive', message: 'Impostazioni salvate', timeout: 1500 });
|
||||
}
|
||||
|
||||
@@ -12,18 +12,30 @@
|
||||
/>
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="vehicle-edit-page__loading">
|
||||
<q-spinner-dots size="50px" color="primary" />
|
||||
<div
|
||||
v-if="loading"
|
||||
class="vehicle-edit-page__loading"
|
||||
>
|
||||
<q-spinner-dots
|
||||
size="50px"
|
||||
color="primary"
|
||||
/>
|
||||
<p>Caricamento dati...</p>
|
||||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
<div class="vehicle-edit-page__section">
|
||||
<h3 class="vehicle-edit-page__section-title">
|
||||
@@ -32,12 +44,16 @@
|
||||
</h3>
|
||||
|
||||
<div class="vehicle-edit-page__photos">
|
||||
<!-- Existing Photos -->
|
||||
<div
|
||||
v-for="(photo, index) in form.photos"
|
||||
:key="index"
|
||||
class="vehicle-edit-page__photo"
|
||||
>
|
||||
<q-img :src="photo" :ratio="1" />
|
||||
<q-img
|
||||
:src="photo"
|
||||
:ratio="1"
|
||||
/>
|
||||
<q-btn
|
||||
round
|
||||
flat
|
||||
@@ -47,19 +63,29 @@
|
||||
class="vehicle-edit-page__photo-remove"
|
||||
@click="removePhoto(index)"
|
||||
/>
|
||||
<div v-if="index === 0" class="vehicle-edit-page__photo-badge">
|
||||
<q-icon name="star" size="12px" />
|
||||
Principale
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Photo Button -->
|
||||
<div
|
||||
v-if="form.photos.length < 4"
|
||||
v-if="form.photos.length < 5"
|
||||
class="vehicle-edit-page__photo-add"
|
||||
@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>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -76,10 +102,15 @@
|
||||
v-for="vType in vehicleTypes"
|
||||
:key="vType.value"
|
||||
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"
|
||||
>
|
||||
<q-icon :name="vType.icon" size="28px" />
|
||||
<q-icon
|
||||
:name="vType.icon"
|
||||
size="28px"
|
||||
/>
|
||||
<span>{{ vType.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,7 +121,7 @@
|
||||
v-model="form.brand"
|
||||
label="Marca *"
|
||||
outlined
|
||||
:rules="[val => !!val || 'Campo obbligatorio']"
|
||||
:rules="[(val) => !!val || 'Campo obbligatorio']"
|
||||
class="vehicle-edit-page__input"
|
||||
>
|
||||
<template #prepend>
|
||||
@@ -102,7 +133,7 @@
|
||||
v-model="form.model"
|
||||
label="Modello *"
|
||||
outlined
|
||||
:rules="[val => !!val || 'Campo obbligatorio']"
|
||||
:rules="[(val) => !!val || 'Campo obbligatorio']"
|
||||
class="vehicle-edit-page__input"
|
||||
>
|
||||
<template #prepend>
|
||||
@@ -158,13 +189,8 @@
|
||||
v-model="form.plate"
|
||||
label="Targa *"
|
||||
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"
|
||||
@update:model-value="val => form.plate = val.toUpperCase()"
|
||||
@update:model-value="(val) => (form.plate = val.toUpperCase())"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="pin" />
|
||||
@@ -191,7 +217,10 @@
|
||||
@click="form.seats = Math.max(1, form.seats - 1)"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<q-btn
|
||||
@@ -202,9 +231,7 @@
|
||||
@click="form.seats = Math.min(8, form.seats + 1)"
|
||||
/>
|
||||
</div>
|
||||
<p class="vehicle-edit-page__seats-hint">
|
||||
Escluso il conducente
|
||||
</p>
|
||||
<p class="vehicle-edit-page__seats-hint">Escluso il conducente</p>
|
||||
</div>
|
||||
|
||||
<!-- Fuel Type -->
|
||||
@@ -215,10 +242,15 @@
|
||||
v-for="fuel in fuelTypes"
|
||||
:key="fuel.value"
|
||||
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"
|
||||
>
|
||||
<q-icon :name="fuel.icon" size="20px" />
|
||||
<q-icon
|
||||
:name="fuel.icon"
|
||||
size="20px"
|
||||
/>
|
||||
<span>{{ fuel.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,7 +275,10 @@
|
||||
>
|
||||
<template #default>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
@@ -271,7 +306,10 @@
|
||||
|
||||
<!-- Default Toggle -->
|
||||
<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-label>Imposta come predefinito</q-item-label>
|
||||
<q-item-label caption>
|
||||
@@ -279,7 +317,10 @@
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<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>
|
||||
</div>
|
||||
@@ -303,13 +344,14 @@
|
||||
</div>
|
||||
</q-form>
|
||||
|
||||
<!-- Hidden File Input -->
|
||||
<!-- Hidden File Input for Multiple Photos -->
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
style="display: none"
|
||||
@change="onPhotoSelected"
|
||||
@change="onPhotosSelected"
|
||||
/>
|
||||
</q-page>
|
||||
</template>
|
||||
@@ -347,6 +389,7 @@ export default defineComponent({
|
||||
// State
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const uploadingPhotos = ref(false);
|
||||
const isEdit = computed(() => !!route.params.vehicleId);
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
@@ -362,7 +405,7 @@ export default defineComponent({
|
||||
features: [],
|
||||
photos: [],
|
||||
notes: '',
|
||||
isDefault: false
|
||||
isDefault: false,
|
||||
});
|
||||
|
||||
// Options
|
||||
@@ -370,7 +413,7 @@ export default defineComponent({
|
||||
{ value: 'car', label: 'Auto', icon: 'directions_car' },
|
||||
{ value: 'suv', label: 'SUV', icon: 'local_shipping' },
|
||||
{ value: 'van', label: 'Van', icon: 'airport_shuttle' },
|
||||
{ value: 'motorcycle', label: 'Moto', icon: 'two_wheeler' }
|
||||
{ value: 'motorcycle', label: 'Moto', icon: 'two_wheeler' },
|
||||
];
|
||||
|
||||
const fuelTypes = [
|
||||
@@ -379,7 +422,7 @@ export default defineComponent({
|
||||
{ value: 'hybrid', label: 'Ibrido', icon: 'eco' },
|
||||
{ value: 'electric', label: 'Elettrico', icon: 'bolt' },
|
||||
{ value: 'lpg', label: 'GPL', icon: 'propane_tank' },
|
||||
{ value: 'methane', label: 'Metano', icon: 'propane' }
|
||||
{ value: 'methane', label: 'Metano', icon: 'propane' },
|
||||
];
|
||||
|
||||
const colorOptions = [
|
||||
@@ -393,7 +436,7 @@ export default defineComponent({
|
||||
{ value: 'giallo', label: 'Giallo', hex: '#fdd835' },
|
||||
{ value: 'arancione', label: 'Arancione', hex: '#fb8c00' },
|
||||
{ value: 'marrone', label: 'Marrone', hex: '#795548' },
|
||||
{ value: 'beige', label: 'Beige', hex: '#d7ccc8' }
|
||||
{ value: 'beige', label: 'Beige', hex: '#d7ccc8' },
|
||||
];
|
||||
|
||||
const availableFeatures = [
|
||||
@@ -406,73 +449,143 @@ export default defineComponent({
|
||||
{ value: 'animali', label: 'Animali ammessi', icon: 'pets' },
|
||||
{ value: 'wifi', label: 'WiFi', icon: 'wifi' },
|
||||
{ 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
|
||||
const confirmCancel = () => {
|
||||
$q.dialog({
|
||||
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,
|
||||
persistent: true
|
||||
persistent: true,
|
||||
}).onOk(() => {
|
||||
router.back();
|
||||
});
|
||||
};
|
||||
|
||||
const addPhoto = () => {
|
||||
if (form.value.photos.length >= 5) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Puoi caricare massimo 5 foto',
|
||||
});
|
||||
return;
|
||||
}
|
||||
fileInput.value?.click();
|
||||
};
|
||||
|
||||
const onPhotoSelected = async (event: Event) => {
|
||||
const onPhotosSelected = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
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 (!file.type.startsWith('image/')) {
|
||||
if (files.length > remainingSlots) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Seleziona un file immagine valido'
|
||||
type: 'warning',
|
||||
message: `Puoi caricare solo ${remainingSlots} foto in più`,
|
||||
});
|
||||
return;
|
||||
files.splice(remainingSlots);
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'L\'immagine non può superare i 5MB'
|
||||
});
|
||||
return;
|
||||
// Validate all files
|
||||
for (const file of files) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
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 {
|
||||
$q.loading.show({ message: 'Caricamento foto...' });
|
||||
|
||||
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) {
|
||||
form.value.photos.push(response.data.url);
|
||||
if (response.success && response.data.urls) {
|
||||
form.value.photos.push(...response.data.urls);
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: `${files.length} foto caricate con successo`,
|
||||
icon: 'check_circle',
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Errore upload foto:', error);
|
||||
$q.notify({
|
||||
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 {
|
||||
uploadingPhotos.value = false;
|
||||
$q.loading.hide();
|
||||
input.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const removePhoto = (index: number) => {
|
||||
form.value.photos.splice(index, 1);
|
||||
const removePhoto = async (index: number) => {
|
||||
$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 () => {
|
||||
@@ -481,13 +594,13 @@ export default defineComponent({
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
`/api/trasporti/veicoli/${route.params.vehicleId}`,
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/driver/vehicles/${route.params.vehicleId}`,
|
||||
'GET'
|
||||
);
|
||||
|
||||
if (response.success && response.data?.data) {
|
||||
const vehicle = response.data.data;
|
||||
if (response.success && response.data) {
|
||||
const vehicle = response.data;
|
||||
form.value = {
|
||||
type: vehicle.type || 'car',
|
||||
brand: vehicle.brand || '',
|
||||
@@ -500,13 +613,14 @@ export default defineComponent({
|
||||
features: vehicle.features || [],
|
||||
photos: vehicle.photos || [],
|
||||
notes: vehicle.notes || '',
|
||||
isDefault: vehicle.isDefault || false
|
||||
isDefault: vehicle.isDefault || false,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
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();
|
||||
} finally {
|
||||
@@ -515,16 +629,27 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
const endpoint = isEdit.value
|
||||
? `/api/trasporti/veicoli/${route.params.vehicleId}`
|
||||
: '/api/trasporti/veicoli';
|
||||
? `/api/trasporti/driver/vehicles/${route.params.vehicleId}`
|
||||
: '/api/trasporti/driver/vehicles';
|
||||
|
||||
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) {
|
||||
$q.notify({
|
||||
@@ -532,7 +657,7 @@ export default defineComponent({
|
||||
message: isEdit.value
|
||||
? 'Veicolo aggiornato con successo'
|
||||
: 'Veicolo aggiunto con successo',
|
||||
icon: 'check_circle'
|
||||
icon: 'check_circle',
|
||||
});
|
||||
|
||||
router.push('/trasporti/veicoli');
|
||||
@@ -542,7 +667,8 @@ export default defineComponent({
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.data?.message || error.message || 'Errore durante il salvataggio'
|
||||
message:
|
||||
error.data?.message || error.message || 'Errore durante il salvataggio',
|
||||
});
|
||||
} finally {
|
||||
saving.value = false;
|
||||
@@ -559,6 +685,7 @@ export default defineComponent({
|
||||
fileInput,
|
||||
loading,
|
||||
saving,
|
||||
uploadingPhotos,
|
||||
isEdit,
|
||||
currentYear,
|
||||
form,
|
||||
@@ -568,11 +695,11 @@ export default defineComponent({
|
||||
availableFeatures,
|
||||
confirmCancel,
|
||||
addPhoto,
|
||||
onPhotoSelected,
|
||||
onPhotosSelected,
|
||||
removePhoto,
|
||||
onSubmit
|
||||
onSubmit,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -648,7 +775,7 @@ export default defineComponent({
|
||||
// Photos
|
||||
&__photos {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@@ -667,7 +794,23 @@ export default defineComponent({
|
||||
position: absolute;
|
||||
top: 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 {
|
||||
@@ -690,14 +833,21 @@ export default defineComponent({
|
||||
span {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__photo-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 12px 0 0;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
|
||||
.q-icon {
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
// Type Selector
|
||||
|
||||
@@ -266,7 +266,8 @@ export default defineComponent({
|
||||
|
||||
// Methods
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
// router.back();
|
||||
router.push('/trasporti/dashboard');
|
||||
};
|
||||
|
||||
const addVehicle = () => {
|
||||
@@ -352,8 +353,8 @@ export default defineComponent({
|
||||
|
||||
const setDefault = async (vehicle: Vehicle) => {
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
`/api/trasporti/veicoli/${vehicle._id}/default`,
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/driver/vehicles/${vehicle._id}/default`,
|
||||
'PUT'
|
||||
);
|
||||
|
||||
@@ -388,8 +389,8 @@ export default defineComponent({
|
||||
deleting.value = true;
|
||||
|
||||
try {
|
||||
const response = await Api.SendReq(
|
||||
`/api/trasporti/veicoli/${vehicleToDelete.value._id}`,
|
||||
const response = await Api.SendReqWithData(
|
||||
`/api/trasporti/driver/vehicles/${vehicleToDelete.value._id}`,
|
||||
'DELETE'
|
||||
);
|
||||
|
||||
@@ -420,10 +421,10 @@ export default defineComponent({
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await Api.SendReq('/api/trasporti/veicoli', 'GET');
|
||||
const response = await Api.SendReqWithData('/api/trasporti/driver/vehicles', 'GET');
|
||||
|
||||
if (response.success) {
|
||||
vehicles.value = response.data?.data || [];
|
||||
vehicles.value = response.data || [];
|
||||
}
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface Vehicle {
|
||||
features?: VehicleFeature[];
|
||||
isDefault?: boolean;
|
||||
isVerified?: boolean;
|
||||
photos?: string[];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -131,6 +132,11 @@ export interface UserBasic {
|
||||
name?: string;
|
||||
surname?: string;
|
||||
email?: string;
|
||||
profile?: {
|
||||
img?: string;
|
||||
cell?: string;
|
||||
Biografia?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserWithProfile extends UserBasic {
|
||||
@@ -138,6 +144,7 @@ export interface UserWithProfile extends UserBasic {
|
||||
img?: string;
|
||||
Biografia?: string;
|
||||
Cell?: string;
|
||||
cell?: string;
|
||||
cellVerified?: boolean;
|
||||
driverProfile?: DriverProfile;
|
||||
preferences?: UserPreferences;
|
||||
@@ -277,6 +284,16 @@ export interface RideRequestContribution {
|
||||
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 {
|
||||
_id: string;
|
||||
idapp: string;
|
||||
@@ -307,9 +324,11 @@ export interface RideRequest {
|
||||
feedbackGiven: boolean;
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
// Virtuals
|
||||
// Virtuals & Extra fields
|
||||
canCancel?: boolean;
|
||||
isPending?: boolean;
|
||||
rideInfo?: RideInfo;
|
||||
userData?: UserBasic;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -321,7 +340,7 @@ export type MessageType = 'text' | 'ride_share' | 'location' | 'image' | 'voice'
|
||||
|
||||
export interface LastMessage {
|
||||
text: string;
|
||||
senderId: string;
|
||||
senderId: string | UserBasic;
|
||||
timestamp: Date | string;
|
||||
type: MessageType;
|
||||
}
|
||||
@@ -335,14 +354,16 @@ export interface Chat {
|
||||
type: ChatType;
|
||||
title?: string;
|
||||
lastMessage?: LastMessage;
|
||||
unreadCount?: Map<string, number> | Record<string, number>;
|
||||
unreadCount?: Map<string, number> | Record<string, number> | number;
|
||||
isActive: boolean;
|
||||
mutedBy?: string[];
|
||||
blockedBy?: string[];
|
||||
deletedBy?: string[];
|
||||
createdAt: Date | string;
|
||||
updatedAt: Date | string;
|
||||
// Extra per UI
|
||||
otherParticipant?: UserBasic;
|
||||
rideInfo?: RideInfo;
|
||||
}
|
||||
|
||||
export interface MessageMetadata {
|
||||
@@ -376,6 +397,7 @@ export interface Message {
|
||||
chatId: string;
|
||||
senderId: string | UserBasic;
|
||||
text?: string;
|
||||
content?: string; // Alias di text
|
||||
type: MessageType;
|
||||
metadata?: MessageMetadata;
|
||||
readBy?: MessageReadBy[];
|
||||
@@ -386,6 +408,7 @@ export interface Message {
|
||||
deletedAt?: Date | string;
|
||||
reactions?: MessageReaction[];
|
||||
createdAt: Date | string;
|
||||
updatedAt?: Date | string;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -675,17 +698,22 @@ export interface RideRequestCounts {
|
||||
pending: number;
|
||||
accepted: number;
|
||||
rejected: number;
|
||||
cancelled: number;
|
||||
expired: number;
|
||||
completed: number;
|
||||
}
|
||||
|
||||
export interface RequestsReceivedResponse {
|
||||
success: boolean;
|
||||
data: RideRequest[];
|
||||
counts: RideRequestCounts;
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
pages: number;
|
||||
data: {
|
||||
requests: RideRequest[];
|
||||
counts: RideRequestCounts;
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
pages: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ async function sendRequest(
|
||||
const actions = {
|
||||
get: () => Api.get(url, mydata, responsedata),
|
||||
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),
|
||||
put: () => Api.put(url, mydata, responsedata),
|
||||
patch: () => Api.patch(url, mydata, responsedata),
|
||||
|
||||
@@ -130,7 +130,7 @@ async function Request(
|
||||
if (tools.isDebug())
|
||||
console.log('Axios Request', path, type, tools.notshowPwd(payload));
|
||||
|
||||
const isFormData = type === 'postFormData';
|
||||
const isFormData = type === 'postFormData' || type === 'putFormData';
|
||||
let config: AxiosRequestConfig = {
|
||||
baseURL,
|
||||
timeout: 60000,
|
||||
@@ -222,6 +222,8 @@ async function Request(
|
||||
});*/
|
||||
} else if (type === 'postFormData') {
|
||||
response = await axiosInstance.post(path, payload, config);
|
||||
} else if (type === 'putFormData') {
|
||||
response = await axiosInstance.put(path, payload, config);
|
||||
} else {
|
||||
throw new Error(`Unsupported request type: ${type}`);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,13 @@ export const Api = {
|
||||
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) {
|
||||
const globalStore = useGlobalStore();
|
||||
globalStore.connData.downloading_server = 1;
|
||||
@@ -336,6 +343,34 @@ export const Api = {
|
||||
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
|
||||
async SendReq(
|
||||
url: string,
|
||||
|
||||
Reference in New Issue
Block a user