From c9fc1a83d0eeb5979cb57c7031a1bb5d7c0f3bcc Mon Sep 17 00:00:00 2001 From: Surya Paolo Date: Mon, 22 Dec 2025 01:19:23 +0100 Subject: [PATCH] - Implementazione TRASPORTI ! Passo 1 --- quasar.config.ts | 1 + .../AIImageGenerator/AIImageGenerator.vue | 2 +- src/components/CMyCircuit/CMyCircuit.vue | 4 +- src/components/CQRCode/CQRCode.ts | 2 +- src/components/CSignUp/CSignUp.vue | 3 - .../PosterGenerator/PosterGenerator.ts | 2 +- .../AiImageGenerator/AiImageGenerator.vue | 2 +- .../components/AssetManager/AssetManager.vue | 4 +- .../TemplateBuilder/TemplateBuilder.ts | 4 +- src/model/GlobalStore.ts | 1 + .../trasporti/components/chat/ChatInput.scss | 160 +++ .../trasporti/components/chat/ChatInput.ts | 114 +++ .../trasporti/components/chat/ChatInput.vue | 112 +++ .../trasporti/components/chat/ChatList.scss | 180 ++++ .../trasporti/components/chat/ChatList.ts | 185 ++++ .../trasporti/components/chat/ChatList.vue | 133 +++ .../trasporti/components/chat/ChatWindow.scss | 159 +++ .../trasporti/components/chat/ChatWindow.ts | 290 ++++++ .../trasporti/components/chat/ChatWindow.vue | 148 +++ .../components/chat/MessageBubble.scss | 299 ++++++ .../components/chat/MessageBubble.ts | 166 +++ .../components/chat/MessageBubble.vue | 194 ++++ .../trasporti/components/chat/index.ts | 5 + .../components/feedback/FeedbackCard.scss | 225 +++++ .../components/feedback/FeedbackCard.ts | 166 +++ .../components/feedback/FeedbackCard.vue | 158 +++ .../components/feedback/FeedbackForm.scss | 224 +++++ .../components/feedback/FeedbackForm.ts | 249 +++++ .../components/feedback/FeedbackForm.vue | 223 +++++ .../components/feedback/FeedbackList.scss | 231 +++++ .../components/feedback/FeedbackList.ts | 143 +++ .../components/feedback/FeedbackList.vue | 138 +++ .../components/feedback/FeedbackSummary.vue | 116 +++ .../trasporti/components/feedback/index.ts | 5 + .../components/ride/CityAutocomplete.vue | 376 +++++++ .../components/ride/ContribTypeSelector.scss | 138 +++ .../components/ride/ContribTypeSelector.ts | 252 +++++ .../components/ride/ContribTypeSelector.vue | 125 +++ .../trasporti/components/ride/MyRideCard.vue | 312 ++++++ .../components/ride/PreferencesSelector.scss | 134 +++ .../components/ride/PreferencesSelector.ts | 146 +++ .../components/ride/PreferencesSelector.vue | 197 ++++ .../components/ride/RecurrenceSelector.scss | 129 +++ .../components/ride/RecurrenceSelector.ts | 199 ++++ .../components/ride/RecurrenceSelector.vue | 194 ++++ .../trasporti/components/ride/RequestCard.vue | 205 ++++ .../trasporti/components/ride/RideCard.scss | 356 +++++++ .../trasporti/components/ride/RideCard.ts | 286 ++++++ .../trasporti/components/ride/RideCard.vue | 217 ++++ .../components/ride/RideFilters.scss | 71 ++ .../trasporti/components/ride/RideFilters.ts | 202 ++++ .../trasporti/components/ride/RideFilters.vue | 365 +++++++ .../trasporti/components/ride/RideMap.scss | 153 +++ .../trasporti/components/ride/RideMap.ts | 440 ++++++++ .../trasporti/components/ride/RideMap.vue | 80 ++ .../components/ride/RideTypeToggle.scss | 132 +++ .../components/ride/RideTypeToggle.ts | 59 ++ .../components/ride/RideTypeToggle.vue | 66 ++ .../trasporti/components/ride/StarRating.scss | 81 ++ .../trasporti/components/ride/StarRating.ts | 109 ++ .../trasporti/components/ride/StarRating.vue | 47 + .../components/ride/VehicleSelector.scss | 186 ++++ .../components/ride/VehicleSelector.ts | 144 +++ .../components/ride/VehicleSelector.vue | 224 +++++ .../components/ride/WaypointsEditor.scss | 141 +++ .../components/ride/WaypointsEditor.ts | 139 +++ .../components/ride/WaypointsEditor.vue | 117 +++ .../trasporti/components/ride/index.ts | 12 + .../components/widgets/RideWidget.scss | 549 ++++++++++ .../components/widgets/RideWidget.ts | 286 ++++++ .../components/widgets/RideWidget.vue | 241 +++++ src/modules/trasporti/composables/index.ts | 9 + src/modules/trasporti/composables/useChat.ts | 524 ++++++++++ .../composables/useCitySuggestions.ts | 136 +++ .../trasporti/composables/useContribTypes.ts | 208 ++++ .../trasporti/composables/useDriverProfile.ts | 397 ++++++++ .../trasporti/composables/useFeedback.ts | 490 +++++++++ .../trasporti/composables/useGeocoding.ts | 195 ++++ .../trasporti/composables/useRealtimeChat.ts | 254 +++++ .../trasporti/composables/useRecentCities.ts | 165 +++ .../trasporti/composables/useRideRequests.ts | 408 ++++++++ src/modules/trasporti/composables/useRides.ts | 586 +++++++++++ src/modules/trasporti/pages/ChatListPage.scss | 299 ++++++ src/modules/trasporti/pages/ChatListPage.ts | 310 ++++++ src/modules/trasporti/pages/ChatListPage.vue | 275 +++++ src/modules/trasporti/pages/ChatPage.scss | 413 ++++++++ src/modules/trasporti/pages/ChatPage.ts | 466 +++++++++ src/modules/trasporti/pages/ChatPage.vue | 345 +++++++ .../trasporti/pages/DriverProfilePage.scss | 405 ++++++++ .../trasporti/pages/DriverProfilePage.ts | 172 ++++ .../trasporti/pages/DriverProfilePage.vue | 327 ++++++ src/modules/trasporti/pages/Helppage.vue | 463 +++++++++ src/modules/trasporti/pages/MyRidesPage.scss | 146 +++ src/modules/trasporti/pages/MyRidesPage.ts | 298 ++++++ src/modules/trasporti/pages/MyRidesPage.vue | 221 ++++ .../trasporti/pages/Myfeedbackpage.vue | 790 +++++++++++++++ src/modules/trasporti/pages/Requestspage.vue | 649 ++++++++++++ .../trasporti/pages/RideCreatePage.scss | 181 ++++ src/modules/trasporti/pages/RideCreatePage.ts | 369 +++++++ .../trasporti/pages/RideCreatePage.vue | 349 +++++++ .../trasporti/pages/RideDetailPage.scss | 380 +++++++ src/modules/trasporti/pages/RideDetailPage.ts | 414 ++++++++ .../trasporti/pages/RideDetailPage.vue | 402 ++++++++ .../trasporti/pages/RideSearchPage.scss | 231 +++++ src/modules/trasporti/pages/RideSearchPage.ts | 335 +++++++ .../trasporti/pages/RideSearchPage.vue | 245 +++++ .../trasporti/pages/RidesListPage.scss | 151 +++ src/modules/trasporti/pages/RidesListPage.ts | 190 ++++ src/modules/trasporti/pages/RidesListPage.vue | 136 +++ src/modules/trasporti/pages/Settingspage.vue | 680 +++++++++++++ .../trasporti/pages/Vehicleeditpage.vue | 943 ++++++++++++++++++ src/modules/trasporti/pages/Vehiclespage.vue | 755 ++++++++++++++ src/modules/trasporti/pages/index.ts | 7 + src/modules/trasporti/types/index.ts | 2 + .../trasporti/types/trasporti.types.ts | 884 ++++++++++++++++ src/rootgen/admin/confsite/confsite.vue | 3 + src/router/routesAI.ts | 1 + src/router/routesTrasporti.ts | 619 ++++++++++++ src/statics/lang/it.js | 32 + src/store/Api/index.ts | 32 +- src/store/Modules/serv_constants.ts | 1 + src/store/globalStore.ts | 4 + tsconfig.json | 6 +- 123 files changed, 27433 insertions(+), 28 deletions(-) create mode 100644 src/modules/trasporti/components/chat/ChatInput.scss create mode 100644 src/modules/trasporti/components/chat/ChatInput.ts create mode 100644 src/modules/trasporti/components/chat/ChatInput.vue create mode 100644 src/modules/trasporti/components/chat/ChatList.scss create mode 100644 src/modules/trasporti/components/chat/ChatList.ts create mode 100644 src/modules/trasporti/components/chat/ChatList.vue create mode 100644 src/modules/trasporti/components/chat/ChatWindow.scss create mode 100644 src/modules/trasporti/components/chat/ChatWindow.ts create mode 100644 src/modules/trasporti/components/chat/ChatWindow.vue create mode 100644 src/modules/trasporti/components/chat/MessageBubble.scss create mode 100644 src/modules/trasporti/components/chat/MessageBubble.ts create mode 100644 src/modules/trasporti/components/chat/MessageBubble.vue create mode 100644 src/modules/trasporti/components/chat/index.ts create mode 100644 src/modules/trasporti/components/feedback/FeedbackCard.scss create mode 100644 src/modules/trasporti/components/feedback/FeedbackCard.ts create mode 100644 src/modules/trasporti/components/feedback/FeedbackCard.vue create mode 100644 src/modules/trasporti/components/feedback/FeedbackForm.scss create mode 100644 src/modules/trasporti/components/feedback/FeedbackForm.ts create mode 100644 src/modules/trasporti/components/feedback/FeedbackForm.vue create mode 100644 src/modules/trasporti/components/feedback/FeedbackList.scss create mode 100644 src/modules/trasporti/components/feedback/FeedbackList.ts create mode 100644 src/modules/trasporti/components/feedback/FeedbackList.vue create mode 100644 src/modules/trasporti/components/feedback/FeedbackSummary.vue create mode 100644 src/modules/trasporti/components/feedback/index.ts create mode 100644 src/modules/trasporti/components/ride/CityAutocomplete.vue create mode 100644 src/modules/trasporti/components/ride/ContribTypeSelector.scss create mode 100644 src/modules/trasporti/components/ride/ContribTypeSelector.ts create mode 100644 src/modules/trasporti/components/ride/ContribTypeSelector.vue create mode 100644 src/modules/trasporti/components/ride/MyRideCard.vue create mode 100644 src/modules/trasporti/components/ride/PreferencesSelector.scss create mode 100644 src/modules/trasporti/components/ride/PreferencesSelector.ts create mode 100644 src/modules/trasporti/components/ride/PreferencesSelector.vue create mode 100644 src/modules/trasporti/components/ride/RecurrenceSelector.scss create mode 100644 src/modules/trasporti/components/ride/RecurrenceSelector.ts create mode 100644 src/modules/trasporti/components/ride/RecurrenceSelector.vue create mode 100644 src/modules/trasporti/components/ride/RequestCard.vue create mode 100644 src/modules/trasporti/components/ride/RideCard.scss create mode 100644 src/modules/trasporti/components/ride/RideCard.ts create mode 100644 src/modules/trasporti/components/ride/RideCard.vue create mode 100644 src/modules/trasporti/components/ride/RideFilters.scss create mode 100644 src/modules/trasporti/components/ride/RideFilters.ts create mode 100644 src/modules/trasporti/components/ride/RideFilters.vue create mode 100644 src/modules/trasporti/components/ride/RideMap.scss create mode 100644 src/modules/trasporti/components/ride/RideMap.ts create mode 100644 src/modules/trasporti/components/ride/RideMap.vue create mode 100644 src/modules/trasporti/components/ride/RideTypeToggle.scss create mode 100644 src/modules/trasporti/components/ride/RideTypeToggle.ts create mode 100644 src/modules/trasporti/components/ride/RideTypeToggle.vue create mode 100644 src/modules/trasporti/components/ride/StarRating.scss create mode 100644 src/modules/trasporti/components/ride/StarRating.ts create mode 100644 src/modules/trasporti/components/ride/StarRating.vue create mode 100644 src/modules/trasporti/components/ride/VehicleSelector.scss create mode 100644 src/modules/trasporti/components/ride/VehicleSelector.ts create mode 100644 src/modules/trasporti/components/ride/VehicleSelector.vue create mode 100644 src/modules/trasporti/components/ride/WaypointsEditor.scss create mode 100644 src/modules/trasporti/components/ride/WaypointsEditor.ts create mode 100644 src/modules/trasporti/components/ride/WaypointsEditor.vue create mode 100644 src/modules/trasporti/components/ride/index.ts create mode 100644 src/modules/trasporti/components/widgets/RideWidget.scss create mode 100644 src/modules/trasporti/components/widgets/RideWidget.ts create mode 100644 src/modules/trasporti/components/widgets/RideWidget.vue create mode 100644 src/modules/trasporti/composables/index.ts create mode 100644 src/modules/trasporti/composables/useChat.ts create mode 100644 src/modules/trasporti/composables/useCitySuggestions.ts create mode 100644 src/modules/trasporti/composables/useContribTypes.ts create mode 100644 src/modules/trasporti/composables/useDriverProfile.ts create mode 100644 src/modules/trasporti/composables/useFeedback.ts create mode 100644 src/modules/trasporti/composables/useGeocoding.ts create mode 100644 src/modules/trasporti/composables/useRealtimeChat.ts create mode 100644 src/modules/trasporti/composables/useRecentCities.ts create mode 100644 src/modules/trasporti/composables/useRideRequests.ts create mode 100644 src/modules/trasporti/composables/useRides.ts create mode 100644 src/modules/trasporti/pages/ChatListPage.scss create mode 100644 src/modules/trasporti/pages/ChatListPage.ts create mode 100644 src/modules/trasporti/pages/ChatListPage.vue create mode 100644 src/modules/trasporti/pages/ChatPage.scss create mode 100644 src/modules/trasporti/pages/ChatPage.ts create mode 100644 src/modules/trasporti/pages/ChatPage.vue create mode 100644 src/modules/trasporti/pages/DriverProfilePage.scss create mode 100644 src/modules/trasporti/pages/DriverProfilePage.ts create mode 100644 src/modules/trasporti/pages/DriverProfilePage.vue create mode 100644 src/modules/trasporti/pages/Helppage.vue create mode 100644 src/modules/trasporti/pages/MyRidesPage.scss create mode 100644 src/modules/trasporti/pages/MyRidesPage.ts create mode 100644 src/modules/trasporti/pages/MyRidesPage.vue create mode 100644 src/modules/trasporti/pages/Myfeedbackpage.vue create mode 100644 src/modules/trasporti/pages/Requestspage.vue create mode 100644 src/modules/trasporti/pages/RideCreatePage.scss create mode 100644 src/modules/trasporti/pages/RideCreatePage.ts create mode 100644 src/modules/trasporti/pages/RideCreatePage.vue create mode 100644 src/modules/trasporti/pages/RideDetailPage.scss create mode 100644 src/modules/trasporti/pages/RideDetailPage.ts create mode 100644 src/modules/trasporti/pages/RideDetailPage.vue create mode 100644 src/modules/trasporti/pages/RideSearchPage.scss create mode 100644 src/modules/trasporti/pages/RideSearchPage.ts create mode 100644 src/modules/trasporti/pages/RideSearchPage.vue create mode 100644 src/modules/trasporti/pages/RidesListPage.scss create mode 100644 src/modules/trasporti/pages/RidesListPage.ts create mode 100644 src/modules/trasporti/pages/RidesListPage.vue create mode 100644 src/modules/trasporti/pages/Settingspage.vue create mode 100644 src/modules/trasporti/pages/Vehicleeditpage.vue create mode 100644 src/modules/trasporti/pages/Vehiclespage.vue create mode 100644 src/modules/trasporti/pages/index.ts create mode 100644 src/modules/trasporti/types/index.ts create mode 100644 src/modules/trasporti/types/trasporti.types.ts create mode 100644 src/router/routesTrasporti.ts diff --git a/quasar.config.ts b/quasar.config.ts index c570aa4f..1c2dd207 100644 --- a/quasar.config.ts +++ b/quasar.config.ts @@ -60,6 +60,7 @@ export default defineConfig((ctx) => { '@': path.resolve(__dirname, 'src'), '@components': path.resolve(__dirname, 'src/components'), '@views': path.resolve(__dirname, 'src/views'), + '@modules': path.resolve(__dirname, 'src/modules'), '@boot': path.resolve(__dirname, 'src/boot'), '@store': path.resolve(__dirname, 'src/store'), '@storemod': path.resolve(__dirname, 'src/store/Modules'), diff --git a/src/components/AIImageGenerator/AIImageGenerator.vue b/src/components/AIImageGenerator/AIImageGenerator.vue index d93f21bd..38b6e172 100644 --- a/src/components/AIImageGenerator/AIImageGenerator.vue +++ b/src/components/AIImageGenerator/AIImageGenerator.vue @@ -390,7 +390,7 @@ const generateImage = async () => { } catch (error: any) { $q.notify({ type: 'negative', - message: error.message || 'Errore durante la generazione', + message: error.data?.message || error.message || 'Errore durante la generazione', icon: 'error' }); } finally { diff --git a/src/components/CMyCircuit/CMyCircuit.vue b/src/components/CMyCircuit/CMyCircuit.vue index 896ca6ac..0bbc99c6 100755 --- a/src/components/CMyCircuit/CMyCircuit.vue +++ b/src/components/CMyCircuit/CMyCircuit.vue @@ -219,7 +219,7 @@ " > {{ - circuit.askManagerToEnter ? t('circuit.ask') : t('circuit.Iscriviti') + circuit.askManagerToEnter ? t('circuit.ask') : t('circuit.iscriviti') }} @@ -436,7 +436,7 @@ " icon="fas fa-user-plus" color="primary" - :label="circuit.askManagerToEnter ? t('circuit.ask') : t('circuit.Iscriviti')" + :label="circuit.askManagerToEnter ? t('circuit.ask') : t('circuit.iscriviti')" rounded size="lg" @click=" diff --git a/src/components/CQRCode/CQRCode.ts b/src/components/CQRCode/CQRCode.ts index 076feb62..c3a9fcaf 100755 --- a/src/components/CQRCode/CQRCode.ts +++ b/src/components/CQRCode/CQRCode.ts @@ -241,7 +241,7 @@ export default defineComponent({ console.error('Errore download QR:', error); $q.notify({ type: 'negative', - message: error.message || 'Errore durante il download', + message: error.data?.message || error.message || 'Errore durante il download', position: 'top', }); emit('error', error); diff --git a/src/components/CSignUp/CSignUp.vue b/src/components/CSignUp/CSignUp.vue index 72552888..088865f9 100755 --- a/src/components/CSignUp/CSignUp.vue +++ b/src/components/CSignUp/CSignUp.vue @@ -262,9 +262,6 @@ ref="inputUsername" tabindex="1" v-model="signup.username" - :readonly=" - tools.getAskToVerifyReg() && !site.confpages?.enableRegMultiChoice - " filled class="modern-input" @blur="v$.username.$touch" diff --git a/src/components/PosterGenerator/PosterGenerator.ts b/src/components/PosterGenerator/PosterGenerator.ts index 4cbddee7..8982c9f4 100644 --- a/src/components/PosterGenerator/PosterGenerator.ts +++ b/src/components/PosterGenerator/PosterGenerator.ts @@ -251,7 +251,7 @@ export function usePosterGenerator() { $q.notify({ type: 'negative', - message: error.message || 'Errore durante la generazione', + message: error.data?.message || error.message || 'Errore durante la generazione', icon: 'error' }); } finally { diff --git a/src/components/PosterGenerator/components/AiImageGenerator/AiImageGenerator.vue b/src/components/PosterGenerator/components/AiImageGenerator/AiImageGenerator.vue index 2ba4271f..c26302ba 100644 --- a/src/components/PosterGenerator/components/AiImageGenerator/AiImageGenerator.vue +++ b/src/components/PosterGenerator/components/AiImageGenerator/AiImageGenerator.vue @@ -390,7 +390,7 @@ const generateImage = async () => { } catch (error: any) { $q.notify({ type: 'negative', - message: error.message || 'Errore durante la generazione', + message: error.data?.message || error.message || 'Errore durante la generazione', icon: 'error' }); } finally { diff --git a/src/components/PosterGenerator/components/AssetManager/AssetManager.vue b/src/components/PosterGenerator/components/AssetManager/AssetManager.vue index 2fbec0a7..f69fe801 100644 --- a/src/components/PosterGenerator/components/AssetManager/AssetManager.vue +++ b/src/components/PosterGenerator/components/AssetManager/AssetManager.vue @@ -168,7 +168,7 @@ const uploadAsset = async (type: 'backgroundImage' | 'mainImage', file: File) => } catch (error: any) { $q.notify({ type: 'negative', - message: error.message || 'Errore durante il caricamento' + message: error.data?.message || error.message || 'Errore durante il caricamento' }); } }; @@ -214,7 +214,7 @@ const handleLogoUpload = async (event: Event) => { } catch (error: any) { $q.notify({ type: 'negative', - message: error.message || 'Errore durante il caricamento' + message: error.data?.message || error.message || 'Errore durante il caricamento' }); } diff --git a/src/components/TemplateBuilder/TemplateBuilder.ts b/src/components/TemplateBuilder/TemplateBuilder.ts index ff5e7b02..6cde800a 100644 --- a/src/components/TemplateBuilder/TemplateBuilder.ts +++ b/src/components/TemplateBuilder/TemplateBuilder.ts @@ -421,7 +421,7 @@ export function useTemplateBuilder() { } catch (error: any) { $q.notify({ type: 'negative', - message: error.message || 'Errore durante il salvataggio', + message: error.data?.message || error.message || 'Errore durante il salvataggio', icon: 'error' }); } finally { @@ -448,7 +448,7 @@ export function useTemplateBuilder() { } catch (error: any) { $q.notify({ type: 'negative', - message: error.message || 'Errore durante la pubblicazione' + message: error.data?.message || error.message || 'Errore durante la pubblicazione' }); } finally { isPublishing.value = false; diff --git a/src/model/GlobalStore.ts b/src/model/GlobalStore.ts index 29f382c5..1a0c344c 100755 --- a/src/model/GlobalStore.ts +++ b/src/model/GlobalStore.ts @@ -298,6 +298,7 @@ export interface IConfPages { enableEcommerce: boolean enableAI: boolean enablePoster: boolean + enableTrasporti: boolean enableGroups: boolean enableCircuits: boolean enableProj?: boolean diff --git a/src/modules/trasporti/components/chat/ChatInput.scss b/src/modules/trasporti/components/chat/ChatInput.scss new file mode 100644 index 00000000..c6dc816b --- /dev/null +++ b/src/modules/trasporti/components/chat/ChatInput.scss @@ -0,0 +1,160 @@ +.chat-input { + background: white; + border-top: 1px solid rgba(0, 0, 0, 0.08); + + &__reply { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: rgba(var(--q-primary-rgb), 0.04); + border-bottom: 1px solid rgba(0, 0, 0, 0.04); + } + + &__reply-content { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + overflow: hidden; + } + + &__reply-info { + display: flex; + flex-direction: column; + gap: 2px; + overflow: hidden; + } + + &__reply-sender { + font-size: 12px; + font-weight: 600; + color: var(--q-primary); + } + + &__reply-text { + font-size: 12px; + color: var(--q-grey-7); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__main { + display: flex; + align-items: flex-end; + gap: 8px; + padding: 8px 12px; + } + + &__field { + flex: 1; + background: #f5f5f5; + border-radius: 24px; + padding: 8px 16px; + + :deep(.q-field__control) { + height: auto; + min-height: 36px; + } + + :deep(.q-field__native) { + padding: 0; + min-height: 20px; + max-height: 120px; + } + + :deep(textarea) { + resize: none; + } + } + + &__send { + width: 44px; + height: 44px; + flex-shrink: 0; + } + + &__emoji-picker { + padding: 8px 12px; + background: #fafafa; + border-top: 1px solid rgba(0, 0, 0, 0.04); + } + + &__emoji-grid { + display: flex; + flex-wrap: wrap; + gap: 4px; + } + + &__emoji { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + background: none; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s ease; + + &:hover { + background: rgba(0, 0, 0, 0.08); + } + } +} + +// Animations +.slide-down-enter-active, +.slide-down-leave-active { + transition: all 0.2s ease; +} + +.slide-down-enter-from, +.slide-down-leave-to { + opacity: 0; + transform: translateY(-10px); +} + +.slide-up-enter-active, +.slide-up-leave-active { + transition: all 0.2s ease; +} + +.slide-up-enter-from, +.slide-up-leave-to { + opacity: 0; + transform: translateY(10px); +} + +// Dark mode +.body--dark { + .chat-input { + background: #1e1e1e; + border-color: rgba(255, 255, 255, 0.08); + + &__reply { + background: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.04); + } + + &__reply-text { + color: rgba(255, 255, 255, 0.6); + } + + &__field { + background: #2d2d2d; + } + + &__emoji-picker { + background: #252525; + border-color: rgba(255, 255, 255, 0.04); + } + + &__emoji:hover { + background: rgba(255, 255, 255, 0.1); + } + } +} \ No newline at end of file diff --git a/src/modules/trasporti/components/chat/ChatInput.ts b/src/modules/trasporti/components/chat/ChatInput.ts new file mode 100644 index 00000000..d95f1112 --- /dev/null +++ b/src/modules/trasporti/components/chat/ChatInput.ts @@ -0,0 +1,114 @@ +import { ref, computed, defineComponent, PropType } from 'vue'; +import type { Message, UserBasic } from '../../types'; + +export default defineComponent({ + name: 'ChatInput', + + props: { + replyTo: { + type: Object as PropType, + default: null + }, + sending: { + type: Boolean, + default: false + }, + disabled: { + type: Boolean, + default: false + } + }, + + emits: ['send', 'cancel-reply', 'share-location', 'share-ride'], + + setup(props, { emit }) { + const inputRef = ref(null); + const message = ref(''); + const showAttachMenu = ref(false); + const showEmojiPicker = ref(false); + + const commonEmojis = [ + '๐Ÿ˜Š', '๐Ÿ˜‚', 'โค๏ธ', '๐Ÿ‘', '๐Ÿ™', '๐Ÿ˜', '๐ŸŽ‰', '๐Ÿ”ฅ', + '๐Ÿ˜ข', '๐Ÿ˜ฎ', '๐Ÿค”', '๐Ÿ‘', '๐Ÿ’ช', 'โœจ', '๐Ÿš—', '๐Ÿ“', + 'โฐ', 'โœ…', 'โŒ', '๐Ÿ‘‹', '๐Ÿ™‚', '๐Ÿ˜‰', '๐Ÿค', '๐Ÿ’ฏ' + ]; + + // Computed + const canSend = computed(() => { + return message.value.trim().length > 0 && !props.sending && !props.disabled; + }); + + const replyToSenderName = computed(() => { + if (!props.replyTo) return ''; + const sender = props.replyTo.senderId; + if (typeof sender === 'object') { + return (sender as UserBasic).name || (sender as UserBasic).username || 'Utente'; + } + return 'Utente'; + }); + + const replyToText = computed(() => { + if (!props.replyTo) return ''; + const text = props.replyTo.text || ''; + return text.length > 60 ? text.substring(0, 60) + '...' : text; + }); + + // Methods + const send = () => { + if (!canSend.value) return; + + emit('send', { + text: message.value.trim(), + replyTo: props.replyTo?._id + }); + + message.value = ''; + showEmojiPicker.value = false; + }; + + const newLine = () => { + message.value += '\n'; + }; + + const insertEmoji = (emoji: string) => { + message.value += emoji; + inputRef.value?.focus(); + }; + + const shareLocation = () => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + emit('share-location', { + lat: position.coords.latitude, + lng: position.coords.longitude + }); + }, + (error) => { + console.error('Errore geolocalizzazione:', error); + } + ); + } + }; + + const focus = () => { + inputRef.value?.focus(); + }; + + return { + inputRef, + message, + showAttachMenu, + showEmojiPicker, + commonEmojis, + canSend, + replyToSenderName, + replyToText, + send, + newLine, + insertEmoji, + shareLocation, + focus + }; + } +}); diff --git a/src/modules/trasporti/components/chat/ChatInput.vue b/src/modules/trasporti/components/chat/ChatInput.vue new file mode 100644 index 00000000..c6fc65a0 --- /dev/null +++ b/src/modules/trasporti/components/chat/ChatInput.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/src/modules/trasporti/components/feedback/index.ts b/src/modules/trasporti/components/feedback/index.ts new file mode 100644 index 00000000..c21f3bf2 --- /dev/null +++ b/src/modules/trasporti/components/feedback/index.ts @@ -0,0 +1,5 @@ +// Export all feedback components +export { default as FeedbackForm } from './FeedbackForm.vue'; +export { default as FeedbackCard } from './FeedbackCard.vue'; +export { default as FeedbackList } from './FeedbackList.vue'; +export { default as FeedbackSummary } from './FeedbackSummary.vue'; diff --git a/src/modules/trasporti/components/ride/CityAutocomplete.vue b/src/modules/trasporti/components/ride/CityAutocomplete.vue new file mode 100644 index 00000000..ee8477c2 --- /dev/null +++ b/src/modules/trasporti/components/ride/CityAutocomplete.vue @@ -0,0 +1,376 @@ + + + + + + diff --git a/src/modules/trasporti/components/ride/ContribTypeSelector.scss b/src/modules/trasporti/components/ride/ContribTypeSelector.scss new file mode 100644 index 00000000..ea54c443 --- /dev/null +++ b/src/modules/trasporti/components/ride/ContribTypeSelector.scss @@ -0,0 +1,138 @@ +.contrib-selector { + width: 100%; + + &__header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + } + + &__title { + font-weight: 600; + font-size: 16px; + } + + &__loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 32px; + gap: 12px; + color: var(--q-grey); + } + + &__types { + display: flex; + flex-direction: column; + gap: 8px; + } + + &__type { + border: 2px solid transparent; + border-radius: 12px; + padding: 12px; + background: rgba(0, 0, 0, 0.02); + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: rgba(0, 0, 0, 0.04); + } + + &--selected { + border-color: var(--q-primary); + background: rgba(var(--q-primary-rgb), 0.04); + } + } + + &__type-header { + display: flex; + align-items: center; + gap: 12px; + } + + &__type-icon { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + font-size: 20px; + } + + &__type-label { + font-weight: 500; + font-size: 15px; + } + + &__price-input { + margin-top: 12px; + padding: 12px; + background: rgba(0, 0, 0, 0.02); + border-radius: 8px; + } + + &__options { + .q-toggle { + margin-bottom: 4px; + } + } + + &__summary { + margin-top: 16px; + padding: 16px; + background: linear-gradient(135deg, rgba(var(--q-primary-rgb), 0.08), rgba(var(--q-secondary-rgb), 0.08)); + border-radius: 12px; + } + + &__summary-title { + font-weight: 600; + font-size: 13px; + margin-bottom: 12px; + } + + &__summary-items { + display: flex; + flex-wrap: wrap; + gap: 8px; + } +} + +// Expand animation +.expand-enter-active, +.expand-leave-active { + transition: all 0.3s ease; + overflow: hidden; +} + +.expand-enter-from, +.expand-leave-to { + opacity: 0; + max-height: 0; + margin-top: 0; + padding: 0; +} + +// Dark mode +.body--dark { + .contrib-selector { + &__type { + background: rgba(255, 255, 255, 0.04); + + &:hover { + background: rgba(255, 255, 255, 0.08); + } + + &--selected { + background: rgba(var(--q-primary-rgb), 0.12); + } + } + + &__price-input { + background: rgba(255, 255, 255, 0.04); + } + } +} \ No newline at end of file diff --git a/src/modules/trasporti/components/ride/ContribTypeSelector.ts b/src/modules/trasporti/components/ride/ContribTypeSelector.ts new file mode 100644 index 00000000..341c6dbd --- /dev/null +++ b/src/modules/trasporti/components/ride/ContribTypeSelector.ts @@ -0,0 +1,252 @@ +import { ref, reactive, computed, watch, onMounted, defineComponent, PropType } from 'vue'; +import { useContribTypes } from '../../composables/useContribTypes'; +import type { Contribution, ContributionItem, ContribType } from '../../types'; + +interface SelectedItem { + contribTypeId: string; + price?: number; + pricePerKm?: number; + isPerKm?: boolean; + notes?: string; +} + +export default defineComponent({ + name: 'ContribTypeSelector', + + props: { + modelValue: { + type: Object as PropType, + default: () => ({ contribTypes: [] }) + } + }, + + emits: ['update:modelValue'], + + setup(props, { emit }) { + const { + contribTypes, + loading, + fetchContribTypes, + findById, + formatPrice, + requiresPrice: checkRequiresPrice + } = useContribTypes(); + + // State + const selectedItems = ref([]); + const localContribution = reactive>({ + negotiable: true, + freeForStudents: false, + freeForElders: false + }); + + // โœ… Flag to prevent circular updates + let isInternalUpdate = false; + + // Fetch contrib types on mount + onMounted(async () => { + await fetchContribTypes(); + }); + + // Watch per sincronizzare con modelValue (from parent) + watch(() => props.modelValue, (newVal) => { + // โœ… Skip if this update was triggered by us + if (isInternalUpdate) return; + + if (newVal) { + localContribution.negotiable = newVal.negotiable ?? true; + localContribution.freeForStudents = newVal.freeForStudents ?? false; + localContribution.freeForElders = newVal.freeForElders ?? false; + + if (newVal.contribTypes) { + // โœ… Create new array + selectedItems.value = newVal.contribTypes.map(ct => ({ + contribTypeId: typeof ct.contribTypeId === 'string' ? ct.contribTypeId : ct.contribTypeId._id, + price: ct.price ?? ct.pricePerKm, + pricePerKm: ct.pricePerKm, + isPerKm: !!ct.pricePerKm, + notes: ct.notes + })); + } + } + }, { immediate: true, deep: true }); + + // โœ… Emit updates to parent + const emitUpdate = () => { + isInternalUpdate = true; + + const contribution: Contribution = { + contribTypes: selectedItems.value.map(item => ({ + contribTypeId: item.contribTypeId, + price: item.isPerKm ? undefined : item.price, + pricePerKm: item.isPerKm ? item.price : undefined, + notes: item.notes + })), + negotiable: localContribution.negotiable, + freeForStudents: localContribution.freeForStudents, + freeForElders: localContribution.freeForElders + }; + + emit('update:modelValue', contribution); + + // โœ… Reset flag after next tick + setTimeout(() => { + isInternalUpdate = false; + }, 0); + }; + + // Watch local changes and emit (debounced to avoid rapid updates) + watch( + [selectedItems, () => localContribution.negotiable, () => localContribution.freeForStudents, () => localContribution.freeForElders], + () => { + if (!isInternalUpdate) { + emitUpdate(); + } + }, + { deep: true } + ); + + // Methods + const isSelected = (contribTypeId: string): boolean => { + return selectedItems.value.some(item => item.contribTypeId === contribTypeId); + }; + + // โœ… Fixed: Create new array instead of mutating + const toggleContribType = (contribType: ContribType) => { + const exists = selectedItems.value.some(item => item.contribTypeId === contribType._id); + + if (!exists) { + // Add - create new array + selectedItems.value = [ + ...selectedItems.value, + { + contribTypeId: contribType._id, + price: undefined, + isPerKm: false, + notes: '' + } + ]; + } else { + // Remove - create new array + selectedItems.value = selectedItems.value.filter( + item => item.contribTypeId !== contribType._id + ); + } + }; + + const getSelectedItem = (contribTypeId: string): SelectedItem => { + return selectedItems.value.find(item => item.contribTypeId === contribTypeId) || { + contribTypeId, + price: undefined, + isPerKm: false, + notes: '' + }; + }; + + // โœ… Fixed: Update item without mutating + const updateSelectedItem = (contribTypeId: string, updates: Partial) => { + selectedItems.value = selectedItems.value.map(item => + item.contribTypeId === contribTypeId + ? { ...item, ...updates } + : item + ); + }; + + const requiresPrice = (contribType: ContribType): boolean => { + const label = contribType.label.toLowerCase(); + const noPriceTypes = ['dono', 'baratto', 'scambio lavoro']; + return !noPriceTypes.includes(label); + }; + + const showPerKmOption = (contribType: ContribType): boolean => { + const label = contribType.label.toLowerCase(); + return label.includes('euro') || label === 'ris'; + }; + + const getPriceLabel = (contribType: ContribType): string => { + const label = contribType.label.toLowerCase(); + if (label.includes('euro')) return 'Prezzo in Euro'; + if (label === 'ris') return 'Crediti RIS'; + if (label === 'banca del tempo') return 'Ore'; + return 'Valore'; + }; + + const getPricePrefix = (contribType: ContribType): string => { + const label = contribType.label.toLowerCase(); + if (label.includes('euro')) return 'โ‚ฌ'; + if (label.includes('bitcoin')) return 'โ‚ฟ'; + return ''; + }; + + const getPriceSuffix = (contribType: ContribType): string => { + const label = contribType.label.toLowerCase(); + if (label === 'ris') return 'RIS'; + if (label === 'banca del tempo') return 'ore'; + return ''; + }; + + const getContribIcon = (contribTypeId: string): string => { + const type = findById(contribTypeId); + return type?.icon || '๐Ÿ’ฐ'; + }; + + const getContribColor = (contribTypeId: string): string => { + const type = findById(contribTypeId); + return type?.color || '#9e9e9e'; + }; + + const formatContribPrice = (item: SelectedItem): string => { + const type = findById(item.contribTypeId); + if (!type) return ''; + + const label = type.label.toLowerCase(); + + if (!requiresPrice(type)) { + return type.label; + } + + if (item.price === undefined) { + return type.label; + } + + let priceStr = ''; + if (label.includes('euro')) { + priceStr = `โ‚ฌ${item.price}`; + } else if (label === 'ris') { + priceStr = `${item.price} RIS`; + } else if (label === 'banca del tempo') { + priceStr = `${item.price} ore`; + } else { + priceStr = `${item.price}`; + } + + if (item.isPerKm) { + priceStr += '/km'; + } + + return priceStr; + }; + + return { + // State + contribTypes, + loading, + selectedItems, + localContribution, + + // Methods + isSelected, + toggleContribType, + getSelectedItem, + updateSelectedItem, // โœ… Add this for template use + requiresPrice, + showPerKmOption, + getPriceLabel, + getPricePrefix, + getPriceSuffix, + getContribIcon, + getContribColor, + formatContribPrice + }; + } +}); diff --git a/src/modules/trasporti/components/ride/ContribTypeSelector.vue b/src/modules/trasporti/components/ride/ContribTypeSelector.vue new file mode 100644 index 00000000..f0a846b0 --- /dev/null +++ b/src/modules/trasporti/components/ride/ContribTypeSelector.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/src/modules/trasporti/components/ride/PreferencesSelector.scss b/src/modules/trasporti/components/ride/PreferencesSelector.scss new file mode 100644 index 00000000..1fd370aa --- /dev/null +++ b/src/modules/trasporti/components/ride/PreferencesSelector.scss @@ -0,0 +1,134 @@ +.preferences-selector { + width: 100%; + + &__header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + } + + &__title { + font-weight: 600; + font-size: 16px; + } + + &__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + } + + &__item { + background: rgba(0, 0, 0, 0.02); + border-radius: 12px; + padding: 12px; + + &--toggle { + display: flex; + flex-direction: column; + justify-content: space-between; + } + } + + &__item-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + } + + &__item-icon { + font-size: 18px; + } + + &__item-label { + font-weight: 500; + font-size: 13px; + color: var(--q-grey-8); + } + + &__select { + :deep(.q-field__control) { + border-radius: 8px; + } + } + + &__packages-options { + margin-top: 16px; + padding: 16px; + background: rgba(var(--q-secondary-rgb), 0.08); + border-radius: 12px; + } + + &__label { + font-size: 13px; + font-weight: 500; + color: var(--q-grey-7); + margin-bottom: 8px; + } + + &__toggles { + .preferences-selector__toggle-item { + border-radius: 12px; + margin-bottom: 4px; + + &:hover { + background: rgba(0, 0, 0, 0.02); + } + } + } + + &__notes { + :deep(.q-field__control) { + border-radius: 12px; + } + } + + &__summary { + margin-top: 16px; + padding: 16px; + background: rgba(var(--q-primary-rgb), 0.04); + border-radius: 12px; + } + + &__summary-title { + font-weight: 600; + font-size: 13px; + margin-bottom: 8px; + } + + &__summary-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + } +} + +// Expand animation +.expand-enter-active, +.expand-leave-active { + transition: all 0.3s ease; + overflow: hidden; +} + +.expand-enter-from, +.expand-leave-to { + opacity: 0; + max-height: 0; + padding: 0; + margin: 0; +} + +// Dark mode +.body--dark { + .preferences-selector { + &__item { + background: rgba(255, 255, 255, 0.04); + } + + &__item-label { + color: rgba(255, 255, 255, 0.8); + } + } +} \ No newline at end of file diff --git a/src/modules/trasporti/components/ride/PreferencesSelector.ts b/src/modules/trasporti/components/ride/PreferencesSelector.ts new file mode 100644 index 00000000..98100e55 --- /dev/null +++ b/src/modules/trasporti/components/ride/PreferencesSelector.ts @@ -0,0 +1,146 @@ +import { reactive, computed, watch, defineComponent, PropType } from 'vue'; +import type { RidePreferences } from '../../types'; +import { + SMOKING_OPTIONS, + PETS_OPTIONS, + LUGGAGE_OPTIONS, + MUSIC_OPTIONS, + CONVERSATION_OPTIONS +} from '../../types'; + +export default defineComponent({ + name: 'PreferencesSelector', + + props: { + modelValue: { + type: Object as PropType, + default: () => ({}) + } + }, + + emits: ['update:modelValue'], + + setup(props, { emit }) { + // State + const localPreferences = reactive({ + smoking: 'no', + pets: 'small', + luggage: 'medium', + music: 'moderate', + conversation: 'moderate', + packages: false, + maxPackageSize: 'medium', + foodAllowed: true, + childrenFriendly: true, + wheelchairAccessible: false, + otherPreferences: '' + }); + + // Options con icone + const smokingOptions = SMOKING_OPTIONS.map(opt => ({ + label: `${opt.icon} ${opt.label}`, + value: opt.value + })); + + const petsOptions = PETS_OPTIONS.map(opt => ({ + label: `${opt.icon} ${opt.label}`, + value: opt.value + })); + + const luggageOptions = LUGGAGE_OPTIONS.map(opt => ({ + label: `${opt.icon} ${opt.label}`, + value: opt.value + })); + + const musicOptions = MUSIC_OPTIONS.map(opt => ({ + label: `${opt.icon} ${opt.label}`, + value: opt.value + })); + + const conversationOptions = CONVERSATION_OPTIONS.map(opt => ({ + label: `${opt.icon} ${opt.label}`, + value: opt.value + })); + + const packageSizeOptions = [ + { label: '๐Ÿ“ฆ Piccolo', value: 'small' }, + { label: '๐Ÿ“ฆ๐Ÿ“ฆ Medio', value: 'medium' }, + { label: '๐Ÿ“ฆ๐Ÿ“ฆ๐Ÿ“ฆ Grande', value: 'large' }, + { label: '๐Ÿšš XL', value: 'xlarge' } + ]; + + // Watch per sincronizzare con modelValue + watch(() => props.modelValue, (newVal) => { + if (newVal) { + Object.assign(localPreferences, newVal); + } + }, { immediate: true, deep: true }); + + // Watch per emettere update + watch(localPreferences, (newVal) => { + emit('update:modelValue', { ...newVal }); + }, { deep: true }); + + // Computed + const hasPreferences = computed(() => { + return localPreferences.smoking !== 'no' || + localPreferences.pets !== 'no' || + localPreferences.packages || + localPreferences.wheelchairAccessible || + localPreferences.otherPreferences; + }); + + const activePreferencesChips = computed(() => { + const chips: { key: string; icon: string; label: string; color: string }[] = []; + + // Fumatori + if (localPreferences.smoking === 'no') { + chips.push({ key: 'smoking', icon: '๐Ÿšญ', label: 'Non fumatori', color: 'positive' }); + } else if (localPreferences.smoking === 'yes') { + chips.push({ key: 'smoking', icon: '๐Ÿšฌ', label: 'Fumatori OK', color: 'grey' }); + } + + // Animali + if (localPreferences.pets !== 'no') { + const petLabel = localPreferences.pets === 'all' ? 'Tutti' : + localPreferences.pets === 'small' ? 'Piccoli' : + localPreferences.pets === 'medium' ? 'Medi' : 'Grandi'; + chips.push({ key: 'pets', icon: '๐Ÿพ', label: `Animali: ${petLabel}`, color: 'amber' }); + } + + // Bagagli + if (localPreferences.luggage && localPreferences.luggage !== 'none') { + chips.push({ key: 'luggage', icon: '๐Ÿงณ', label: `Bagagli: ${localPreferences.luggage}`, color: 'info' }); + } + + // Pacchi + if (localPreferences.packages) { + chips.push({ key: 'packages', icon: '๐Ÿ“ฆ', label: 'Pacchi OK', color: 'secondary' }); + } + + // Accessibile + if (localPreferences.wheelchairAccessible) { + chips.push({ key: 'wheelchair', icon: 'โ™ฟ', label: 'Accessibile', color: 'primary' }); + } + + return chips; + }); + + return { + // State + localPreferences, + + // Options + smokingOptions, + petsOptions, + luggageOptions, + musicOptions, + conversationOptions, + packageSizeOptions, + + // Computed + hasPreferences, + activePreferencesChips + }; + } +}); diff --git a/src/modules/trasporti/components/ride/PreferencesSelector.vue b/src/modules/trasporti/components/ride/PreferencesSelector.vue new file mode 100644 index 00000000..155c2bb4 --- /dev/null +++ b/src/modules/trasporti/components/ride/PreferencesSelector.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/src/modules/trasporti/components/ride/RideCard.scss b/src/modules/trasporti/components/ride/RideCard.scss new file mode 100644 index 00000000..d4375fed --- /dev/null +++ b/src/modules/trasporti/components/ride/RideCard.scss @@ -0,0 +1,356 @@ +.ride-card { + position: relative; + border-radius: 16px !important; + transition: all 0.3s ease; + cursor: pointer; + overflow: visible; + + &:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important; + } + + &--offer { + border-left: 4px solid #4caf50 !important; + } + + &--request { + border-left: 4px solid #f44336 !important; + } + + &--featured { + background: linear-gradient(135deg, #fff9c4 0%, #fff 30%); + border-color: #ffc107 !important; + } + + &--compact { + .ride-card__header { + padding: 12px; + } + + .ride-card__route { + padding: 8px 12px; + } + + .ride-card__footer { + padding: 8px 12px; + } + } + + // Type Badge + &__type-badge { + position: absolute; + top: -8px; + left: 16px; + display: flex; + align-items: center; + gap: 4px; + background: white; + padding: 4px 12px; + border-radius: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + z-index: 1; + } + + &__type-icon { + font-size: 12px; + } + + &__type-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + &__status { + top: 8px !important; + right: 8px !important; + } + + // Header + &__header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 16px; + padding-top: 20px; + } + + &__user { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + + &:hover .ride-card__user-name { + color: var(--q-primary); + } + } + + &__avatar { + background: linear-gradient(135deg, var(--q-primary), var(--q-secondary)); + color: white; + font-weight: 600; + font-size: 14px; + } + + &__user-info { + display: flex; + flex-direction: column; + } + + &__user-name { + font-weight: 600; + font-size: 15px; + transition: color 0.2s ease; + } + + &__user-rating { + display: flex; + align-items: center; + gap: 4px; + font-size: 13px; + color: var(--q-grey); + } + + &__datetime { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: var(--q-grey-7); + background: rgba(0, 0, 0, 0.04); + padding: 6px 12px; + border-radius: 20px; + } + + // Route + &__route { + padding: 16px; + } + + &__route-visual { + display: flex; + flex-direction: column; + gap: 0; + } + + &__location { + display: flex; + align-items: center; + gap: 12px; + + &--start { + .ride-card__location-dot { + background: #4caf50; + } + } + + &--end { + .ride-card__location-dot { + background: #f44336; + } + } + } + + &__location-dot { + width: 12px; + height: 12px; + border-radius: 50%; + flex-shrink: 0; + position: relative; + + &::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 20px; + height: 20px; + border-radius: 50%; + background: inherit; + opacity: 0.2; + } + } + + &__location-info { + display: flex; + flex-direction: column; + flex: 1; + } + + &__location-city { + font-weight: 600; + font-size: 16px; + } + + &__location-time { + font-size: 12px; + color: var(--q-grey); + } + + &__route-line { + display: flex; + align-items: center; + padding-left: 5px; + margin: 4px 0; + min-height: 24px; + } + + &__route-line-inner { + width: 2px; + height: 100%; + min-height: 24px; + background: repeating-linear-gradient( + to bottom, + var(--q-grey-4) 0px, + var(--q-grey-4) 4px, + transparent 4px, + transparent 8px + ); + } + + &__waypoints-indicator { + display: flex; + align-items: center; + gap: 4px; + margin-left: 12px; + font-size: 12px; + color: var(--q-primary); + cursor: pointer; + padding: 4px 8px; + border-radius: 12px; + transition: background 0.2s ease; + + &:hover { + background: rgba(var(--q-primary-rgb), 0.1); + } + } + + &__waypoints-list { + margin-top: 8px; + padding-left: 24px; + display: flex; + flex-direction: column; + gap: 4px; + } + + &__waypoint { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--q-grey-7); + } + + // Info + &__info { + padding: 12px 16px; + } + + &__info-grid { + display: flex; + flex-wrap: wrap; + gap: 16px; + } + + &__info-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: var(--q-grey-8); + } + + &__preferences { + display: flex; + align-items: center; + gap: 8px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px dashed var(--q-grey-4); + } + + &__pref-icon { + font-size: 16px; + cursor: help; + } + + // Footer + &__footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: rgba(0, 0, 0, 0.02); + } + + &__contribution { + display: flex; + flex-direction: column; + } + + &__price { + font-size: 20px; + font-weight: 700; + color: var(--q-primary); + } + + &__price-label { + font-size: 11px; + color: var(--q-grey); + } + + &__price-free { + font-size: 16px; + font-weight: 600; + color: #4caf50; + } + + &__contrib-types { + display: flex; + flex-wrap: wrap; + gap: 4px; + } +} + +// Expand animation +.expand-enter-active, +.expand-leave-active { + transition: all 0.3s ease; + overflow: hidden; +} + +.expand-enter-from, +.expand-leave-to { + opacity: 0; + max-height: 0; +} + +.expand-enter-to, +.expand-leave-from { + max-height: 200px; +} + +// Dark mode +.body--dark { + .ride-card { + &__type-badge { + background: #2d2d2d; + } + + &__datetime { + background: rgba(255, 255, 255, 0.08); + } + + &__footer { + background: rgba(255, 255, 255, 0.04); + } + + &--featured { + background: linear-gradient(135deg, rgba(255, 193, 7, 0.15) 0%, transparent 30%); + } + } +} \ No newline at end of file diff --git a/src/modules/trasporti/components/ride/RideCard.ts b/src/modules/trasporti/components/ride/RideCard.ts new file mode 100644 index 00000000..f688190d --- /dev/null +++ b/src/modules/trasporti/components/ride/RideCard.ts @@ -0,0 +1,286 @@ +import { ref, computed, defineComponent, PropType } from 'vue'; +import { useRides } from '../../composables/useRides'; +import type { Ride, ContributionItem, RideStatus } from '../../types'; + +export default defineComponent({ + name: 'RideCard', + + props: { + ride: { + type: Object as PropType, + required: true + }, + compact: { + type: Boolean, + default: false + }, + showStatus: { + type: Boolean, + default: false + }, + showBookButton: { + type: Boolean, + default: true + }, + showContactButton: { + type: Boolean, + default: false + }, + currentUserId: { + type: String, + default: '' + }, + clickable: { + type: Boolean, + default: true + } + }, + + emits: ['click', 'book', 'contact', 'user-click'], + + setup(props, { emit }) { + const { + formatRideDate, + formatDuration, + getStatusColor, + getStatusLabel, + canBook: checkCanBook + } = useRides(); + + // State + const showWaypoints = ref(false); + + // User computed + const user = computed(() => { + if (typeof props.ride.userId === 'object') { + return props.ride.userId; + } + return null; + }); + + const userName = computed(() => { + if (user.value) { + if (user.value.name) { + return `${user.value.name} ${user.value.surname?.[0] || ''}`.trim(); + } + return user.value.username; + } + return 'Utente'; + }); + + const userInitials = computed(() => { + const name = userName.value; + return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2); + }); + + const userImg = computed(() => { + return user.value?.profile?.img; + }); + + const userRating = computed(() => { + return user.value?.profile?.driverProfile?.averageRating || 0; + }); + + const userRidesCount = computed(() => { + return user.value?.profile?.driverProfile?.ridesCompletedAsDriver || 0; + }); + + // Date computed + const formattedDate = computed(() => { + return formatRideDate(props.ride.dateTime); + }); + + const formattedDepartureTime = computed(() => { + const date = new Date(props.ride.dateTime); + return date.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); + }); + + const estimatedArrival = computed(() => { + if (!props.ride.estimatedDuration) return null; + const departure = new Date(props.ride.dateTime); + const arrival = new Date(departure.getTime() + props.ride.estimatedDuration * 60000); + return arrival.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); + }); + + const formattedDuration = computed(() => { + if (!props.ride.estimatedDuration) return ''; + return formatDuration(props.ride.estimatedDuration); + }); + + // Route computed + const waypointsCount = computed(() => { + return props.ride.waypoints?.length || 0; + }); + + // Seats computed + const seatsColor = computed(() => { + if (!props.ride.passengers) return 'grey'; + const available = props.ride.passengers.available; + const max = props.ride.passengers.max; + if (available === 0) return 'negative'; + if (available <= max * 0.3) return 'warning'; + return 'positive'; + }); + + // Preferences + const hasPreferences = computed(() => { + const prefs = props.ride.preferences; + if (!prefs) return false; + return prefs.smoking === 'no' || prefs.pets !== 'no' || prefs.packages; + }); + + // Contribution computed + const mainContribution = computed(() => { + const contribs = props.ride.contribution?.contribTypes; + if (!contribs || contribs.length === 0) return null; + + // Prioritร : Euro > RIS > altri con prezzo + const euroContrib = contribs.find(c => { + const label = getContribLabel(c).toLowerCase(); + return label.includes('euro') && c.price; + }); + if (euroContrib) return euroContrib; + + const risContrib = contribs.find(c => { + const label = getContribLabel(c).toLowerCase(); + return label === 'ris' && c.price; + }); + if (risContrib) return risContrib; + + return contribs.find(c => c.price) || null; + }); + + const formattedPrice = computed(() => { + if (!mainContribution.value) return ''; + const price = mainContribution.value.price; + const label = getContribLabel(mainContribution.value).toLowerCase(); + + if (label.includes('euro')) return `โ‚ฌ${price?.toFixed(2)}`; + if (label === 'ris') return `${price} RIS`; + return `${price}`; + }); + + const priceLabel = computed(() => { + if (!mainContribution.value) return ''; + if (mainContribution.value.pricePerKm) return 'per km'; + return 'a persona'; + }); + + // Book button + const canBook = computed(() => { + return checkCanBook(props.ride, props.currentUserId); + }); + + const bookButtonLabel = computed(() => { + if (props.ride.status === 'full') return 'Completo'; + if (!canBook.value) return 'Non disponibile'; + return 'Prenota'; + }); + + // Methods + const toggleWaypoints = () => { + showWaypoints.value = !showWaypoints.value; + }; + + const getContribId = (contrib: ContributionItem): string => { + if (typeof contrib.contribTypeId === 'object') { + return contrib.contribTypeId._id; + } + return contrib.contribTypeId; + }; + + const getContribLabel = (contrib: ContributionItem): string => { + if (typeof contrib.contribTypeId === 'object') { + return contrib.contribTypeId.label; + } + return ''; + }; + + const getContribIcon = (contrib: ContributionItem): string => { + if (typeof contrib.contribTypeId === 'object') { + return contrib.contribTypeId.icon; + } + return '๐Ÿ’ฐ'; + }; + + const getContribColor = (contrib: ContributionItem): string => { + if (typeof contrib.contribTypeId === 'object') { + return contrib.contribTypeId.color; + } + return 'grey'; + }; + + // Events + const onClick = () => { + if (props.clickable) { + emit('click', props.ride); + } + }; + + const onBook = () => { + emit('book', props.ride); + }; + + const onContact = () => { + emit('contact', props.ride); + }; + + const onUserClick = () => { + const userId = typeof props.ride.userId === 'object' + ? props.ride.userId._id + : props.ride.userId; + emit('user-click', userId); + }; + + return { + // State + showWaypoints, + + // User + userName, + userInitials, + userImg, + userRating, + userRidesCount, + + // Date + formattedDate, + formattedDepartureTime, + estimatedArrival, + formattedDuration, + + // Route + waypointsCount, + + // Seats + seatsColor, + + // Preferences + hasPreferences, + + // Contribution + mainContribution, + formattedPrice, + priceLabel, + + // Book + canBook, + bookButtonLabel, + + // Methods + toggleWaypoints, + getStatusColor, + getStatusLabel, + getContribId, + getContribLabel, + getContribIcon, + getContribColor, + + // Events + onClick, + onBook, + onContact, + onUserClick + }; + } +}); diff --git a/src/modules/trasporti/components/ride/RideCard.vue b/src/modules/trasporti/components/ride/RideCard.vue new file mode 100644 index 00000000..804ed702 --- /dev/null +++ b/src/modules/trasporti/components/ride/RideCard.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/src/modules/trasporti/pages/MyRidesPage.scss b/src/modules/trasporti/pages/MyRidesPage.scss new file mode 100644 index 00000000..1c2d99cd --- /dev/null +++ b/src/modules/trasporti/pages/MyRidesPage.scss @@ -0,0 +1,146 @@ +.my-rides-page { + min-height: 100vh; + background: #f5f5f5; + + &__container { + max-width: 900px; + margin: 0 auto; + padding: 16px; + } + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + + @media (max-width: 599px) { + .q-btn { + display: none; + } + } + } + + &__title { + font-size: 28px; + font-weight: 700; + margin: 0; + } + + &__tabs { + background: white; + border-radius: 12px; + margin-bottom: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + } + + &__sub-tabs { + background: rgba(0, 0, 0, 0.02); + border-radius: 8px; + margin-bottom: 16px; + } + + &__filters { + display: flex; + gap: 8px; + margin-bottom: 16px; + flex-wrap: wrap; + } + + &__panels { + background: transparent; + + .q-tab-panel { + padding: 0; + } + } + + &__loading { + padding: 16px 0; + } + + &__empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 64px 24px; + text-align: center; + + &--small { + padding: 32px; + } + + h3 { + font-size: 20px; + font-weight: 600; + margin: 16px 0 8px; + } + + p { + font-size: 14px; + color: var(--q-grey-6); + margin: 0 0 24px; + } + + span { + margin-top: 12px; + color: var(--q-grey-6); + } + } + + &__empty-icon { + font-size: 64px; + } + + &__list { + display: flex; + flex-direction: column; + gap: 16px; + } + + &__requests-list { + background: white; + border-radius: 12px; + overflow: hidden; + } + + &__requests-dialog { + border-radius: 16px 16px 0 0; + } +} + +// List transition +.list-enter-active, +.list-leave-active { + transition: all 0.3s ease; +} + +.list-enter-from { + opacity: 0; + transform: translateX(-20px); +} + +.list-leave-to { + opacity: 0; + transform: translateX(20px); +} + +// Dark mode +.body--dark { + .my-rides-page { + background: #121212; + + &__tabs { + background: #1e1e1e; + } + + &__sub-tabs { + background: rgba(255, 255, 255, 0.04); + } + + &__requests-list { + background: #1e1e1e; + } + } +} \ No newline at end of file diff --git a/src/modules/trasporti/pages/MyRidesPage.ts b/src/modules/trasporti/pages/MyRidesPage.ts new file mode 100644 index 00000000..a61eb882 --- /dev/null +++ b/src/modules/trasporti/pages/MyRidesPage.ts @@ -0,0 +1,298 @@ +import { ref, computed, onMounted, watch, defineComponent } from 'vue'; +import { useRouter } from 'vue-router'; +import { useQuasar } from 'quasar'; +import { useRides } from '../composables/useRides'; +import { useRideRequests } from '../composables/useRideRequests'; +import MyRideCard from '../components/ride/MyRideCard.vue'; +import RequestCard from '../components/ride/RequestCard.vue'; +import type { Ride, RideRequest } from '../types'; +import { useUserStore } from 'app/src/store'; + +export default defineComponent({ + name: 'MyRidesPage', + + components: { + MyRideCard, + RequestCard + }, + + setup() { + const router = useRouter(); + const $q = useQuasar(); + + const userStore = useUserStore() + + const { + myRides, + loading, + fetchMyRides, + deleteRide, + completeRide: completeRideApi + } = useRides(); + + const { + receivedRequests, + sentRequests, + loading: loadingRequests, + requestCounts, + fetchReceivedRequests, + fetchSentRequests, + acceptRequest: acceptRequestApi, + rejectRequest: rejectRequestApi, + cancelRequest: cancelRequestApi + } = useRideRequests(); + + // State + const activeTab = ref('upcoming'); + const requestsSubTab = ref('received'); + const roleFilter = ref<'all' | 'driver' | 'passenger'>('all'); + const showRequestsDialog = ref(false); + const selectedRide = ref(null); + const selectedRideRequests = ref([]); + const currentUserId = ref(userStore.my._id); + + // Filters + const roleFilters = [ + { label: 'Tutti', value: 'all' }, + { label: '๐Ÿš— Come conducente', value: 'driver' }, + { label: '๐Ÿ‘ค Come passeggero', value: 'passenger' } + ]; + + // Computed + const upcomingCount = computed(() => myRides.upcoming.length); + const pendingRequestsCount = computed(() => requestCounts.value.pending); + + const filteredUpcoming = computed(() => { + if (roleFilter.value === 'all') return myRides.upcoming; + if (roleFilter.value === 'driver') { + return myRides.upcoming.filter(r => isDriver(r)); + } + return myRides.upcoming.filter(r => !isDriver(r)); + }); + + const filteredPast = computed(() => { + if (roleFilter.value === 'all') return myRides.past; + if (roleFilter.value === 'driver') { + return myRides.past.filter(r => isDriver(r)); + } + return myRides.past.filter(r => !isDriver(r)); + }); + + // Methods + const isDriver = (ride: Ride): boolean => { + const userId = typeof ride.userId === 'string' ? ride.userId : ride.userId._id; + return userId === currentUserId.value; + }; + + const getPendingRequests = (rideId: string): number => { + return receivedRequests.value.filter(r => { + const reqRideId = typeof r.rideId === 'string' ? r.rideId : r.rideId._id; + return reqRideId === rideId && r.status === 'pending'; + }).length; + }; + + const canLeaveFeedback = (ride: Ride): boolean => { + // TODO: Implement logic to check if user can leave feedback + return ride.status === 'completed'; + }; + + const goToCreate = () => { + router.push({ path: '/trasporti/richiedi' }); + }; + + const goToRide = (rideId: string) => { + router.push(`/trasporti/viaggio/${rideId}`); + }; + + const goToProfile = (userId: string) => { + router.push(`/trasporti/profilo/${userId}`); + }; + + const editRide = (rideId: string) => { + router.push(`/trasporti/viaggio/${rideId}/modifica`); + }; + + const cancelRide = async (ride: Ride) => { + $q.dialog({ + title: 'Cancella Viaggio', + message: 'Sei sicuro di voler cancellare questo viaggio?', + prompt: { + model: '', + type: 'text', + label: 'Motivo (opzionale)' + }, + cancel: true + }).onOk(async (reason: string) => { + try { + await deleteRide(ride._id, reason); + $q.notify({ type: 'positive', message: 'Viaggio cancellato' }); + await fetchMyRides(); + } catch (error: any) { + $q.notify({ type: 'negative', message: error.data?.message || error.message }); + } + }); + }; + + const completeRide = async (ride: Ride) => { + $q.dialog({ + title: 'Completa Viaggio', + message: 'Confermi che il viaggio รจ stato completato?', + cancel: true + }).onOk(async () => { + try { + await completeRideApi(ride._id); + $q.notify({ type: 'positive', message: 'Viaggio completato!' }); + await fetchMyRides(); + } catch (error: any) { + $q.notify({ type: 'negative', message: error.data?.message || error.message }); + } + }); + }; + + const openRequestsDialog = async (ride: Ride) => { + selectedRide.value = ride; + selectedRideRequests.value = receivedRequests.value.filter(r => { + const reqRideId = typeof r.rideId === 'string' ? r.rideId : r.rideId._id; + return reqRideId === ride._id; + }); + showRequestsDialog.value = true; + }; + + const openFeedbackDialog = (ride: Ride) => { + router.push({ + name: 'leave-feedback', + params: { rideId: ride._id } + }); + }; + + const acceptRequest = async (request: RideRequest) => { + $q.dialog({ + title: 'Accetta Richiesta', + message: `Vuoi accettare la richiesta di ${getUserName(request.passengerId)}?`, + prompt: { + model: '', + type: 'text', + label: 'Messaggio (opzionale)' + }, + cancel: true + }).onOk(async (message: string) => { + try { + await acceptRequestApi(request._id, message); + $q.notify({ type: 'positive', message: 'Richiesta accettata!' }); + await fetchReceivedRequests(); + await fetchMyRides(); + } catch (error: any) { + $q.notify({ type: 'negative', message: error.data?.message || error.message }); + } + }); + }; + + const rejectRequest = async (request: RideRequest) => { + $q.dialog({ + title: 'Rifiuta Richiesta', + message: 'Vuoi rifiutare questa richiesta?', + prompt: { + model: '', + type: 'text', + label: 'Motivo (opzionale)' + }, + cancel: true + }).onOk(async (message: string) => { + try { + await rejectRequestApi(request._id, message); + $q.notify({ type: 'info', message: 'Richiesta rifiutata' }); + await fetchReceivedRequests(); + } catch (error: any) { + $q.notify({ type: 'negative', message: error.data?.message || error.message }); + } + }); + }; + + const cancelRequest = async (request: RideRequest) => { + $q.dialog({ + title: 'Annulla Richiesta', + message: 'Vuoi annullare questa richiesta?', + cancel: true + }).onOk(async () => { + try { + await cancelRequestApi(request._id); + $q.notify({ type: 'info', message: 'Richiesta annullata' }); + await fetchSentRequests(); + } catch (error: any) { + $q.notify({ type: 'negative', message: error.data?.message || error.message }); + } + }); + }; + + const getUserName = (user: any): string => { + if (typeof user === 'string') return 'Utente'; + if (user.name) return `${user.name} ${user.surname || ''}`.trim(); + return user.username || 'Utente'; + }; + + // Watch tab changes + watch(activeTab, async (tab) => { + if (tab === 'requests') { + if (requestsSubTab.value === 'received') { + await fetchReceivedRequests(); + } else { + await fetchSentRequests(); + } + } + }); + + watch(requestsSubTab, async (subTab) => { + if (subTab === 'received') { + await fetchReceivedRequests(); + } else { + await fetchSentRequests(); + } + }); + + // Lifecycle + onMounted(async () => { + await fetchMyRides(); + await fetchReceivedRequests(); + }); + + return { + // State + activeTab, + requestsSubTab, + roleFilter, + showRequestsDialog, + selectedRideRequests, + loading, + loadingRequests, + myRides, + receivedRequests, + sentRequests, + currentUserId, + + // Filters + roleFilters, + + // Computed + upcomingCount, + pendingRequestsCount, + filteredUpcoming, + filteredPast, + + // Methods + isDriver, + getPendingRequests, + canLeaveFeedback, + goToCreate, + goToRide, + goToProfile, + editRide, + cancelRide, + completeRide, + openRequestsDialog, + openFeedbackDialog, + acceptRequest, + rejectRequest, + cancelRequest + }; + } +}); diff --git a/src/modules/trasporti/pages/MyRidesPage.vue b/src/modules/trasporti/pages/MyRidesPage.vue new file mode 100644 index 00000000..59bf8c2d --- /dev/null +++ b/src/modules/trasporti/pages/MyRidesPage.vue @@ -0,0 +1,221 @@ + + + + + diff --git a/src/modules/trasporti/pages/Requestspage.vue b/src/modules/trasporti/pages/Requestspage.vue new file mode 100644 index 00000000..8972b9cc --- /dev/null +++ b/src/modules/trasporti/pages/Requestspage.vue @@ -0,0 +1,649 @@ + + + + + + diff --git a/src/modules/trasporti/pages/RideCreatePage.scss b/src/modules/trasporti/pages/RideCreatePage.scss new file mode 100644 index 00000000..6d77d628 --- /dev/null +++ b/src/modules/trasporti/pages/RideCreatePage.scss @@ -0,0 +1,181 @@ +.ride-create-page { + min-height: 100vh; + background: linear-gradient(180deg, rgba(var(--q-primary-rgb), 0.02) 0%, transparent 50%); + + &__container { + max-width: 800px; + margin: 0 auto; + padding: 16px; + } + + &__header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 24px; + } + + &__title { + font-size: 24px; + font-weight: 600; + margin: 0; + } + + &__stepper { + background: white; + border-radius: 16px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + + :deep(.q-stepper__header) { + border-radius: 16px 16px 0 0; + } + + :deep(.q-stepper__step-inner) { + padding: 24px; + } + } + + &__step-content { + max-width: 600px; + margin: 0 auto; + } + + &__step-title { + font-size: 20px; + font-weight: 600; + margin: 0 0 24px 0; + text-align: center; + } + + &__type-toggle { + max-width: 500px; + margin: 0 auto; + } + + &__route-inputs { + display: flex; + flex-direction: column; + gap: 16px; + } + + &__route-info { + display: flex; + justify-content: center; + gap: 32px; + padding: 16px; + background: rgba(var(--q-primary-rgb), 0.04); + border-radius: 12px; + margin-top: 16px; + } + + &__route-info-item { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; + } + + &__flexibility { + padding: 16px; + background: rgba(0, 0, 0, 0.02); + border-radius: 12px; + } + + &__label { + font-weight: 600; + font-size: 14px; + color: var(--q-grey-8); + margin-bottom: 12px; + } + + &__seats { + .q-btn-toggle { + background: rgba(0, 0, 0, 0.04); + border-radius: 12px; + padding: 4px; + + .q-btn { + border-radius: 8px !important; + min-width: 48px; + } + } + } + + &__seats-toggle { + max-width: 400px; + margin: 0 auto; + } + + &__preview { + max-width: 100%; + } + + &__nav { + display: flex; + align-items: center; + padding: 16px 24px; + border-top: 1px solid rgba(0, 0, 0, 0.08); + } +} + +// Expand animation +.expand-enter-active, +.expand-leave-active { + transition: all 0.3s ease; + overflow: hidden; +} + +.expand-enter-from, +.expand-leave-to { + opacity: 0; + max-height: 0; +} + +// Dark mode +.body--dark { + .ride-create-page { + background: linear-gradient(180deg, rgba(var(--q-primary-rgb), 0.05) 0%, transparent 50%); + + &__stepper { + background: #1e1e1e; + } + + &__flexibility, + &__route-info { + background: rgba(255, 255, 255, 0.04); + } + + &__nav { + border-color: rgba(255, 255, 255, 0.08); + } + + &__seats .q-btn-toggle { + background: rgba(255, 255, 255, 0.08); + } + } +} + +// Responsive +@media (max-width: 599px) { + .ride-create-page { + &__container { + padding: 8px; + } + + &__stepper { + :deep(.q-stepper__step-inner) { + padding: 16px; + } + } + + &__step-title { + font-size: 18px; + } + + &__route-info { + flex-direction: column; + gap: 12px; + align-items: center; + } + } +} \ No newline at end of file diff --git a/src/modules/trasporti/pages/RideCreatePage.ts b/src/modules/trasporti/pages/RideCreatePage.ts new file mode 100644 index 00000000..047e9e3a --- /dev/null +++ b/src/modules/trasporti/pages/RideCreatePage.ts @@ -0,0 +1,369 @@ +import { ref, reactive, computed, onMounted, defineComponent } from 'vue'; +import { useRouter, useRoute } from 'vue-router'; +import { useQuasar } from 'quasar'; +import { useRides } from '../composables/useRides'; +import { useGeocoding } from '../composables/useGeocoding'; +import { useDriverProfile } from '../composables/useDriverProfile'; +import RideTypeToggle from '../components/ride/RideTypeToggle.vue'; +import CityAutocomplete from '../components/ride/CityAutocomplete.vue'; +import WaypointsEditor from '../components/ride/WaypointsEditor.vue'; +import RideMap from '../components/ride/RideMap.vue'; +import RecurrenceSelector from '../components/ride/RecurrenceSelector.vue'; +import VehicleSelector from '../components/ride/VehicleSelector.vue'; +import PreferencesSelector from '../components/ride/PreferencesSelector.vue'; +import ContribTypeSelector from '../components/ride/ContribTypeSelector.vue'; +import RideCard from '../components/ride/RideCard.vue'; +import type { + RideFormData, + Location, + Waypoint, + Vehicle, + RouteResult, + Ride, + RideType, +} from '../types'; + +export default defineComponent({ + name: 'RideCreatePage', + + components: { + RideTypeToggle, + CityAutocomplete, + WaypointsEditor, + RideMap, + RecurrenceSelector, + VehicleSelector, + PreferencesSelector, + ContribTypeSelector, + RideCard, + }, + + setup() { + const router = useRouter(); + const route = useRoute(); + const $q = useQuasar(); + + const { createRide, updateRide, fetchRide, formatDuration } = useRides(); + const { suggestWaypoints } = useGeocoding(); + const { myVehicles, addVehicle } = useDriverProfile(); + + // Refs + const stepperRef = ref(null); + const currentStep = ref(1); + const submitting = ref(false); + const routeInfo = ref(null); + const suggestedWaypoints = ref([]); + + // Check if editing + const rideId = computed(() => route.params.id as string | undefined); + const isEditing = computed(() => !!rideId.value); + + // Form data + const formData = reactive({ + type: (route.query.type as RideType) || 'offer', + departure: undefined as any, + destination: undefined as any, + waypoints: [], + date: '', + time: '', + dateTime: '', + flexibleTime: false, + flexibleMinutes: 30, + recurrence: { type: 'once' }, + passengers: { max: 3, available: 3 }, + seatsNeeded: 1, + vehicle: {} as Vehicle, + preferences: {}, + contribution: { contribTypes: [] }, + notes: '', + }); + + // Options + const seatsOptions = [ + { label: '1', value: 1 }, + { label: '2', value: 2 }, + { label: '3', value: 3 }, + { label: '4', value: 4 }, + { label: '5', value: 5 }, + { label: '6+', value: 6 }, + ]; + + const seatsNeededOptions = [ + { label: '1 posto', value: 1 }, + { label: '2 posti', value: 2 }, + { label: '3 posti', value: 3 }, + { label: '4+ posti', value: 4 }, + ]; + + // Computed + const totalSteps = computed(() => 7); + + const savedVehicles = computed((): Vehicle[] => { + const vehicles = myVehicles.value; + + // If it's already an array, return it + if (Array.isArray(vehicles)) { + return vehicles; + } + + // If it's a response object with data array, extract it + if (vehicles && typeof vehicles === 'object' && 'data' in vehicles) { + const data = (vehicles as any).data; + return Array.isArray(data) ? data : []; + } + + // Fallback to empty array + return []; + }); + + const formattedDuration = computed(() => { + if (!routeInfo.value) return ''; + return formatDuration(routeInfo.value.duration); + }); + + const canProceed = computed(() => { + switch (currentStep.value) { + case 1: + return !!formData.type; + case 2: + return !!formData.departure?.city && !!formData.destination?.city; + case 3: + return !!formData.date && !!formData.time; + case 4: + if (formData.type === 'offer') { + return formData.passengers.max > 0; + } + return formData.seatsNeeded > 0; + case 5: + return true; // Preferenze opzionali + case 6: + return true; // Contributo opzionale + default: + return true; + } + }); + + // Preview ride per il riepilogo + const previewRide = computed((): Partial => { + const dateTime = + formData.date && formData.time + ? new Date(`${formData.date}T${formData.time}`) + : new Date(); + + return { + _id: 'preview', + type: formData.type, + departure: formData.departure || { city: '', coordinates: { lat: 0, lng: 0 } }, + destination: formData.destination || { + city: '', + coordinates: { lat: 0, lng: 0 }, + }, + waypoints: formData.waypoints || [], + dateTime: dateTime.toISOString(), + passengers: formData.passengers, + seatsNeeded: formData.seatsNeeded, + vehicle: formData.vehicle, + preferences: formData.preferences, + contribution: formData.contribution, + status: 'active', + estimatedDistance: routeInfo.value?.distance, + estimatedDuration: routeInfo.value?.duration, + notes: formData.notes, + userId: { + _id: 'current', + username: 'Tu', + name: 'Tu', + } as any, + } as Partial; + }); + + // Methods + const goBack = () => { + if (currentStep.value > 1) { + stepperRef.value?.previous(); + } else { + router.back(); + } + }; + + const onDepartureSelect = async (location: Location) => { + formData.departure = location; + await updateSuggestedWaypoints(); + }; + + const onDestinationSelect = async (location: Location) => { + formData.destination = location; + await updateSuggestedWaypoints(); + }; + + const updateSuggestedWaypoints = async () => { + if (formData.departure?.coordinates && formData.destination?.coordinates) { + try { + const suggestions = await suggestWaypoints( + formData.departure.coordinates, + formData.destination.coordinates + ); + suggestedWaypoints.value = suggestions || []; + } catch (error) { + console.error('Errore suggerimento waypoints:', error); + } + } + }; + + const onRouteCalculated = (route: RouteResult) => { + routeInfo.value = route; + }; + + const onSaveVehicle = async (vehicle: Vehicle) => { + try { + await addVehicle(vehicle); + $q.notify({ + type: 'positive', + message: 'Veicolo salvato nel profilo', + }); + } catch (error) { + console.error('Errore salvataggio veicolo:', error); + } + }; + + const submitRide = async () => { + submitting.value = true; + + try { + // Combina data e ora + const dateTime = new Date(`${formData.date}T${formData.time}`); + + const rideData: Partial = { + type: formData.type, + departure: formData.departure, + destination: formData.destination, + waypoints: formData.waypoints, + dateTime: dateTime.toISOString(), + flexibleTime: formData.flexibleTime, + flexibleMinutes: formData.flexibleMinutes, + recurrence: formData.recurrence, + preferences: formData.preferences, + contribution: formData.contribution, + notes: formData.notes, + }; + + if (formData.type === 'offer') { + rideData.passengers = formData.passengers; + rideData.vehicle = formData.vehicle; + } else { + rideData.seatsNeeded = formData.seatsNeeded; + } + + let response; + if (isEditing.value) { + response = await updateRide(rideId.value!, rideData); + } else { + response = await createRide(rideData); + } + + if (response?.success) { + $q.notify({ + type: 'positive', + message: isEditing.value ? 'Viaggio aggiornato!' : 'Viaggio pubblicato!', + caption: + formData.type === 'offer' + ? 'I passeggeri potranno ora prenotare' + : 'I conducenti potranno contattarti', + }); + + router.push({ + name: 'ride-detail', + params: { id: response.data?._id || rideId.value }, + }); + } + } catch (error: any) { + $q.notify({ + type: 'negative', + message: 'Errore durante la pubblicazione', + caption: error.data?.message || error.message, + }); + } finally { + submitting.value = false; + } + }; + + // Load existing ride if editing + onMounted(async () => { + if (isEditing.value) { + try { + const response = await fetchRide(rideId.value!); + if (response?.data?.data) { + const ride = response.data.data; + + formData.type = ride.type; + formData.departure = ride.departure; + formData.destination = ride.destination; + formData.waypoints = ride.waypoints || []; + + const dt = new Date(ride.dateTime); + formData.date = dt.toISOString().split('T')[0]; + formData.time = dt.toTimeString().slice(0, 5); + + formData.flexibleTime = ride.flexibleTime || false; + formData.flexibleMinutes = ride.flexibleMinutes || 30; + formData.recurrence = ride.recurrence || { type: 'once' }; + formData.passengers = ride.passengers || { max: 3, available: 3 }; + formData.seatsNeeded = ride.seatsNeeded || 1; + formData.vehicle = ride.vehicle || {}; + formData.preferences = ride.preferences || {}; + formData.contribution = ride.contribution || { contribTypes: [] }; + formData.notes = ride.notes || ''; + } + } catch (error) { + $q.notify({ + type: 'negative', + message: 'Errore caricamento viaggio', + }); + router.back(); + } + } + }); + + // Create safe navigation methods + const goNext = () => { + stepperRef.value?.next(); + }; + + const goPrevious = () => { + stepperRef.value?.previous(); + }; + return { + // Refs + stepperRef, + currentStep, + submitting, + routeInfo, + suggestedWaypoints, + + // State + formData, + isEditing, + + // Options + seatsOptions, + seatsNeededOptions, + + // Computed + totalSteps, + savedVehicles, + formattedDuration, + canProceed, + previewRide, + + // Methods + goBack, + onDepartureSelect, + onDestinationSelect, + onRouteCalculated, + onSaveVehicle, + submitRide, + goPrevious, + goNext, + }; + }, +}); diff --git a/src/modules/trasporti/pages/RideCreatePage.vue b/src/modules/trasporti/pages/RideCreatePage.vue new file mode 100644 index 00000000..465b1cb9 --- /dev/null +++ b/src/modules/trasporti/pages/RideCreatePage.vue @@ -0,0 +1,349 @@ + + + + + diff --git a/src/modules/trasporti/pages/Vehicleeditpage.vue b/src/modules/trasporti/pages/Vehicleeditpage.vue new file mode 100644 index 00000000..13de1604 --- /dev/null +++ b/src/modules/trasporti/pages/Vehicleeditpage.vue @@ -0,0 +1,943 @@ + + + + + + diff --git a/src/modules/trasporti/pages/Vehiclespage.vue b/src/modules/trasporti/pages/Vehiclespage.vue new file mode 100644 index 00000000..8e3491cd --- /dev/null +++ b/src/modules/trasporti/pages/Vehiclespage.vue @@ -0,0 +1,755 @@ + + + + + + diff --git a/src/modules/trasporti/pages/index.ts b/src/modules/trasporti/pages/index.ts new file mode 100644 index 00000000..8ddcde29 --- /dev/null +++ b/src/modules/trasporti/pages/index.ts @@ -0,0 +1,7 @@ +// Export all pages +export { default as RidesListPage } from './RidesListPage.vue'; +export { default as RideCreatePage } from './RideCreatePage.vue'; +export { default as RideDetailPage } from './RideDetailPage.vue'; +export { default as DriverProfilePage } from './DriverProfilePage.vue'; +export { default as MyRidesPage } from './MyRidesPage.vue'; +export { default as RideSearchPage } from './RideSearchPage.vue'; diff --git a/src/modules/trasporti/types/index.ts b/src/modules/trasporti/types/index.ts new file mode 100644 index 00000000..3b284ab1 --- /dev/null +++ b/src/modules/trasporti/types/index.ts @@ -0,0 +1,2 @@ +// Re-export all types from a single entry point +export * from './trasporti.types'; diff --git a/src/modules/trasporti/types/trasporti.types.ts b/src/modules/trasporti/types/trasporti.types.ts new file mode 100644 index 00000000..2ca6cbda --- /dev/null +++ b/src/modules/trasporti/types/trasporti.types.ts @@ -0,0 +1,884 @@ +// ============================================================ +// ๐ŸŒ COORDINATE E LOCALITร€ +// ============================================================ + +export interface Coordinates { + lat: number; + lng: number; +} + +export interface Location { + city: string; + address?: string; + province?: string; + region?: string; + country?: string; + postalCode?: string; + coordinates: Coordinates; +} + +export interface Waypoint { + _id?: string; + location: Location; + order: number; + estimatedArrival?: Date | string; + stopDuration?: number; // minuti +} + +// ============================================================ +// ๐Ÿš— VEICOLI +// ============================================================ + +export type VehicleType = 'auto' | 'moto' | 'furgone' | 'minibus' | 'altro'; + +export type VehicleFeature = + | 'aria_condizionata' + | 'wifi' + | 'presa_usb' + | 'bluetooth' + | 'bagagliaio_grande' + | 'seggiolino_bimbi'; + +export interface Vehicle { + _id?: string; + type?: VehicleType; + brand?: string; + model?: string; + color?: string; + colorHex?: string; + year?: number; + seats?: number; + licensePlate?: string; + features?: VehicleFeature[]; + isDefault?: boolean; + isVerified?: boolean; +} + +// ============================================================ +// ๐Ÿ”„ RICORRENZA +// ============================================================ + +export type RecurrenceType = 'once' | 'weekly' | 'custom_days' | 'custom_dates'; + +export interface Recurrence { + type: RecurrenceType; + daysOfWeek?: number[]; // 0 = Domenica, 1 = Lunedรฌ, ... + customDates?: Date[] | string[]; + startDate?: Date | string; + endDate?: Date | string; + excludedDates?: Date[] | string[]; +} + +// ============================================================ +// โš™๏ธ PREFERENZE VIAGGIO +// ============================================================ + +export type SmokingPreference = 'yes' | 'no' | 'outside_only'; +export type PetsPreference = 'no' | 'small' | 'medium' | 'large' | 'all'; +export type LuggageSize = 'none' | 'small' | 'medium' | 'large'; +export type MusicPreference = 'no_music' | 'quiet' | 'moderate' | 'loud' | 'passenger_choice'; +export type ConversationPreference = 'quiet' | 'moderate' | 'chatty'; + +export interface RidePreferences { + smoking?: SmokingPreference; + pets?: PetsPreference; + luggage?: LuggageSize; + packages?: boolean; + maxPackageSize?: 'small' | 'medium' | 'large' | 'xlarge'; + music?: MusicPreference; + conversation?: ConversationPreference; + foodAllowed?: boolean; + childrenFriendly?: boolean; + wheelchairAccessible?: boolean; + otherPreferences?: string; +} + +// ============================================================ +// ๐Ÿ’ฐ CONTRIBUTI +// ============================================================ + +export interface ContribType { + _id: string; + idapp: string; + label: string; + icon: string; + color: string; + showprice?: boolean; +} + +export interface ContributionItem { + _id?: string; + contribTypeId: string | ContribType; + price?: number; + pricePerKm?: number; + notes?: string; +} + +export interface Contribution { + contribTypes: ContributionItem[]; + negotiable?: boolean; + freeForStudents?: boolean; + freeForElders?: boolean; +} + +// ============================================================ +// ๐Ÿ‘ค UTENTE (per riferimenti) +// ============================================================ + +export interface UserBasic { + _id: string; + username: string; + name?: string; + surname?: string; + email?: string; +} + +export interface UserWithProfile extends UserBasic { + profile?: { + img?: string; + Biografia?: string; + Cell?: string; + cellVerified?: boolean; + driverProfile?: DriverProfile; + preferences?: UserPreferences; + }; +} + +export interface DriverProfile { + isDriver: boolean; + bio?: string; + vehicles?: Vehicle[]; + ridesCompletedAsDriver: number; + ridesCompletedAsPassenger: number; + averageRating: number; + totalRatings: number; + verifiedDriver: boolean; + licenseVerified: boolean; + licenseNumber?: string; + licenseExpiry?: Date | string; + memberSince?: Date | string; + responseRate?: number; + responseTime?: 'within_hour' | 'within_day' | 'within_days'; + totalKmShared?: number; + co2Saved?: number; + badges?: Badge[]; + level?: number; + points?: number; +} + +export interface Badge { + name: string; + earnedAt: Date | string; +} + +export interface UserPreferences { + smoking?: SmokingPreference; + pets?: PetsPreference; + music?: MusicPreference; + conversation?: ConversationPreference; + notifications?: NotificationPreferences; + privacy?: PrivacyPreferences; + favoriteLocations?: FavoriteLocation[]; + languages?: string[]; + preferredContribType?: string; +} + +export interface NotificationPreferences { + rideRequests: boolean; + rideAccepted: boolean; + rideReminders: boolean; + messages: boolean; + marketing: boolean; + pushEnabled: boolean; + emailEnabled: boolean; +} + +export interface PrivacyPreferences { + showEmail: boolean; + showPhone: boolean; + showLastName: boolean; + showRides: boolean; +} + +export interface FavoriteLocation { + _id?: string; + name: string; + city: string; + address?: string; + coordinates?: Coordinates; + type: 'home' | 'work' | 'other'; +} + +// ============================================================ +// ๐Ÿš— RIDE (Viaggio) +// ============================================================ + +export type RideType = 'offer' | 'request'; +export type RideStatus = 'draft' | 'active' | 'full' | 'in_progress' | 'completed' | 'cancelled' | 'expired'; + +export interface RidePassengers { + available: number; + max: number; +} + +export interface ConfirmedPassenger { + userId: string | UserBasic; + seats: number; + pickupPoint?: Location; + dropoffPoint?: Location; + confirmedAt: Date | string; +} + +export interface Ride { + _id: string; + idapp: string; + userId: string | UserWithProfile; + type: RideType; + departure: Location; + destination: Location; + waypoints?: Waypoint[]; + dateTime: Date | string; + flexibleTime?: boolean; + flexibleMinutes?: number; + recurrence?: Recurrence; + passengers?: RidePassengers; + seatsNeeded?: number; // Solo per type = 'request' + vehicle?: Vehicle; + preferences?: RidePreferences; + contribution?: Contribution; + status: RideStatus; + estimatedDistance?: number; // km + estimatedDuration?: number; // minuti + routePolyline?: string; + confirmedPassengers?: ConfirmedPassenger[]; + views?: number; + isFeatured?: boolean; + notes?: string; + cancellationReason?: string; + cancelledAt?: Date | string; + createdAt: Date | string; + updatedAt: Date | string; + // Virtuals + isFull?: boolean; + bookedSeats?: number; + allCities?: string[]; +} + +// ============================================================ +// ๐Ÿ“ฉ RIDE REQUEST (Richiesta Passaggio) +// ============================================================ + +export type RideRequestStatus = 'pending' | 'accepted' | 'rejected' | 'cancelled' | 'expired' | 'completed'; + +export interface RideRequestContribution { + agreed: boolean; + contribTypeId?: string | ContribType; + amount?: number; + notes?: string; +} + +export interface RideRequest { + _id: string; + idapp: string; + rideId: string | Ride; + passengerId: string | UserWithProfile; + driverId: string | UserWithProfile; + message?: string; + pickupPoint?: Location; + dropoffPoint?: Location; + useOriginalRoute: boolean; + seatsRequested: number; + hasLuggage: boolean; + luggageSize?: LuggageSize; + hasPackages: boolean; + packageDescription?: string; + hasPets: boolean; + petType?: string; + petSize?: 'small' | 'medium' | 'large'; + specialNeeds?: string; + status: RideRequestStatus; + responseMessage?: string; + respondedAt?: Date | string; + contribution?: RideRequestContribution; + cancelledBy?: 'passenger' | 'driver'; + cancellationReason?: string; + cancelledAt?: Date | string; + completedAt?: Date | string; + feedbackGiven: boolean; + createdAt: Date | string; + updatedAt: Date | string; + // Virtuals + canCancel?: boolean; + isPending?: boolean; +} + +// ============================================================ +// ๐Ÿ’ฌ CHAT & MESSAGGI +// ============================================================ + +export type ChatType = 'direct' | 'ride' | 'group'; +export type MessageType = 'text' | 'ride_share' | 'location' | 'image' | 'voice' | 'system' | 'ride_request' | 'ride_accepted' | 'ride_rejected'; + +export interface LastMessage { + text: string; + senderId: string; + timestamp: Date | string; + type: MessageType; +} + +export interface Chat { + _id: string; + idapp: string; + participants: (string | UserBasic)[]; + rideId?: string | Ride; + rideRequestId?: string; + type: ChatType; + title?: string; + lastMessage?: LastMessage; + unreadCount?: Map | Record; + isActive: boolean; + mutedBy?: string[]; + blockedBy?: string[]; + createdAt: Date | string; + updatedAt: Date | string; + // Extra per UI + otherParticipant?: UserBasic; +} + +export interface MessageMetadata { + rideId?: string; + rideRequestId?: string; + location?: { + lat: number; + lng: number; + address?: string; + }; + imageUrl?: string; + voiceUrl?: string; + voiceDuration?: number; + systemAction?: string; +} + +export interface MessageReadBy { + userId: string; + readAt: Date | string; +} + +export interface MessageReaction { + userId: string; + emoji: string; + createdAt: Date | string; +} + +export interface Message { + _id: string; + idapp: string; + chatId: string; + senderId: string | UserBasic; + text?: string; + type: MessageType; + metadata?: MessageMetadata; + readBy?: MessageReadBy[]; + replyTo?: string | Message; + isEdited: boolean; + editedAt?: Date | string; + isDeleted: boolean; + deletedAt?: Date | string; + reactions?: MessageReaction[]; + createdAt: Date | string; +} + +// ============================================================ +// โญ FEEDBACK +// ============================================================ + +export type FeedbackRole = 'driver' | 'passenger'; + +export type FeedbackTag = + | 'puntuale' + | 'gentile' + | 'auto_pulita' + | 'guida_sicura' + | 'buona_conversazione' + | 'silenzioso' + | 'flessibile' + | 'rispettoso' + | 'affidabile' + | 'consigliato' + | 'in_ritardo' + | 'scortese' + | 'guida_pericolosa' + | 'auto_sporca' + | 'non_rispettoso'; + +export interface FeedbackCategories { + punctuality?: number; + cleanliness?: number; + communication?: number; + driving?: number; + respect?: number; + reliability?: number; +} + +export interface FeedbackResponse { + text: string; + respondedAt: Date | string; +} + +export interface FeedbackHelpful { + count: number; + users: string[]; +} + +export interface FeedbackReported { + isReported: boolean; + reason?: string; + reportedBy?: string; + reportedAt?: Date | string; +} + +export interface Feedback { + _id: string; + idapp: string; + rideId: string | Ride; + rideRequestId?: string; + fromUserId: string | UserBasic; + toUserId: string | UserBasic; + role: FeedbackRole; + rating: number; + categories?: FeedbackCategories; + comment?: string; + pros?: string[]; + cons?: string[]; + tags?: FeedbackTag[]; + isPublic: boolean; + isVerified: boolean; + response?: FeedbackResponse; + helpful?: FeedbackHelpful; + reported?: FeedbackReported; + createdAt: Date | string; + // Virtual + categoryAverage?: number; +} + +export interface FeedbackStats { + asDriver: { + averageRating: number; + totalFeedbacks: number; + categories: FeedbackCategories; + } | null; + asPassenger: { + averageRating: number; + totalFeedbacks: number; + categories: FeedbackCategories; + } | null; + overall: { + averageRating: number; + totalFeedbacks: number; + }; +} + +export interface RatingDistribution { + _id: number; // 1-5 + count: number; +} + +// ============================================================ +// ๐Ÿ—บ๏ธ GEOCODING +// ============================================================ + +export interface GeoAutocompleteResult { + city: string; + province?: string; + region?: string; + country?: string; + postalCode?: string; + coordinates: Coordinates; + displayName: string; + type?: string; +} + +export interface GeocodingResult { + displayName: string; + city?: string; + address?: string; + province?: string; + region?: string; + country?: string; + postalCode?: string; + coordinates: Coordinates; + type?: string; + importance?: number; +} + +export interface RouteStep { + instruction: string; + name: string; + distance: number; + duration: number; +} + +export interface RouteLeg { + distance: number; // km + duration: number; // minuti + summary?: string; + steps: RouteStep[]; +} + +export interface RouteResult { + distance: number; // km + duration: number; // minuti + polyline: string; + legs: RouteLeg[]; +} + +export interface DistanceResult { + distance: number; // km + duration: number; // minuti + durationFormatted: string; +} + +export interface SuggestedWaypoint { + city: string; + province?: string; + region?: string; + coordinates: Coordinates; +} + +// ============================================================ +// ๐Ÿ“Š STATISTICHE & WIDGET +// ============================================================ + +export interface RidesStats { + totalActiveOffers: number; + totalActiveRequests: number; + todayRides: number; + myUpcomingAsDriver: number; + myUpcomingAsPassenger: number; + pendingRequests: number; +} + +export interface RidesStatsResponse { + stats: RidesStats; + recentRides: Ride[]; +} + +export interface DriverPublicProfile { + user: { + _id: string; + username: string; + name?: string; + surname?: string; + img?: string; + bio?: string; + driverProfile?: DriverProfile; + languages?: string[]; + }; + stats: { + ridesAsDriver: number; + ridesAsPassenger: number; + completedRides: number; + } & FeedbackStats; + recentRides: Ride[]; + recentFeedback: Feedback[]; +} + +// ============================================================ +// ๐Ÿ“ FORM & INPUT TYPES +// ============================================================ + +export interface RideFormData { + type: RideType; + departure: Location; + destination: Location; + waypoints: Waypoint[]; + dateTime: string; + flexibleTime: boolean; + flexibleMinutes: number; + recurrence: Recurrence; + passengers: RidePassengers; + seatsNeeded: number; + vehicle: Vehicle; + preferences: RidePreferences; + contribution: Contribution; + notes: string; +} + +export interface RideRequestFormData { + rideId: string; + message: string; + pickupPoint?: Location; + dropoffPoint?: Location; + useOriginalRoute: boolean; + seatsRequested: number; + hasLuggage: boolean; + luggageSize: LuggageSize; + hasPackages: boolean; + packageDescription: string; + hasPets: boolean; + petType: string; + petSize: 'small' | 'medium' | 'large'; + specialNeeds: string; + contribution: RideRequestContribution; +} + +export interface FeedbackFormData { + rideId: string; + rideRequestId?: string; + toUserId: string; + role: FeedbackRole; + rating: number; + categories: FeedbackCategories; + comment: string; + pros: string[]; + cons: string[]; + tags: FeedbackTag[]; + isPublic: boolean; +} + +export interface RideSearchFilters { + type?: RideType; + from?: string; + to?: string; + date?: string; + dateFrom?: string; + dateTo?: string; + seats?: number; + preferences?: Partial; + contribTypes?: string[]; + passingThrough?: string; +} + +// ============================================================ +// ๐Ÿ“ก API RESPONSE TYPES +// ============================================================ + +export interface ApiResponse { + success: boolean; + message?: string; + data?: T; + error?: string; +} + +export interface PaginatedResponse { + success: boolean; + data: T[]; + pagination: { + page: number; + limit: number; + total: number; + pages: number; + }; +} + +export interface RideRequestCounts { + pending: number; + accepted: number; + rejected: number; +} + +export interface RequestsReceivedResponse { + success: boolean; + data: RideRequest[]; + counts: RideRequestCounts; + pagination: { + page: number; + limit: number; + total: number; + pages: number; + }; +} + +export interface MyRidesResponse { + success: boolean; + data: { + all: Ride[]; + upcoming: Ride[]; + past: Ride[]; + }; + pagination: { + page: number; + limit: number; + total: number; + pages: number; + }; +} + +export interface ChatWithUnread extends Chat { + otherParticipant?: UserBasic; + unreadCount?: number; +} + +// ============================================================ +// ๐ŸŽจ UI HELPER TYPES +// ============================================================ + +export interface RideTypeOption { + value: RideType; + label: string; + icon: string; + color: string; + description: string; +} + +export interface PreferenceOption { + value: T; + label: string; + icon?: string; + description?: string; +} + +export interface DayOfWeekOption { + value: number; + label: string; + shortLabel: string; +} + +export interface VehicleColorOption { + name: string; + hex: string; +} + +export interface FeedbackTagOption { + value: FeedbackTag; + label: string; + icon: string; + isPositive: boolean; +} + +// ============================================================ +// ๐Ÿ”ง CONSTANTS +// ============================================================ + +export const RIDE_TYPE_OPTIONS: RideTypeOption[] = [ + { + value: 'offer', + label: 'Offro Passaggio', + icon: '๐ŸŸข', + color: 'positive', + description: 'Ho posti disponibili nella mia auto' + }, + { + value: 'request', + label: 'Cerco Passaggio', + icon: '๐Ÿ”ด', + color: 'negative', + description: 'Sto cercando qualcuno che mi dia un passaggio' + } +]; + +export const DAYS_OF_WEEK: DayOfWeekOption[] = [ + { value: 0, label: 'Domenica', shortLabel: 'Dom' }, + { value: 1, label: 'Lunedรฌ', shortLabel: 'Lun' }, + { value: 2, label: 'Martedรฌ', shortLabel: 'Mar' }, + { value: 3, label: 'Mercoledรฌ', shortLabel: 'Mer' }, + { value: 4, label: 'Giovedรฌ', shortLabel: 'Gio' }, + { value: 5, label: 'Venerdรฌ', shortLabel: 'Ven' }, + { value: 6, label: 'Sabato', shortLabel: 'Sab' } +]; + +export const SMOKING_OPTIONS: PreferenceOption[] = [ + { value: 'no', label: 'Non fumatori', icon: '๐Ÿšญ' }, + { value: 'yes', label: 'Fumatori OK', icon: '๐Ÿšฌ' }, + { value: 'outside_only', label: 'Solo fuori', icon: '๐ŸชŸ' } +]; + +export const PETS_OPTIONS: PreferenceOption[] = [ + { value: 'no', label: 'No animali', icon: '๐Ÿšซ' }, + { value: 'small', label: 'Taglia piccola', icon: '๐Ÿ•' }, + { value: 'medium', label: 'Taglia media', icon: '๐Ÿ•โ€๐Ÿฆบ' }, + { value: 'large', label: 'Taglia grande', icon: '๐Ÿฆฎ' }, + { value: 'all', label: 'Tutti OK', icon: '๐Ÿพ' } +]; + +export const LUGGAGE_OPTIONS: PreferenceOption[] = [ + { value: 'none', label: 'Nessun bagaglio', icon: '๐ŸŽ’' }, + { value: 'small', label: 'Piccolo', icon: '๐Ÿ‘œ' }, + { value: 'medium', label: 'Medio', icon: '๐Ÿงณ' }, + { value: 'large', label: 'Grande', icon: '๐Ÿ›„' } +]; + +export const MUSIC_OPTIONS: PreferenceOption[] = [ + { value: 'no_music', label: 'Silenzio', icon: '๐Ÿ”‡' }, + { value: 'quiet', label: 'Sottofondo', icon: '๐Ÿ”ˆ' }, + { value: 'moderate', label: 'Normale', icon: '๐Ÿ”‰' }, + { value: 'loud', label: 'Alta', icon: '๐Ÿ”Š' }, + { value: 'passenger_choice', label: 'Scelgono i passeggeri', icon: '๐ŸŽต' } +]; + +export const CONVERSATION_OPTIONS: PreferenceOption[] = [ + { value: 'quiet', label: 'Preferisco silenzio', icon: '๐Ÿคซ' }, + { value: 'moderate', label: 'Chiacchierata leggera', icon: '๐Ÿ’ฌ' }, + { value: 'chatty', label: 'Amo conversare', icon: '๐Ÿ—ฃ๏ธ' } +]; + +export const VEHICLE_TYPES: PreferenceOption[] = [ + { value: 'auto', label: 'Auto', icon: '๐Ÿš—' }, + { value: 'moto', label: 'Moto', icon: '๐Ÿ๏ธ' }, + { value: 'furgone', label: 'Furgone', icon: '๐Ÿš' }, + { value: 'minibus', label: 'Minibus', icon: '๐ŸšŒ' }, + { value: 'altro', label: 'Altro', icon: '๐Ÿš™' } +]; + +export const VEHICLE_COLORS: VehicleColorOption[] = [ + { name: 'Bianco', hex: '#FFFFFF' }, + { name: 'Nero', hex: '#000000' }, + { name: 'Grigio', hex: '#808080' }, + { name: 'Argento', hex: '#C0C0C0' }, + { name: 'Rosso', hex: '#FF0000' }, + { name: 'Blu', hex: '#0000FF' }, + { name: 'Verde', hex: '#008000' }, + { name: 'Giallo', hex: '#FFFF00' }, + { name: 'Arancione', hex: '#FFA500' }, + { name: 'Marrone', hex: '#8B4513' }, + { name: 'Beige', hex: '#F5F5DC' }, + { name: 'Bordeaux', hex: '#800020' } +]; + +export const FEEDBACK_TAGS_OPTIONS: FeedbackTagOption[] = [ + { value: 'puntuale', label: 'Puntuale', icon: 'โฐ', isPositive: true }, + { value: 'gentile', label: 'Gentile', icon: '๐Ÿ˜Š', isPositive: true }, + { value: 'auto_pulita', label: 'Auto pulita', icon: 'โœจ', isPositive: true }, + { value: 'guida_sicura', label: 'Guida sicura', icon: '๐Ÿ›ก๏ธ', isPositive: true }, + { value: 'buona_conversazione', label: 'Buona conversazione', icon: '๐Ÿ’ฌ', isPositive: true }, + { value: 'silenzioso', label: 'Rispetta il silenzio', icon: '๐Ÿคซ', isPositive: true }, + { value: 'flessibile', label: 'Flessibile', icon: '๐Ÿคธ', isPositive: true }, + { value: 'rispettoso', label: 'Rispettoso', icon: '๐Ÿ™', isPositive: true }, + { value: 'affidabile', label: 'Affidabile', icon: '๐Ÿ’ฏ', isPositive: true }, + { value: 'consigliato', label: 'Consigliato', icon: '๐Ÿ‘', isPositive: true }, + { value: 'in_ritardo', label: 'In ritardo', icon: 'โณ', isPositive: false }, + { value: 'scortese', label: 'Scortese', icon: '๐Ÿ˜ค', isPositive: false }, + { value: 'guida_pericolosa', label: 'Guida pericolosa', icon: 'โš ๏ธ', isPositive: false }, + { value: 'auto_sporca', label: 'Auto sporca', icon: '๐Ÿ—‘๏ธ', isPositive: false }, + { value: 'non_rispettoso', label: 'Non rispettoso', icon: '๐Ÿ‘Ž', isPositive: false } +]; + +export const RECURRENCE_TYPE_OPTIONS: PreferenceOption[] = [ + { value: 'once', label: 'Una volta sola', icon: '1๏ธโƒฃ' }, + { value: 'weekly', label: 'Ogni settimana', icon: '๐Ÿ”„' }, + { value: 'custom_days', label: 'Giorni specifici', icon: '๐Ÿ“…' }, + { value: 'custom_dates', label: 'Date personalizzate', icon: '๐Ÿ—“๏ธ' } +]; + +export const VEHICLE_FEATURES_OPTIONS: PreferenceOption[] = [ + { value: 'aria_condizionata', label: 'Aria condizionata', icon: 'โ„๏ธ' }, + { value: 'wifi', label: 'WiFi', icon: '๐Ÿ“ถ' }, + { value: 'presa_usb', label: 'Presa USB', icon: '๐Ÿ”Œ' }, + { value: 'bluetooth', label: 'Bluetooth', icon: '๐Ÿ”ต' }, + { value: 'bagagliaio_grande', label: 'Bagagliaio grande', icon: '๐Ÿงณ' }, + { value: 'seggiolino_bimbi', label: 'Seggiolino bimbi', icon: '๐Ÿ‘ถ' } +]; + +// ============================================================ +// ๐Ÿ› ๏ธ UTILITY TYPES +// ============================================================ + +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; + +export type WithId = T & { _id: string }; + +export type CreateInput = Omit; + +export type UpdateInput = DeepPartial>; diff --git a/src/rootgen/admin/confsite/confsite.vue b/src/rootgen/admin/confsite/confsite.vue index e03bb985..25c0b883 100755 --- a/src/rootgen/admin/confsite/confsite.vue +++ b/src/rootgen/admin/confsite/confsite.vue @@ -238,6 +238,9 @@ + + diff --git a/src/router/routesAI.ts b/src/router/routesAI.ts index 5f859500..06c15a8a 100644 --- a/src/router/routesAI.ts +++ b/src/router/routesAI.ts @@ -92,6 +92,7 @@ function getRoutesAI(site: ISites) { return menuAI; } + function getRoutesPoster(site: ISites) { const routes_posters: IListRoutes[] = [ { diff --git a/src/router/routesTrasporti.ts b/src/router/routesTrasporti.ts new file mode 100644 index 00000000..b9cbb7e6 --- /dev/null +++ b/src/router/routesTrasporti.ts @@ -0,0 +1,619 @@ +// ============================================================ +// ๐Ÿš— ROUTES TRASPORTI - Sistema Carpooling Solidale +// ============================================================ +// File: /myprojplanet_vite/src/modules/trasporti/routes/routes_trasporti.ts + +import { ISites, IListRoutes } from '@/model'; // Adatta al tuo path + +/** + * Genera le routes per il modulo Trasporti + * @param site - Configurazione del sito + */ +export function getRoutesTrasporti(site: ISites) { + + // ============================================================ + // ๐Ÿ“‹ ROUTES SINGOLE + // ============================================================ + const routes_trasporti: IListRoutes[] = [ + + // ---------------------------------------------------------- + // ๐Ÿ  HOME & DASHBOARD. (๐Ÿ“‹ LISTA & RICERCA) + // ---------------------------------------------------------- + { + active: true, + order: 300, + path: '/trasporti', + materialIcon: 'commute', + faIcon: 'fas fa-car-side', + name: 'mypages.TrasportiHome', + component: () => import('@/modules/trasporti/pages/RidesListPage.vue'), + inmenu: false, // รˆ giร  nel menu principale + submenu: false, + level_parent: 0, + level_child: 0, + meta: { + requiresAuth: false, + title: 'Viaggi Disponibili', + description: 'Esplora i viaggi disponibili nella community' + } + }, + { + active: true, + order: 301, + path: '/trasporti/dashboard', + materialIcon: 'dashboard', + faIcon: 'fas fa-tachometer-alt', + name: 'mypages.TrasportiDashboard', + component: () => import('@/modules/trasporti/pages/MyRidesPage.vue'), + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: true, + title: 'Dashboard Trasporti' + } + }, + { + active: true, + order: 311, + path: '/trasporti/cerca', + materialIcon: 'search', + faIcon: 'fas fa-search', + name: 'mypages.TrasportiCerca', + component: () => import('@/modules/trasporti/pages/RideSearchPage.vue'), + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: false, + title: 'Cerca Passaggio' + } + }, + { + active: true, + order: 312, + path: '/trasporti/mappa', + materialIcon: 'map', + faIcon: 'fas fa-map-marked-alt', + name: 'mypages.TrasportiMappa', + component: () => import('@/modules/trasporti/pages/RideSearchPage.vue'), + // props: { defaultView: 'map' }, + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: false, + title: 'Mappa Viaggi' + } + }, + + // ---------------------------------------------------------- + // โž• CREA VIAGGIO + // ---------------------------------------------------------- + { + active: true, + order: 320, + path: '/trasporti/crea', + materialIcon: 'add_circle', + faIcon: 'fas fa-plus-circle', + name: 'mypages.TrasportiCrea', + component: () => import('@/modules/trasporti/pages/RideCreatePage.vue'), + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: true, + title: 'Crea Viaggio' + } + }, + { + active: true, + order: 321, + path: '/trasporti/offri', + materialIcon: 'directions_car', + faIcon: 'fas fa-car', + name: 'mypages.TrasportiOffri', + component: () => import('@/modules/trasporti/pages/RideCreatePage.vue'), + // props: { defaultType: 'offer' }, + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: true, + title: 'Offri Passaggio' + } + }, + { + active: true, + order: 322, + path: '/trasporti/richiedi', + materialIcon: 'hail', + faIcon: 'fas fa-hand-paper', + name: 'mypages.TrasportiRichiedi', + component: () => import('@/modules/trasporti/pages/RideCreatePage.vue'), + // props: { defaultType: 'request' }, + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: true, + title: 'Richiedi Passaggio' + } + }, + + // ---------------------------------------------------------- + // ๐Ÿ“„ DETTAGLIO & MODIFICA VIAGGIO + // ---------------------------------------------------------- + { + active: true, + order: 330, + path: '/trasporti/ride/:id', + materialIcon: 'info', + faIcon: 'fas fa-info-circle', + name: 'mypages.TrasportiDettaglio', + component: () => import('@/modules/trasporti/pages/RideDetailPage.vue'), + inmenu: false, + submenu: false, + level_parent: 0, + level_child: 1, + meta: { + requiresAuth: false, + title: 'Dettaglio Viaggio' + } + }, + { + active: true, + order: 331, + path: '/trasporti/ride/:id/modifica', + materialIcon: 'edit', + faIcon: 'fas fa-edit', + name: 'mypages.TrasportiModifica', + component: () => import('@/modules/trasporti/pages/RideCreatePage.vue'), + // props: { editMode: true }, + inmenu: false, + submenu: false, + level_parent: 0, + level_child: 1, + meta: { + requiresAuth: true, + title: 'Modifica Viaggio' + } + }, + + // ---------------------------------------------------------- + // ๐Ÿ‘ค I MIEI VIAGGI + // ---------------------------------------------------------- + { + active: true, + order: 340, + path: '/trasporti/rides/my', + materialIcon: 'folder_shared', + faIcon: 'fas fa-folder-open', + name: 'mypages.TrasportiMieiViaggi', + component: () => import('@/modules/trasporti/pages/MyRidesPage.vue'), + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: true, + title: 'I Miei Viaggi' + } + }, + { + active: true, + order: 341, + path: '/trasporti/rides/my/conducente', + materialIcon: 'drive_eta', + faIcon: 'fas fa-car-side', + name: 'mypages.TrasportiMieiViaggiDriver', + component: () => import('@/modules/trasporti/pages/MyRidesPage.vue'), + // props: { defaultTab: 'driver' }, + inmenu: false, + submenu: false, + level_parent: 0, + level_child: 1, + meta: { + requiresAuth: true, + title: 'Viaggi come Conducente' + } + }, + { + active: true, + order: 342, + path: '/trasporti/miei-viaggi/passeggero', + materialIcon: 'airline_seat_recline_normal', + faIcon: 'fas fa-user', + name: 'mypages.TrasportiMieiViaggiPassenger', + component: () => import('@/modules/trasporti/pages/MyRidesPage.vue'), + // props: { defaultTab: 'passenger' }, + inmenu: false, + submenu: false, + level_parent: 0, + level_child: 1, + meta: { + requiresAuth: true, + title: 'Viaggi come Passeggero' + } + }, + { + active: true, + order: 343, + path: '/trasporti/storico', + materialIcon: 'history', + faIcon: 'fas fa-history', + name: 'mypages.TrasportiStorico', + component: () => import('@/modules/trasporti/pages/MyRidesPage.vue'), + // props: { defaultTab: 'history' }, + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: true, + title: 'Storico Viaggi' + } + }, + + // ---------------------------------------------------------- + // ๐Ÿ’ฌ CHAT & MESSAGGI + // ---------------------------------------------------------- + { + active: true, + order: 350, + path: '/trasporti/chat', + materialIcon: 'chat', + faIcon: 'fas fa-comments', + name: 'mypages.TrasportiChatList', + component: () => import('@/modules/trasporti/pages/ChatListPage.vue'), + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: true, + title: 'Messaggi' + } + }, + { + active: true, + order: 351, + path: '/trasporti/chat/:id', + materialIcon: 'chat_bubble', + faIcon: 'fas fa-comment', + name: 'mypages.TrasportiChatDetail', + component: () => import('@/modules/trasporti/pages/ChatPage.vue'), + inmenu: false, + submenu: false, + level_parent: 0, + level_child: 1, + meta: { + requiresAuth: true, + title: 'Conversazione' + } + }, + + // ---------------------------------------------------------- + // ๐Ÿ‘จโ€โœˆ๏ธ PROFILO CONDUCENTE + // ---------------------------------------------------------- + { + active: true, + order: 360, + path: '/trasporti/profilo/:userId', + materialIcon: 'person', + faIcon: 'fas fa-user', + name: 'mypages.TrasportiProfiloDriver', + component: () => import('@/modules/trasporti/pages/DriverProfilePage.vue'), + inmenu: false, + submenu: false, + level_parent: 0, + level_child: 1, + meta: { + requiresAuth: false, + title: 'Profilo Conducente' + } + }, + { + active: true, + order: 361, + path: '/trasporti/mio-profilo', + materialIcon: 'account_circle', + faIcon: 'fas fa-user-circle', + name: 'mypages.TrasportiMioProfilo', + component: () => import('@/modules/trasporti/pages/DriverProfilePage.vue'), + // props: { isOwnProfile: true }, + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: true, + title: 'Il Mio Profilo' + } + }, + + // ---------------------------------------------------------- + // ๐Ÿš— VEICOLI + // ---------------------------------------------------------- + { + active: true, + order: 370, + path: '/trasporti/veicoli', + materialIcon: 'garage', + faIcon: 'fas fa-car-alt', + name: 'mypages.TrasportiVeicoli', + component: () => import('@/modules/trasporti/pages/Vehiclespage.vue'), + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: true, + title: 'I Miei Veicoli' + } + }, + { + active: true, + order: 371, + path: '/trasporti/veicoli/nuovo', + materialIcon: 'add', + faIcon: 'fas fa-plus', + name: 'mypages.TrasportiVeicoloNuovo', + component: () => import('@/modules/trasporti/pages/Vehicleeditpage.vue'), + inmenu: false, + submenu: false, + level_parent: 0, + level_child: 1, + meta: { + requiresAuth: true, + title: 'Aggiungi Veicolo' + } + }, + { + active: true, + order: 372, + path: '/trasporti/veicoli/:vehicleId/modifica', + materialIcon: 'edit', + faIcon: 'fas fa-edit', + name: 'mypages.TrasportiVeicoloModifica', + component: () => import('@/modules/trasporti/pages/Vehicleeditpage.vue'), + // props: { editMode: true }, + inmenu: false, + submenu: false, + level_parent: 0, + level_child: 1, + meta: { + requiresAuth: true, + title: 'Modifica Veicolo' + } + }, + + // ---------------------------------------------------------- + // โญ FEEDBACK & RECENSIONI + // ---------------------------------------------------------- + { + active: true, + order: 380, + path: '/trasporti/feedback/viaggio/:rideId', + materialIcon: 'rate_review', + faIcon: 'fas fa-star', + name: 'mypages.TrasportiFeedbackRide', + component: () => import('@/modules/trasporti/pages/Myfeedbackpage.vue'), + inmenu: false, + submenu: false, + level_parent: 0, + level_child: 1, + meta: { + requiresAuth: true, + title: 'Lascia Feedback' + } + }, + { + active: true, + order: 381, + path: '/trasporti/feedback/viaggio/:rideId/utente/:toUserId', + materialIcon: 'star_rate', + faIcon: 'fas fa-star-half-alt', + name: 'mypages.TrasportiFeedbackUser', + component: () => import('@/modules/trasporti/pages/Myfeedbackpage.vue'), + inmenu: false, + submenu: false, + level_parent: 0, + level_child: 1, + meta: { + requiresAuth: true, + title: 'Valuta Utente' + } + }, + { + active: true, + order: 382, + path: '/trasporti/miei-feedback', + materialIcon: 'reviews', + faIcon: 'fas fa-comments', + name: 'mypages.TrasportiMieiFeedback', + component: () => import('@/modules/trasporti/pages/Myfeedbackpage.vue'), + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: true, + title: 'I Miei Feedback' + } + }, + + // ---------------------------------------------------------- + // ๐Ÿ“ฉ RICHIESTE PASSAGGIO + // ---------------------------------------------------------- + { + active: true, + order: 390, + path: '/trasporti/richieste', + materialIcon: 'inbox', + faIcon: 'fas fa-inbox', + name: 'mypages.TrasportiRichieste', + component: () => import('@/modules/trasporti/pages/Requestspage.vue'), + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: true, + title: 'Richieste Passaggio' + } + }, + { + active: true, + order: 391, + path: '/trasporti/richieste/ricevute', + materialIcon: 'move_to_inbox', + faIcon: 'fas fa-inbox', + name: 'mypages.TrasportiRichiesteRicevute', + component: () => import('@/modules/trasporti/pages/Requestspage.vue'), + // props: { defaultTab: 'received' }, + inmenu: false, + submenu: false, + level_parent: 0, + level_child: 1, + meta: { + requiresAuth: true, + title: 'Richieste Ricevute' + } + }, + { + active: true, + order: 392, + path: '/trasporti/richieste/inviate', + materialIcon: 'send', + faIcon: 'fas fa-paper-plane', + name: 'mypages.TrasportiRichiesteInviate', + component: () => import('@/modules/trasporti/pages/Requestspage.vue'), + // props: { defaultTab: 'sent' }, + inmenu: false, + submenu: false, + level_parent: 0, + level_child: 1, + meta: { + requiresAuth: true, + title: 'Richieste Inviate' + } + }, + + // ---------------------------------------------------------- + // โš™๏ธ IMPOSTAZIONI + // ---------------------------------------------------------- + { + active: true, + order: 400, + path: '/trasporti/impostazioni', + materialIcon: 'settings', + faIcon: 'fas fa-cog', + name: 'mypages.TrasportiSettings', + component: () => import('@/modules/trasporti/pages/Settingspage.vue'), + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: true, + title: 'Impostazioni Trasporti' + } + }, + + // ---------------------------------------------------------- + // โ“ HELP & INFO + // ---------------------------------------------------------- + { + active: true, + order: 410, + path: '/trasporti/come-funziona', + materialIcon: 'help', + faIcon: 'fas fa-question-circle', + name: 'mypages.TrasportiHelp', + component: () => import('@/modules/trasporti/pages/Helppage.vue'), + inmenu: true, + submenu: true, + level_parent: 0, + level_child: 0.5, + meta: { + requiresAuth: false, + title: 'Come Funziona' + } + }, + ]; + + // ============================================================ + // ๐Ÿ“‹ MENU PRINCIPALE TRASPORTI + // ============================================================ + const menuTrasporti: IListRoutes[] = [ + { + active: site.confpages?.enableTrasporti ?? true, + path: '/trasporti', + order: 1402, + faIcon: 'fas fa-car-side', + materialIcon: 'commute', + name: 'mypages.menuTrasporti', + routes2: routes_trasporti.filter(r => r.submenu && r.active), + inmenu: true, + onlyif_logged: false, // Menu visibile a tutti + // onlySocioResidente: true, // Decommentare se serve + solotitle: true, + infooter: true, + // Badge per notifiche non lette + //badge: { + // getter: 'trasporti/getTotalNotifications', + // color: 'negative' + //} + }, + // Spread di tutte le routes per il Vue Router + ...routes_trasporti, + ]; + + return menuTrasporti; +} + +/** + * Ottieni le quick actions per il FAB o menu rapido + */ +export function getTrasportiQuickActions() { + return [ + { + label: 'Offri Passaggio', + icon: 'directions_car', + color: 'positive', + path: '/trasporti/offri' + }, + { + label: 'Cerca Passaggio', + icon: 'hail', + color: 'info', + path: '/trasporti/richiedi' + }, + { + label: 'Cerca', + icon: 'search', + color: 'primary', + path: '/trasporti/cerca' + }, + { + label: 'I Miei Viaggi', + icon: 'folder_shared', + color: 'warning', + path: '/trasporti/miei-viaggi' + } + ]; +} + +// ============================================================ +// ๐Ÿ“ค EXPORT DEFAULT +// ============================================================ +export const routesTrasporti = { + routesTrasporti: getRoutesTrasporti, +}; diff --git a/src/statics/lang/it.js b/src/statics/lang/it.js index 2f1da9fa..1aaf89d8 100755 --- a/src/statics/lang/it.js +++ b/src/statics/lang/it.js @@ -1298,6 +1298,37 @@ const msg_it = { date_updated: 'Ult. Aggiornamento', }, mypages: { + TrasportiHome: 'Trasporti', + TrasportiDashboard: 'Dashboard', + TrasportiLista: 'Lista Viaggi', + TrasportiCerca: 'Cerca Passaggio', + TrasportiMappa: 'Mappa', + TrasportiCrea: 'Nuovo Viaggio', + TrasportiOffri: 'Offri Passaggio', + TrasportiRichiedi: 'Cerca Passaggio', + TrasportiDettaglio: 'Dettaglio Viaggio', + TrasportiModifica: 'Modifica Viaggio', + TrasportiMieiViaggi: 'I Miei Viaggi', + TrasportiMieiViaggiDriver: 'Come Conducente', + TrasportiMieiViaggiPassenger: 'Come Passeggero', + TrasportiStorico: 'Storico', + TrasportiChatList: 'Messaggi', + TrasportiChatDetail: 'Chat', + TrasportiProfiloDriver: 'Profilo', + TrasportiMioProfilo: 'Il Mio Profilo', + TrasportiVeicoli: 'I Miei Veicoli', + TrasportiVeicoloNuovo: 'Nuovo Veicolo', + TrasportiVeicoloModifica: 'Modifica Veicolo', + TrasportiFeedbackRide: 'Feedback Viaggio', + TrasportiFeedbackUser: 'Valuta Utente', + TrasportiMieiFeedback: 'I Miei Feedback', + TrasportiRichieste: 'Richieste', + TrasportiRichiesteRicevute: 'Ricevute', + TrasportiRichiesteInviate: 'Inviate', + TrasportiSettings: 'Impostazioni', + TrasportiHelp: 'Come Funziona', + menuTrasporti: 'Trasporti', + menuPoster: 'Poster', Settings: 'Impostazioni', AssetsList: 'Lista Inventario', @@ -1882,6 +1913,7 @@ const msg_it = { enableCircuits: 'Circuiti', enableAI: 'AI Tools', enablePoster: 'Generatore Poster', + enableTrasporti: 'Trasporti', enableProj: 'Mostra Project', enableTodos: 'Mostra Todos', enableRegByBot: 'Registrazione usando il BOT', diff --git a/src/store/Api/index.ts b/src/store/Api/index.ts index 91e959a7..28d593ea 100755 --- a/src/store/Api/index.ts +++ b/src/store/Api/index.ts @@ -49,11 +49,11 @@ export const Api = { return await Request('post', path, payload, responsedata, options); }, -async postFormData(path: string, payload?: any, responsedata?: any, options?: any) { - const globalStore = useGlobalStore(); - globalStore.connData.downloading_server = 1; - globalStore.connData.uploading_server = 1; - return await Request('postFormData', path, payload, responsedata, options); + async postFormData(path: string, payload?: any, responsedata?: any, options?: any) { + const globalStore = useGlobalStore(); + globalStore.connData.downloading_server = 1; + globalStore.connData.uploading_server = 1; + return await Request('postFormData', path, payload, responsedata, options); }, async get(path: string, payload?: any, responsedata?: any) { @@ -156,7 +156,11 @@ async postFormData(path: string, payload?: any, responsedata?: any, options?: an const newAccessToken = await this.refreshToken(); if (newAccessToken) { - userStore.setAuth(newAccessToken, userStore.refreshToken, userStore.browser_random); + userStore.setAuth( + newAccessToken, + userStore.refreshToken, + userStore.browser_random + ); if (!evitaloop) { console.log('Nuovo token ottenuto. Riprovo la richiesta...'); @@ -201,12 +205,12 @@ async postFormData(path: string, payload?: any, responsedata?: any, options?: an async SendReqBase( url: string, method: string, - mydata: any, + mydata?: any, setAuthToken = false, evitaloop = false, - myformdata: any, - responsedata: any, - options: any + myformdata: any = null, + responsedata: any = null, + options: any = null ) { const mydataout = { ...mydata, @@ -278,7 +282,6 @@ async postFormData(path: string, payload?: any, responsedata?: any, options?: an userStore.setAuth('', ''); // throw { code: toolsext.ERR_AUTHENTICATION }; throw { status: toolsext.ERR_RETRY_LOGIN }; - } } @@ -292,8 +295,12 @@ async postFormData(path: string, payload?: any, responsedata?: any, options?: an ); if (ret) return ret; + // if (error.status !== serv_constants.RIS_CODE__HTTP_BAD_REQUEST) { console.error('Errore nella richiesta:', error); throw error; + // } else { + // return error; + // } } }, @@ -333,7 +340,7 @@ async postFormData(path: string, payload?: any, responsedata?: any, options?: an async SendReq( url: string, method: string, - mydata: any, + mydata?: any, setAuthToken = false, evitaloop = false, retryCount = 1, @@ -359,6 +366,7 @@ async postFormData(path: string, payload?: any, responsedata?: any, options?: an error && error.status !== serv_constants.RIS_CODE__HTTP_INVALID_TOKEN && error.status !== serv_constants.RIS_CODE__HTTP_FORBIDDEN_PERMESSI && + error.status !== serv_constants.RIS_CODE__HTTP_BAD_REQUEST && error.status !== toolsext.ERR_RETRY_LOGIN; if (retryCount > 0 && riprova) { console.log(`โŒโŒโŒ Retrying request. Attempts remaining: ${retryCount}`); diff --git a/src/store/Modules/serv_constants.ts b/src/store/Modules/serv_constants.ts index d21ec00b..78698921 100755 --- a/src/store/Modules/serv_constants.ts +++ b/src/store/Modules/serv_constants.ts @@ -31,6 +31,7 @@ export const serv_constants = { RIS_CODE_OK: 1, RIS_CODE_LOGIN_OK: 1, RIS_ISCRIZIONE_OK: 5, + RIS_CODE__HTTP_BAD_REQUEST: 400, RIS_CODE__HTTP_INVALID_TOKEN: 401, RIS_CODE__HTTP_FORBIDDEN_PERMESSI: 403, RIS_CODE__HTTP_TOKEN_EXPIRED: 408, diff --git a/src/store/globalStore.ts b/src/store/globalStore.ts index 230e1aa5..ed7439e6 100644 --- a/src/store/globalStore.ts +++ b/src/store/globalStore.ts @@ -53,6 +53,7 @@ import { routesAI } from '@/router/routesAI'; import LandingFooter from '@/components/LandingFooter/LandingFooter'; import { useProducts } from '@store/Products'; import { useCatalogStore } from './CatalogStore'; +import getRoutesTrasporti, { routesTrasporti } from '../router/routesTrasporti'; const stateConnDefault = 'online'; @@ -182,6 +183,7 @@ export const useGlobalStore = defineStore('GlobalStore', { enableEcommerce: false, enableAI: false, enablePoster: false, + enableTrasporti: false, enableGroups: false, enableCircuits: false, enableProj: false, @@ -842,6 +844,7 @@ export const useGlobalStore = defineStore('GlobalStore', { const ecommRoutes = routesECommerce.routesEcomm(this.site); const AIRoutes = routesAI.routesAI(this.site); const PosterRoutes = routesAI.routesPoster(this.site); + const TrasportiRoutes = routesTrasporti.routesTrasporti(this.site); const last = { active: true, @@ -878,6 +881,7 @@ export const useGlobalStore = defineStore('GlobalStore', { ...arrpagesroute, ...AIRoutes, ...PosterRoutes, + ...TrasportiRoutes, last, ]; } diff --git a/tsconfig.json b/tsconfig.json index f43679b0..fe4fc527 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -78,6 +78,9 @@ "@views": [ "src/views/*" ], + "@modules": [ + "src/modules/*" + ], "@/*": [ "src/*" ], @@ -132,9 +135,6 @@ "@store/*": [ "src/store/*" ], - "@modules": [ - "src/store/Modules/index.ts" - ], "@model": [ "src/model/index.ts" ],