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