ソースを参照

freemium place

Vincent 4 ヶ月 前
コミット
1417dda7c2

+ 39 - 8
components/Form/Freemium/Event.vue

@@ -66,6 +66,7 @@
           listValue="id"
           listLabel="name"
           v-if="!newPlace"
+          @update:model-value="getPlace(entity)"
         />
 
         <div class="d-flex justify-center"
@@ -78,22 +79,30 @@
           >
             {{ $t('add_place') }}
           </v-btn>
+
+          <v-btn
+            prepend-icon="fa-solid fa-plus"
+            class="my-5"
+            @click="onEditPlaceClick(entity)"
+          >
+            {{ $t('edit_place') }}
+          </v-btn>
         </div>
 
-        <UiInputText v-if="newPlace" v-model="entity.placeName" field="placeName" />
+        <UiInputText :readonly="!newPlace && !editPlace" v-model="entity.placeName" field="placeName" />
 
-        <UiInputText v-if="newPlace" v-model="entity.streetAddress" field="streetAddress" />
+        <UiInputText :readonly="!newPlace && !editPlace" v-model="entity.streetAddress" field="streetAddress" />
 
-        <UiInputText v-if="newPlace" v-model="entity.streetAddressSecond" field="streetAddressSecond" />
+        <UiInputText :readonly="!newPlace && !editPlace" v-model="entity.streetAddressSecond" field="streetAddressSecond" />
 
-        <UiInputText v-if="newPlace" v-model="entity.streetAddressThird" field="streetAddressThird" />
+        <UiInputText :readonly="!newPlace && !editPlace" v-model="entity.streetAddressThird" field="streetAddressThird" />
 
-        <UiInputText v-if="newPlace" v-model="entity.postalCode" field="postalCode" />
+        <UiInputText :readonly="!newPlace && !editPlace" v-model="entity.postalCode" field="postalCode" />
 
-        <UiInputText v-if="newPlace" v-model="entity.addressCity" field="addressCity" />
+        <UiInputText :readonly="!newPlace && !editPlace" v-model="entity.addressCity" field="addressCity" />
 
         <UiInputAutocompleteApiResources
-          v-if="newPlace"
+          :readonly="!newPlace && !editPlace"
           v-model="entity.addressCountry"
           field="addressCountry"
           :model="Country"
@@ -103,7 +112,6 @@
 
         <client-only>
           <UiMapLeaflet
-            v-if="newPlace"
             v-model:latitude="entity.latitude"
             v-model:longitude="entity.longitude"
             :streetAddress="entity.streetAddress"
@@ -147,15 +155,18 @@
 
 <script setup lang="ts">
 import Event from "~/models/Freemium/Event";
+import Place from "~/models/Freemium/Place";
 import {getAssertUtils} from "~/services/asserts/getAssertUtils";
 import DateUtils from "~/services/utils/dateUtils";
 import Country from "~/models/Core/Country";
 import PlaceSearchItem from "~/models/Custom/Search/PlaceSearchItem";
+import {useEntityManager} from "~/composables/data/useEntityManager";
 
 const props = defineProps<{
   entity: Event
 }>()
 
+const {em} = useEntityManager()
 const getAsserts = (key) => getAssertUtils(Event.getAsserts(), key)
 
 const onUpdateDateTimeStart = (entity, dateTime) =>{
@@ -170,6 +181,7 @@ const onUpdateDateTimeEnd = (entity, dateTime) =>{
 }
 
 const newPlace: Ref<boolean> = ref(false)
+const editPlace: Ref<boolean> = ref(false)
 const onAddPlaceClick = function(entity: Event){
   newPlace.value = true
   entity.placeName = null
@@ -179,8 +191,27 @@ const onAddPlaceClick = function(entity: Event){
   entity.addressCity = null
   entity.postalCode = null
   entity.addressCountry = null
+  entity.latitude = null
+  entity.longitude = null
   entity.place = null
 }
+
+const onEditPlaceClick = function(){
+  editPlace.value = true
+}
+
+const getPlace = async (entity: Event)=>{
+  const placeInstance = await em.fetch(Place, entity.place as number)
+  entity.placeName = placeInstance.name
+  entity.streetAddress = placeInstance.streetAddress
+  entity.streetAddressSecond = placeInstance.streetAddressSecond
+  entity.streetAddressThird = placeInstance.streetAddressThird
+  entity.addressCity = placeInstance.addressCity
+  entity.postalCode = placeInstance.postalCode
+  entity.addressCountry = placeInstance.addressCountry
+  entity.latitude = placeInstance.latitude
+  entity.longitude = placeInstance.longitude
+}
 </script>
 
 <style scoped lang="scss">

+ 4 - 5
components/Layout/MobytStatus.vue

@@ -1,7 +1,7 @@
 <template>
   <v-col cols="12" lg="12">
     <strong>{{ $t('remaining_sms_credit') }}</strong> -
-    <span v-if="!mobytPending && mobytStatus !== null && mobytStatus.active">
+    <span v-if="mobytPendingStatus == FETCHING_STATUS.SUCCESS && mobytStatus !== null && mobytStatus.active">
       {{
         mobytStatus.money.toLocaleString($i18n.locale, {
           style: 'currency',
@@ -18,19 +18,18 @@
 </template>
 
 <script setup lang="ts">
-import type { Ref } from 'vue'
 import type { AsyncData } from '#app'
-import { useAbility } from '@casl/vue'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 import MobytUserStatus from '~/models/Organization/MobytUserStatus'
+import {FETCHING_STATUS} from "~/types/enum/data";
+
 
-const ability = useAbility()
 const { fetch } = useEntityFetch()
 const i18n = useI18n()
 const organizationProfile = useOrganizationProfileStore()
 
-const { data: mobytStatus, pending: mobytPending } = fetch(
+const { data: mobytStatus, status: mobytPendingStatus } = fetch(
   MobytUserStatus,
   organizationProfile.id,
 ) as AsyncData<MobytUserStatus | null, Error | null>

+ 43 - 1
components/Ui/Input/TreeSelect.vue

@@ -30,6 +30,7 @@ et sélectionner des éléments organisés en catégories et sous-catégories.
     <template #prepend-item>
       <!-- Champs de recherche textuelle -->
       <v-text-field
+        ref="searchInput"
         v-model="searchText"
         density="compact"
         hide-details
@@ -38,7 +39,9 @@ et sélectionner des éléments organisés en catégories et sous-catégories.
         variant="outlined"
         clearable
         class="mx-2 my-2"
-        @click.stop
+        @click.stop="focusSearchInput"
+        @mousedown.stop
+        @keydown.stop="onKeyDown"
         @input="onSearchInputDebounced"
         @click:clear.stop="onSearchClear"
       />
@@ -188,6 +191,45 @@ const props = defineProps({
   },
 })
 
+const searchInput = ref()
+
+/**
+ * Force le focus sur l'input de recherche
+ */
+const focusSearchInput = () => {
+  nextTick(() => {
+    if (searchInput.value?.$el) {
+      const input = searchInput.value.$el.querySelector('input')
+      if (input) {
+        input.focus()
+      }
+    }
+  })
+}
+
+/**
+ * Gère les événements clavier pour éviter les conflits avec la navigation du menu
+ */
+const onKeyDown = (event: KeyboardEvent) => {
+  // Empêcher la propagation pour tous les caractères alphanumériques
+  // et les touches spéciales de navigation
+  if (
+    event.key.length === 1 || // Caractères simples (a, c, etc.)
+    ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)
+  ) {
+    event.stopPropagation()
+  }
+
+  // Empêcher également Escape de fermer le menu quand on est dans l'input
+  if (event.key === 'Escape') {
+    event.stopPropagation()
+    // Optionnel : vider le texte de recherche
+    if (searchText.value) {
+      searchText.value = ''
+      onSearchClear()
+    }
+  }
+}
 /**
  * A computed property that normalizes the labels of all items upfront.
  * This avoids having to normalize labels during each search operation.

+ 1 - 1
i18n/lang/fr/general.json

@@ -1,10 +1,10 @@
 {
   "event_categories_choices": "Choisissez à quelles catégories appartient votre événement",
-  "refresh_page": "Actualiser la page",
   "search": "Rechercher",
   "others": "autres",
   "placeName": "Nom du lieu",
   "missing_value": "Champs vide",
+  "edit_place": "Éditer le lieu",
   "add_place": "Ajouter un nouveau lieu",
   "place": "Vos lieux déjà enregistrés",
   "place_event": "Lieu de votre événement",

+ 48 - 0
models/Freemium/Place.ts

@@ -0,0 +1,48 @@
+import { Str, Uid } from 'pinia-orm/dist/decorators'
+import ApiModel from '~/models/ApiModel'
+import {Assert, IriEncoded} from '~/models/decorators'
+import {Attr, Num} from "pinia-orm/decorators";
+import Country from "~/models/Core/Country";
+
+/**
+ * AP2i Model : Freemium / Place
+ *
+ * */
+export default class Place extends ApiModel {
+  static entity = 'freemium/places'
+
+  @Uid()
+  declare id: number | string | null
+
+  @Attr(null)
+  declare EventId: number | null
+
+  @Str(null)
+  @Assert({'nullable': false, 'max':255})
+  declare name: string | null
+
+  @Str(null)
+  declare streetAddress: string | null
+
+  @Str(null)
+  declare streetAddressSecond: string | null
+
+  @Str(null)
+  declare streetAddressThird: string | null
+
+  @Str(null)
+  declare postalCode: string | null
+
+  @Str(null)
+  declare addressCity: string | null
+
+  @IriEncoded(Country)
+  @Attr(null)
+  declare addressCountry: number | null
+
+  @Num(null)
+  declare latitude: number | null
+
+  @Num(null)
+  declare longitude: number | null
+}

+ 158 - 0
pages/dev/poc_tree_select_input.vue

@@ -0,0 +1,158 @@
+<template>
+  <div class="pa-4">
+    <h2>Select Hiérarchique - Exemple de base</h2>
+
+    <UiInputTreeSelect
+      ref="treeSelect"
+      v-model="selectedValues"
+      :items="hierarchicalItems"
+      label="Choisissez vos options"
+      placeholder="Sélectionnez des éléments..."
+      :max-visible-chips="2"
+    />
+
+    <div class="mt-4">
+      <h3>Valeurs sélectionnées :</h3>
+      <pre>{{ selectedValues }}</pre>
+    </div>
+
+    <v-divider class="my-6" />
+
+    <h2>Select Hiérarchique - Catégories d'événements</h2>
+
+    <UiInputTreeSelectEventCategories
+      v-model="selectedCategories"
+      label="Choisissez des catégories"
+      placeholder="Sélectionnez des catégories..."
+      :max-visible-chips="3"
+    />
+
+    <div class="mt-4">
+      <h3>Catégories sélectionnées :</h3>
+      <pre>{{ selectedCategories }}</pre>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+const selectedValues = ref<string[]>([])
+const selectedCategories = ref<string[]>([])
+
+const hierarchicalItems = ref([
+  // Catégories principales
+  { id: 'cat1', label: 'Électronique', type: 'category', level: 0 },
+  { id: 'cat2', label: 'Vêtements', type: 'category', level: 0 },
+
+  // Sous-catégories d'Électronique
+  {
+    id: 'subcat1',
+    label: 'Ordinateurs',
+    type: 'subcategory',
+    parentId: 'cat1',
+    level: 1,
+  },
+  {
+    id: 'subcat2',
+    label: 'Téléphones',
+    type: 'subcategory',
+    parentId: 'cat1',
+    level: 1,
+  },
+
+  // Items sous Ordinateurs
+  {
+    id: 'item1',
+    label: 'Laptop Gaming',
+    value: 'laptop-gaming',
+    type: 'item',
+    parentId: 'subcat1',
+    level: 2,
+  },
+  {
+    id: 'item2',
+    label: 'Laptop Bureau',
+    value: 'laptop-office',
+    type: 'item',
+    parentId: 'subcat1',
+    level: 2,
+  },
+  {
+    id: 'item3',
+    label: 'PC Desktop',
+    value: 'pc-desktop',
+    type: 'item',
+    parentId: 'subcat1',
+    level: 2,
+  },
+
+  // Items sous Téléphones
+  {
+    id: 'item4',
+    label: 'iPhone',
+    value: 'iphone',
+    type: 'item',
+    parentId: 'subcat2',
+    level: 2,
+  },
+  {
+    id: 'item5',
+    label: 'Android',
+    value: 'android',
+    type: 'item',
+    parentId: 'subcat2',
+    level: 2,
+  },
+
+  // Sous-catégories de Vêtements
+  {
+    id: 'subcat3',
+    label: 'Homme',
+    type: 'subcategory',
+    parentId: 'cat2',
+    level: 1,
+  },
+  {
+    id: 'subcat4',
+    label: 'Femme',
+    type: 'subcategory',
+    parentId: 'cat2',
+    level: 1,
+  },
+
+  // Items sous Homme
+  {
+    id: 'item6',
+    label: 'Chemises',
+    value: 'chemises-homme',
+    type: 'item',
+    parentId: 'subcat3',
+    level: 2,
+  },
+  {
+    id: 'item7',
+    label: 'Pantalons',
+    value: 'pantalons-homme',
+    type: 'item',
+    parentId: 'subcat3',
+    level: 2,
+  },
+
+  // Items sous Femme
+  {
+    id: 'item8',
+    label: 'Robes',
+    value: 'robes',
+    type: 'item',
+    parentId: 'subcat4',
+    level: 2,
+  },
+  {
+    id: 'item9',
+    label: 'Blouses',
+    value: 'blouses',
+    type: 'item',
+    parentId: 'subcat4',
+    level: 2,
+  },
+])
+</script>

+ 15 - 9
pages/parameters/attendances.vue

@@ -1,38 +1,44 @@
 <template>
   <div>
     <LayoutCommonSection v-if="organizationProfile.isSchool">
-      <h4>{{ $t('alert_configuration') }}</h4>
+      <h4>{{ $t('configuration') }}</h4>
       <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
         <template #default="{ entity : parameters }">
           <div v-if="parameters">
             <v-row>
               <v-col cols="12">
+
                 <h5 class="pa-2">{{ $t('showing') }}</h5>
                 <UiInputCheckbox
                   v-model="parameters.handlePresence"
                   field="handlePresence"
+                  label="handlePresence"
                 />
-            <h5 class="pa-2">{{ $t('alert') }}</h5>
-              <UiInputCheckbox
-                v-model="parameters.sendAttendanceEmail"
-                field="sendAttendanceEmail"
-                label="sendAttendanceEmail"
-              />
 
-              <UiInputCheckbox
+                <h5 class="pa-2">{{ $t('alert') }}</h5>
+                <UiInputCheckbox
+                  v-model="parameters.sendAttendanceEmail"
+                  field="sendAttendanceEmail"
+                  label="sendAttendanceEmail"
+                />
+
+                <UiInputCheckbox
                   v-model="parameters.sendAttendanceSms"
                   field="sendAttendanceSms"
+                  label="sendAttendanceSms"
                 />
 
                 <UiInputCheckbox
                   v-model="parameters.notifyAdministrationAbsence"
                   field="notifyAdministrationAbsence"
+                  label="notifyAdministrationAbsence"
                 />
 
                 <UiInputNumber
                   v-if="parameters.notifyAdministrationAbsence"
                   v-model="parameters.numberConsecutiveAbsences"
                   field="numberConsecutiveAbsences"
+                  label="numberConsecutiveAbsences"
                   :rules="getAsserts('numberConsecutiveAbsences')"
                 />
               </v-col>
@@ -53,7 +59,7 @@
 </template>
 <script setup lang="ts">
 import Parameters from '~/models/Organization/Parameters'
-import { useOrganizationProfileStore } from '~/stores/organizationProfile'
+import {useOrganizationProfileStore} from '~/stores/organizationProfile'
 import AttendanceBookingReason from '~/models/Booking/AttendanceBookingReason'
 import {getAssertUtils} from "~/services/asserts/getAssertUtils";