Browse Source

Merge tag '2.6' into develop

Vincent 3 months ago
parent
commit
eb5870a4b0
100 changed files with 1917 additions and 1197 deletions
  1. 261 163
      components/Form/Freemium/Event.vue
  2. 13 5
      components/Form/Parameter/AttendanceBookingReason.vue
  3. 12 5
      components/Form/Parameter/EducationTiming.vue
  4. 12 5
      components/Form/Parameter/ResidenceArea.vue
  5. 6 2
      components/Layout/Alert/Content.vue
  6. 1 1
      components/Layout/AlertBar/Cotisation.vue
  7. 1 1
      components/Layout/AlertBar/OnlineRegistration.vue
  8. 1 1
      components/Layout/AlertBar/RegistrationStatus.vue
  9. 3 3
      components/Layout/Header.vue
  10. 6 6
      components/Layout/Header/Menu.vue
  11. 2 2
      components/Layout/Header/Notification.vue
  12. 9 1
      components/Layout/Header/Title.vue
  13. 8 3
      components/Layout/MobytStatus.vue
  14. 3 4
      components/Layout/Parameters/EntityTable.vue
  15. 1 1
      components/Layout/Parameters/Table.vue
  16. 8 8
      components/Layout/SubHeader/Breadcrumbs.vue
  17. 2 2
      components/Layout/SubHeader/PersonnalizedList.vue
  18. 4 1
      components/Layout/Subheader.vue
  19. 7 8
      components/Ui/Button/Delete.vue
  20. 25 23
      components/Ui/DatePicker.vue
  21. 76 30
      components/Ui/EventList.vue
  22. 13 10
      components/Ui/Form.vue
  23. 23 21
      components/Ui/Form/Creation.vue
  24. 31 28
      components/Ui/Form/Edition.vue
  25. 11 3
      components/Ui/Input/Autocomplete/Accesses.vue
  26. 71 23
      components/Ui/Input/Autocomplete/ApiResources.vue
  27. 3 3
      components/Ui/Input/Autocomplete/Enum.vue
  28. 17 15
      components/Ui/Input/DatePicker.vue
  29. 20 11
      components/Ui/Input/DateTimePicker.vue
  30. 1 4
      components/Ui/Input/Email.vue
  31. 15 0
      components/Ui/Input/Image.vue
  32. 1 2
      components/Ui/Input/Number.vue
  33. 8 12
      components/Ui/Input/Phone.vue
  34. 1 1
      components/Ui/Input/TextArea.vue
  35. 105 62
      components/Ui/Input/TreeSelect.vue
  36. 3 5
      components/Ui/Input/TreeSelect/EventCategories.vue
  37. 118 0
      components/Ui/Input/TreeSelect/TypeOfPractices.vue
  38. 78 59
      components/Ui/MapLeaflet.client.vue
  39. 12 6
      composables/data/useAp2iRequestService.ts
  40. 9 11
      composables/data/useEntityFetch.ts
  41. 4 4
      composables/data/useEntityManager.ts
  42. 1 1
      composables/form/useDeleteItem.ts
  43. 4 6
      composables/utils/useDownloadFile.ts
  44. 3 3
      composables/utils/useHomeUrl.ts
  45. 3 0
      config/theme.ts
  46. 1 1
      eslint.config.mjs
  47. 30 0
      i18n/lang/fr/breadcrumbs.json
  48. 1 0
      i18n/lang/fr/event_categories.json
  49. 31 26
      i18n/lang/fr/general.json
  50. 109 62
      layouts/freemium.vue
  51. 3 1
      middleware/routing.global.ts
  52. 2 2
      models/Billing/ResidenceArea.ts
  53. 2 2
      models/Booking/AttendanceBookingReason.ts
  54. 18 0
      models/Booking/EventGender.ts
  55. 2 2
      models/Education/Cycle.ts
  56. 2 2
      models/Education/EducationTiming.ts
  57. 19 10
      models/Freemium/Event.ts
  58. 23 11
      models/Freemium/Organization.ts
  59. 4 5
      models/Freemium/Place.ts
  60. 2 2
      models/Organization/Parameters.ts
  61. 5 1
      nuxt.config.ts
  62. 0 251
      pages/freemium/dashboard.vue
  63. 2 5
      pages/freemium/events/[id].vue
  64. 2 5
      pages/freemium/events/new.vue
  65. 316 0
      pages/freemium/index.vue
  66. 160 111
      pages/freemium/organization.vue
  67. 5 3
      pages/my-settings.vue
  68. 4 2
      pages/parameters/attendance_booking_reasons/[id].vue
  69. 0 1
      pages/parameters/attendance_booking_reasons/new.vue
  70. 4 6
      pages/parameters/attendances.vue
  71. 2 3
      pages/parameters/bulletin.vue
  72. 1 2
      pages/parameters/cycles/[id].vue
  73. 5 5
      pages/parameters/education_notation.vue
  74. 4 2
      pages/parameters/education_timings/[id].vue
  75. 0 1
      pages/parameters/education_timings/index.vue
  76. 0 1
      pages/parameters/education_timings/new.vue
  77. 11 12
      pages/parameters/general_parameters.vue
  78. 2 3
      pages/parameters/intranet.vue
  79. 0 1
      pages/parameters/residence_areas/[id].vue
  80. 0 1
      pages/parameters/residence_areas/index.vue
  81. 0 1
      pages/parameters/residence_areas/new.vue
  82. 13 10
      pages/parameters/sms.vue
  83. 1 1
      pages/parameters/subdomains/[id].vue
  84. 8 10
      pages/parameters/super_admin.vue
  85. 7 5
      pages/parameters/teaching.vue
  86. 1 1
      pages/parameters/website.vue
  87. 1 1
      pages/subscription.vue
  88. 6 6
      plugins/vPhoneInput.ts
  89. 16 12
      services/asserts/AssertRuleRegistry.ts
  90. 7 5
      services/asserts/MaxAssert.ts
  91. 9 8
      services/asserts/NullableAssert.ts
  92. 22 0
      services/asserts/PositiveAssert.ts
  93. 19 11
      services/asserts/TypeAssert.ts
  94. 5 5
      services/asserts/getAssertUtils.ts
  95. 1 1
      services/data/Filters/EqualFilter.ts
  96. 2 2
      services/data/Filters/InArrayFilter.ts
  97. 1 1
      services/data/Filters/OrderBy.ts
  98. 1 1
      services/data/Filters/PageFilter.ts
  99. 1 1
      services/data/Filters/SearchFilter.ts
  100. 8 8
      services/data/Filters/TimeFilter.ts

+ 261 - 163
components/Form/Freemium/Event.vue

@@ -1,58 +1,68 @@
 <template>
-    <LayoutCommonSection>
-      <v-row>
-        <v-col cols="12" sm="12">
-          <h4 class="mb-8">{{ $t('general_informations') }}</h4>
+  <LayoutCommonSection>
+    <v-row>
+      <v-col cols="12" sm="12">
+        <h4 class="mb-8">{{ $t('general_informations') }}</h4>
 
-          <UiInputText
-            v-model="entity.name"
-            field="name"
-            :rules="getAsserts('name')"
-          />
+        <UiInputText
+          v-model="proxyEntity.name"
+          field="name"
+          :rules="getAsserts('name')"
+        />
 
-          <span class="label">{{$t('datetimeStart')}}</span>
-          <UiInputDateTimePicker
-            v-model="entity.datetimeStart"
-            field="datetimeStart"
-            label="datetimeStart"
-            :withTimePicker="true"
-            class="my-2"
-            :rules="getAsserts('datetimeStart')"
-            validate-on-blur
-            @update:model-value="onUpdateDateTimeStart(entity, $event)"
-          />
+        <span class="label">{{ $t('datetimeStart') }}</span>
+        <UiInputDateTimePicker
+          :model-value="proxyEntity.datetimeStart"
+          field="datetimeStart"
+          label="datetimeStart"
+          :with-time-picker="true"
+          class="my-2"
+          :rules="getAsserts('datetimeStart')"
+          validate-on-blur
+          @update:model-value="onUpdateDateTimeStart(proxyEntity, $event)"
+        />
 
-          <span class="label">{{$t('datetimeEnd')}}</span>
-          <UiInputDateTimePicker
-            v-model="entity.datetimeEnd"
-            field="datetimeEnd"
-            label="datetimeEnd"
-            :withTimePicker="true"
-            class="my-2"
-            :rules="getAsserts('datetimeEnd')"
-            @update:model-value="onUpdateDateTimeEnd(entity, $event)"
-          />
+        <span class="label">{{ $t('datetimeEnd') }}</span>
+        <UiInputDateTimePicker
+          :model-value="proxyEntity.datetimeEnd"
+          field="datetimeEnd"
+          label="datetimeEnd"
+          :with-time-picker="true"
+          class="my-2"
+          :rules="getAsserts('datetimeEnd')"
+          @update:model-value="onUpdateDateTimeEnd(proxyEntity, $event)"
+        />
 
-          <span class="label">{{$t('description')}}</span>
-          <UiInputTextArea
-            class="mt-3"
-            v-model="entity.description"
-          />
+        <span class="label">{{ $t('description') }}</span>
+        <UiInputTextArea v-model="proxyEntity.description" class="mt-3" />
 
-          <UiInputTreeSelectEventCategories
-            v-model="entity.categories"
-            label="event_categories_choices"
-          />
+        <UiInputAutocompleteApiResources
+          v-model="proxyEntity.gender"
+          field="gender"
+          label="gender_event"
+          :model="EventGender"
+          list-value="id"
+          list-label="name"
+          :rules="getAsserts('gender')"
+          :pagination="false"
+          :api-filters="queryConditions"
+        />
 
-          <UiInputImage
-            v-model="entity.image"
-            field="image"
-            :width="240"
-            :cropping-enabled="true"
-          />
-        </v-col>
-      </v-row>
-    </LayoutCommonSection>
+        <UiInputTreeSelectEventCategories
+          v-model="proxyEntity.categories"
+          label="event_categories_choices"
+        />
+
+        <UiInputImage
+          v-model="proxyEntity.image"
+          field="image"
+          label="event_image"
+          :width="240"
+          :cropping-enabled="true"
+        />
+      </v-col>
+    </v-row>
+  </LayoutCommonSection>
 
   <LayoutCommonSection>
     <v-row>
@@ -60,158 +70,233 @@
         <h4 class="mb-8">{{ $t('place_event') }}</h4>
 
         <UiInputAutocompleteApiResources
-          v-model="entity.place"
+          v-if="!newPlace"
+          v-model="proxyEntity.place"
           field="place"
           :model="PlaceSearchItem"
-          listValue="id"
-          listLabel="name"
-          v-if="!newPlace"
-          @update:model-value="getPlace(entity)"
+          list-value="id"
+          list-label="name"
+          @update:model-value="getPlace(proxyEntity)"
         />
 
-        <v-row  v-if="!newPlace" class="mb-6 justify-center">
+        <v-row class="mb-6 justify-center">
           <v-col
-            v-if="entity.place && !editPlace"
+            v-if="newPlace"
             cols="12"
             sm="6"
             class="d-flex justify-center mb-2"
           >
-              <v-btn
-                prepend-icon="fa-solid fa-pencil"
-                @click="onEditPlaceClick(entity)"
-              >
-                {{ $t('edit_place') }}
-              </v-btn>
+            <v-btn
+              prepend-icon="fa-solid fa-cancel"
+              @click="onCancelPlaceClick(proxyEntity)"
+            >
+              {{ $t('cancel') }}
+            </v-btn>
           </v-col>
 
           <v-col
+            v-if="proxyEntity.place && !editPlace"
+            cols="12"
+            sm="6"
+            class="d-flex justify-center mb-2"
+          >
+            <v-btn
+              prepend-icon="fa-solid fa-pencil"
+              @click="onEditPlaceClick(proxyEntity)"
+            >
+              {{ $t('edit_place') }}
+            </v-btn>
+          </v-col>
+
+          <v-col
+            v-if="!newPlace"
             cols="12"
             sm="6"
             class="d-flex justify-center mb-2"
           >
             <v-btn
               prepend-icon="fa-solid fa-plus"
-              @click="onAddPlaceClick(entity)"
+              @click="onAddPlaceClick(proxyEntity)"
             >
               {{ $t('add_place') }}
             </v-btn>
           </v-col>
-
         </v-row>
 
+        <div v-if="newPlace || editPlace">
+          <UiInputText
+            v-model="proxyEntity.placeName"
+            :readonly="!newPlace && !editPlace"
+            field="placeName"
+          />
 
+          <UiInputText
+            v-model="proxyEntity.streetAddress"
+            :readonly="!newPlace && !editPlace"
+            field="streetAddress"
+          />
 
-        <UiInputText :readonly="!newPlace && !editPlace" v-model="entity.placeName" field="placeName" />
-
-        <UiInputText :readonly="!newPlace && !editPlace" v-model="entity.streetAddress" field="streetAddress" />
-
-        <UiInputText :readonly="!newPlace && !editPlace" v-model="entity.streetAddressSecond" field="streetAddressSecond" />
-
-        <UiInputText :readonly="!newPlace && !editPlace" v-model="entity.streetAddressThird" field="streetAddressThird" />
+          <UiInputText
+            v-model="proxyEntity.streetAddressSecond"
+            :readonly="!newPlace && !editPlace"
+            field="streetAddressSecond"
+          />
 
-        <UiInputText :readonly="!newPlace && !editPlace" v-model="entity.postalCode" field="postalCode" />
+          <UiInputText
+            v-model="proxyEntity.streetAddressThird"
+            :readonly="!newPlace && !editPlace"
+            field="streetAddressThird"
+          />
 
-        <UiInputText :readonly="!newPlace && !editPlace" v-model="entity.addressCity" field="addressCity" />
+          <UiInputText
+            v-model="proxyEntity.postalCode"
+            :readonly="!newPlace && !editPlace"
+            field="postalCode"
+          />
 
-        <UiInputAutocompleteApiResources
-          :readonly="!newPlace && !editPlace"
-          v-model="entity.addressCountry"
-          field="addressCountry"
-          :model="Country"
-          listValue="id"
-          listLabel="name"
-        />
+          <UiInputText
+            v-model="proxyEntity.addressCity"
+            :readonly="!newPlace && !editPlace"
+            field="addressCity"
+          />
 
-        <client-only>
-          <UiMapLeaflet
+          <UiInputAutocompleteApiResources
+            v-model="proxyEntity.addressCountry"
             :readonly="!newPlace && !editPlace"
-            v-model:latitude="entity.latitude"
-            v-model:longitude="entity.longitude"
-            :streetAddress="entity.streetAddress"
-            :streetAddressSecond="entity.streetAddressSecond"
-            :streetAddressThird="entity.streetAddressThird"
-            :postalCode="entity.postalCode"
-            :addressCity="entity.addressCity"
-            :addressCountryId="entity.addressCountry"
-            :searchButton="true"
-          ></UiMapLeaflet>
-        </client-only>
+            field="addressCountry"
+            :model="Country"
+            list-value="id"
+            list-label="name"
+          />
 
+          <client-only>
+            <UiMapLeaflet
+              v-model:latitude="proxyEntity.latitude"
+              v-model:longitude="proxyEntity.longitude"
+              :readonly="!newPlace && !editPlace"
+              :street-address="proxyEntity.streetAddress"
+              :street-address-second="proxyEntity.streetAddressSecond"
+              :street-address-third="proxyEntity.streetAddressThird"
+              :postal-code="proxyEntity.postalCode"
+              :address-city="proxyEntity.addressCity"
+              :address-country-id="proxyEntity.addressCountry"
+              :search-button="true"
+            ></UiMapLeaflet>
+          </client-only>
+        </div>
       </v-col>
     </v-row>
   </LayoutCommonSection>
 
-    <LayoutCommonSection>
-      <v-row>
-        <v-col cols="12">
+  <LayoutCommonSection>
+    <v-row>
+      <v-col cols="12">
+        <h4 class="mb-8">{{ $t('communication_params') }}</h4>
+
+        <UiInputText
+          v-model="proxyEntity.url"
+          field="url"
+          :rules="getAsserts('url')"
+        />
 
-          <h4 class="mb-8">{{ $t('communication_params') }}</h4>
+        <UiInputAutocompleteEnum
+          v-model="proxyEntity.pricing"
+          enum-name="pricing_event"
+          field="pricing"
+        />
 
-          <UiInputText v-model="entity.url" field="url" />
+        <UiInputText
+          v-if="proxyEntity.pricing === 'PAID'"
+          v-model="proxyEntity.urlTicket"
+          :rules="getAsserts('urlTicket')"
+          field="urlTicket"
+        />
 
-          <UiInputAutocompleteEnum
-            v-model="entity.pricing"
-            enum-name="pricing_event"
-            field="pricing"
-          />
+        <UiInputNumber
+          v-if="proxyEntity.pricing === 'PAID'"
+          v-model="proxyEntity.priceMini"
+          field="priceMini"
+          :rules="getAsserts('priceMini')"
+        />
 
-          <UiInputText v-if="entity.pricing==='PAID'" v-model="entity.urlTicket" field="urlTicket" />
-
-          <UiInputNumber v-if="entity.pricing==='PAID'" v-model="entity.priceMini" field="priceMini" />
-
-          <UiInputNumber v-if="entity.pricing==='PAID'" v-model="entity.priceMaxi" field="priceMaxi" />
-
-        </v-col>
-      </v-row>
-    </LayoutCommonSection>
-
-    <LazyLayoutDialog :show="showAlert" theme="warning">
-      <template #dialogType>{{ $t('important') }}</template>
-      <template #dialogTitle>{{ $t('place_change_everywhere') }}</template>
-      <template #dialogText>
-        <v-card-text class="text">
-          <p>
-            {{$t('warning_edit_place')}}
-          </p>
-        </v-card-text>
-      </template>
-      <template #dialogBtn>
-        <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="closeDialog">
-          {{ $t('cancel') }}
-        </v-btn>
-        <v-btn class="mr-4 submitBtn theme-warning" @click="onEditPlaceConfirm">
-          {{ $t('i_understand') }}
-        </v-btn>
-      </template>
-    </LazyLayoutDialog>
+        <UiInputNumber
+          v-if="proxyEntity.pricing === 'PAID'"
+          v-model="proxyEntity.priceMaxi"
+          field="priceMaxi"
+          :rules="getAsserts('priceMaxi')"
+        />
+      </v-col>
+    </v-row>
+  </LayoutCommonSection>
+
+  <LazyLayoutDialog :show="showAlert" theme="warning">
+    <template #dialogType>{{ $t('important') }}</template>
+    <template #dialogTitle>{{ $t('place_change_everywhere') }}</template>
+    <template #dialogText>
+      <v-card-text class="text">
+        <p>
+          {{ $t('warning_edit_place') }}
+        </p>
+        <br />
+        <p>
+          {{ $t('are_you_sure_to_process') }}
+        </p>
+      </v-card-text>
+    </template>
+    <template #dialogBtn>
+      <v-btn class="mr-4 submitBtn theme-neutral-strong" @click="closeDialog">
+        {{ $t('cancel') }}
+      </v-btn>
+      <v-btn class="mr-4 submitBtn theme-warning" @click="onEditPlaceConfirm">
+        {{ $t('i_understand') }}
+      </v-btn>
+    </template>
+  </LazyLayoutDialog>
 </template>
 
 <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";
+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'
+import EventGender from '~/models/Booking/EventGender'
+import EqualFilter from '~/services/data/Filters/EqualFilter'
 
 const props = defineProps<{
   entity: Event
 }>()
 
-const {em} = useEntityManager()
+const { em } = useEntityManager()
 const getAsserts = (key) => getAssertUtils(Event.getAsserts(), key)
 
+const emit = defineEmits(['update:entity'])
+
+// Pour éviter l'erreur eslint "Unexpected mutation of "modelValue" prop"
+const proxyEntity = computed({
+  get: () => props.entity,
+  set: (value) => emit('update:entity', value),
+})
+
+const queryConditions = [new EqualFilter('type', 'CULTURAL_EVENT')]
+
 /**
  * Si la date de début est mise à jour, on s'assure que la date de fin est
  * après elle, sinon elle devient égale
  * @param entity
  * @param dateTime
  */
-const onUpdateDateTimeStart = (entity, dateTime) =>{
-  if(DateUtils.isBefore(props.entity.datetimeEnd, dateTime, false)){
-    entity.datetimeEnd = dateTime
+const onUpdateDateTimeStart = (entity, dateTime) => {
+  if (DateUtils.isBefore(props.entity.datetimeEnd, dateTime, false)) {
+    const dateTimeEnd = new Date(dateTime)
+    // Ajouter 1h
+    dateTimeEnd.setHours(dateTimeEnd.getHours() + 1)
+    entity.datetimeEnd = DateUtils.toIsoUtcOffset(dateTimeEnd)
   }
+  entity.datetimeStart = dateTime
+  emit('update:entity', entity)
 }
 
 /**
@@ -220,10 +305,15 @@ const onUpdateDateTimeStart = (entity, dateTime) =>{
  * @param entity
  * @param dateTime
  */
-const onUpdateDateTimeEnd = (entity, dateTime) =>{
-  if(DateUtils.isBefore(dateTime, props.entity.datetimeStart, false)){
-    entity.datetimeStart = dateTime
+const onUpdateDateTimeEnd = (entity, dateTime) => {
+  if (DateUtils.isBefore(dateTime, props.entity.datetimeStart, false)) {
+    const dateTimeStart = new Date(dateTime)
+    // Retirer 1h
+    dateTimeStart.setHours(dateTimeStart.getHours() - 1)
+    entity.datetimeStart = DateUtils.toIsoUtcOffset(dateTimeStart)
   }
+  entity.datetimeEnd = dateTime
+  emit('update:entity', entity)
 }
 
 const showAlert: Ref<boolean> = ref(false)
@@ -231,45 +321,51 @@ const newPlace: Ref<boolean> = ref(false)
 const editPlace: Ref<boolean> = ref(false)
 
 /**
- * Si on clic sur le bouton "Ajouter un lieu", le choix dans la liste déroulante
+ * Si on clique sur le bouton "Ajouter un lieu", le choix dans la liste déroulante
  * est mise à nulle, et les champs input de l'adresse sont vidées
  * @param entity
  */
-const onAddPlaceClick = function(entity: Event){
+const onAddPlaceClick = function (entity: Event) {
   newPlace.value = true
   entity.place = null
   resetPlace(entity)
 }
 
+const onCancelPlaceClick = function (entity: Event) {
+  newPlace.value = false
+  entity.place = null
+  resetPlace(entity)
+}
+
 /**
- * Quand on clic sur le bouton "Editer le lieu", une alerte s'affiche
+ * Quand on clique sur le bouton "Editer le lieu", une alerte s'affiche.
  */
-const onEditPlaceClick = function(){
+const onEditPlaceClick = function () {
   showAlert.value = true
 }
 
 /**
  * Quand on ferme la boite de dialogue
  */
-const closeDialog = function(){
+const closeDialog = function () {
   showAlert.value = false
 }
 
 /**
  * Si on décide d'éditer le lieu
  */
-const onEditPlaceConfirm = function(){
+const onEditPlaceConfirm = function () {
   showAlert.value = false
   editPlace.value = true
 }
 
 /**
- * Lorsque l'on choisit un lieu dans la liste déroulante, on mets a jour les champs
+ * Lorsque l'on choisit un lieu dans la liste déroulante, on met à jour les champs
  * input de l'adresse
  * @param entity
  */
-const getPlace = async (entity: Event)=>{
-  if(entity.place){
+const getPlace = async (entity: Event) => {
+  if (entity.place) {
     const placeInstance = await em.fetch(Place, entity.place as number)
     entity.placeName = placeInstance.name
     entity.streetAddress = placeInstance.streetAddress
@@ -281,17 +377,19 @@ const getPlace = async (entity: Event)=>{
     entity.latitude = placeInstance.latitude
     entity.longitude = placeInstance.longitude
     editPlace.value = false
-  }else{
-    //Dans le cas où l'on ne récupère aucune place on remet a null le formulaire de l'adresse
+  } else {
+    // Dans le cas où l'on ne récupère aucune place on remet à null le formulaire de l'adresse
     resetPlace(entity)
   }
+
+  emit('update:entity', entity)
 }
 
 /**
  * fonction permettant de remettre à vide tous les champs input de l'adresse
  * @param entity
  */
-const resetPlace = (entity: Event)=>{
+const resetPlace = (entity: Event) => {
   entity.placeName = null
   entity.streetAddress = null
   entity.streetAddressSecond = null
@@ -306,7 +404,7 @@ const resetPlace = (entity: Event)=>{
 // Nettoyer les données lors du démontage du composant
 onBeforeUnmount(() => {
   // Nettoyer les références du store si nécessaire
-  if (process.client) {
+  if (import.meta.client) {
     clearNuxtData('/^' + Place.entity + '_/')
     useRepo(Place).flush()
   }
@@ -314,8 +412,8 @@ onBeforeUnmount(() => {
 </script>
 
 <style scoped lang="scss">
-  .label{
-    font-size: 16px;
-    color: rgb(var(--v-theme-on-primary-alt));
-  }
+.label {
+  font-size: 16px;
+  color: rgb(var(--v-theme-on-primary-alt));
+}
 </style>

+ 13 - 5
components/Form/Parameter/AttendanceBookingReason.vue

@@ -3,7 +3,7 @@
     <v-row>
       <v-col cols="12" sm="6">
         <UiInputText
-          v-model="entity.reason"
+          v-model="proxyEntity.reason"
           field="reason"
           type="string"
           :rules="getAsserts('reason')"
@@ -14,13 +14,21 @@
 </template>
 
 <script setup lang="ts">
-import AttendanceBookingReason from "~/models/Booking/AttendanceBookingReason";
-import {getAssertUtils} from "~/services/asserts/getAssertUtils";
+import AttendanceBookingReason from '~/models/Booking/AttendanceBookingReason'
+import { getAssertUtils } from '~/services/asserts/getAssertUtils'
 
-defineProps<{
+const props = defineProps<{
   entity: AttendanceBookingReason
 }>()
 
-const getAsserts = (key) => getAssertUtils(AttendanceBookingReason.getAsserts(), key)
+const emit = defineEmits(['update:entity'])
 
+// Pour éviter l'erreur eslint "Unexpected mutation of "modelValue" prop"
+const proxyEntity = computed({
+  get: () => props.entity,
+  set: (value) => emit('update:entity', value),
+})
+
+const getAsserts = (key) =>
+  getAssertUtils(AttendanceBookingReason.getAsserts(), key)
 </script>

+ 12 - 5
components/Form/Parameter/EducationTiming.vue

@@ -3,7 +3,7 @@
     <v-row>
       <v-col cols="12" sm="6">
         <UiInputNumber
-          v-model="entity.timing"
+          v-model="proxyEntity.timing"
           field="educationTiming"
           :rules="getAsserts('timing')"
         />
@@ -13,13 +13,20 @@
 </template>
 
 <script setup lang="ts">
-import {getAssertUtils} from "~/services/asserts/getAssertUtils";
-import EducationTiming from "~/models/Education/EducationTiming";
+import { getAssertUtils } from '~/services/asserts/getAssertUtils'
+import EducationTiming from '~/models/Education/EducationTiming'
 
-defineProps<{
+const props = defineProps<{
   entity: EducationTiming
 }>()
 
-const getAsserts = (key) => getAssertUtils(EducationTiming.getAsserts(), key)
+const emit = defineEmits(['update:entity'])
+
+// Pour éviter l'erreur eslint "Unexpected mutation of "modelValue" prop"
+const proxyEntity = computed({
+  get: () => props.entity,
+  set: (value) => emit('update:entity', value),
+})
 
+const getAsserts = (key) => getAssertUtils(EducationTiming.getAsserts(), key)
 </script>

+ 12 - 5
components/Form/Parameter/ResidenceArea.vue

@@ -3,7 +3,7 @@
     <v-row>
       <v-col cols="12" sm="6">
         <UiInputText
-          v-model="entity.label"
+          v-model="proxyEntity.label"
           field="label"
           type="string"
           :rules="getAsserts('label')"
@@ -14,13 +14,20 @@
 </template>
 
 <script setup lang="ts">
-import ResidenceArea from "~/models/Billing/ResidenceArea";
-import {getAssertUtils} from "~/services/asserts/getAssertUtils";
+import ResidenceArea from '~/models/Billing/ResidenceArea'
+import { getAssertUtils } from '~/services/asserts/getAssertUtils'
 
-defineProps<{
+const props = defineProps<{
   entity: ResidenceArea
 }>()
 
-const getAsserts = (key) => getAssertUtils(ResidenceArea.getAsserts(), key)
+const emit = defineEmits(['update:entity'])
+
+// Pour éviter l'erreur eslint "Unexpected mutation of "modelValue" prop"
+const proxyEntity = computed({
+  get: () => props.entity,
+  set: (value) => emit('update:entity', value),
+})
 
+const getAsserts = (key) => getAssertUtils(ResidenceArea.getAsserts(), key)
 </script>

+ 6 - 2
components/Layout/Alert/Content.vue

@@ -15,7 +15,7 @@
   >
     <ul v-if="props.alert.messages.length > 1">
       <li v-for="message in props.alert.messages" :key="message">
-        {{ $t(message) }}
+        - {{ $t(message) }}
       </li>
     </ul>
     <span v-else>
@@ -84,4 +84,8 @@ onUnmounted(() => {
 })
 </script>
 
-<style scoped></style>
+<style scoped>
+.message-alert {
+  list-style: disc;
+}
+</style>

+ 1 - 1
components/Layout/AlertBar/Cotisation.vue

@@ -22,7 +22,7 @@ import UrlUtils from '~/services/utils/urlUtils'
 import type { ALERT_STATE_COTISATION } from '~/types/enum/enums'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import Cotisation from '~/models/Organization/Cotisation'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 const organizationProfile = useOrganizationProfileStore()
 

+ 1 - 1
components/Layout/AlertBar/OnlineRegistration.vue

@@ -17,7 +17,7 @@ import type { ComputedRef } from 'vue'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import { useAccessProfileStore } from '~/stores/accessProfile'
 import RegistrationAvailability from '~/models/OnlineRegistration/RegistrationAvailability'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 const { fetch } = useEntityFetch()
 

+ 1 - 1
components/Layout/AlertBar/RegistrationStatus.vue

@@ -17,7 +17,7 @@ import type { ComputedRef } from 'vue'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import { useAccessProfileStore } from '~/stores/accessProfile'
 import RegistrationStatus from '~/models/OnlineRegistration/RegistrationStatus'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 const { fetch } = useEntityFetch()
 

+ 3 - 3
components/Layout/Header.vue

@@ -14,7 +14,7 @@ Contient entre autres le nom de l'organisation, l'accès à l'aide et aux préf
     </template>
 
     <v-toolbar-title v-if="smAndUp">
-      <LayoutHeaderTitle>
+      <LayoutHeaderTitle :has-lateral-menu>
         {{ title }}
       </LayoutHeaderTitle>
     </v-toolbar-title>
@@ -50,7 +50,7 @@ Contient entre autres le nom de l'organisation, l'accès à l'aide et aux préf
 
     <LayoutHeaderMenu name="MyFamily" :translate-label="false" />
 
-    <LayoutHeaderNotification />
+    <LayoutHeaderNotification v-if="layoutStore.name !== 'freemium'" />
 
     <LayoutHeaderMenu name="Configuration" />
 
@@ -96,7 +96,7 @@ const hasLateralMenu = computed(() => {
   return (
     layoutStore.name !== 'freemium' &&
     ((layoutStore.name !== 'parameters' && hasMenu('Main')) ||
-    (layoutStore.name === 'parameters' && hasMenu('Parameters')))
+      (layoutStore.name === 'parameters' && hasMenu('Parameters')))
   )
 })
 

+ 6 - 6
components/Layout/Header/Menu.vue

@@ -10,13 +10,12 @@ header principal (configuration, paramètres du compte...)
         v-if="menu.icon.avatarId || menu.icon.avatarByDefault"
         size="30"
       >
-        <!-- remplacera l'UiImage en dessous lorsque la gestion des images avec liip sera OK
-         <UiImage
-          :imageId="menu.icon.avatarId"
-          :defaultImage="menu.icon.avatarByDefault"
+        <UiImage
+          :image-id="menu.icon.avatarId"
+          :size="IMAGE_SIZE.SM"
+          :default-image="menu.icon.avatarByDefault"
           :width="30"
-        /> -->
-        <UiImage :default-image="menu.icon.avatarByDefault" :width="30" />
+        />
       </v-avatar>
 
       <v-icon v-else :icon="menu.icon.name" class="on-primary" />
@@ -94,6 +93,7 @@ header principal (configuration, paramètres du compte...)
 <script setup lang="ts">
 import { computed, ref } from 'vue'
 import { useMenu } from '~/composables/layout/useMenu'
+import {IMAGE_SIZE} from "~/types/enum/enums";
 
 const props = defineProps({
   name: {

+ 2 - 2
components/Layout/Header/Notification.vue

@@ -54,7 +54,7 @@
           <span v-intersect="onLastNotificationIntersect" />
 
           <v-row
-            v-if="status==FETCHING_STATUS.PENDING"
+            v-if="status == FETCHING_STATUS.PENDING"
             class="fill-height mt-3 mb-3"
             align="center"
             justify="center"
@@ -96,7 +96,7 @@ import UrlUtils from '~/services/utils/urlUtils'
 import NotificationRepository from '~/stores/repositories/NotificationRepository'
 import Query from '~/services/data/Query'
 import PageFilter from '~/services/data/Filters/PageFilter'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 const accessProfileStore = useAccessProfileStore()
 

+ 9 - 1
components/Layout/Header/Title.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="d-flex flex-row">
+  <div class="d-flex flex-row" :class="!hasLateralMenu ? 'pl-4' : ''">
     <a
       :href="homeUrl"
       :title="$t('go_back_home')"
@@ -15,6 +15,14 @@
 import { useDisplay } from 'vuetify'
 import { useHomeUrl } from '~/composables/utils/useHomeUrl'
 
+const props = defineProps({
+  hasLateralMenu: {
+    type: Boolean,
+    required: false,
+    default: true,
+  },
+})
+
 const { homeUrl } = useHomeUrl()
 const { mdAndUp } = useDisplay()
 </script>

+ 8 - 3
components/Layout/MobytStatus.vue

@@ -1,7 +1,13 @@
 <template>
   <v-col cols="12" lg="12">
     <strong>{{ $t('remaining_sms_credit') }}</strong> -
-    <span v-if="mobytPendingStatus == FETCHING_STATUS.SUCCESS && mobytStatus !== null && mobytStatus.active">
+    <span
+      v-if="
+        mobytPendingStatus == FETCHING_STATUS.SUCCESS &&
+        mobytStatus !== null &&
+        mobytStatus.active
+      "
+    >
       {{
         mobytStatus.money.toLocaleString($i18n.locale, {
           style: 'currency',
@@ -22,8 +28,7 @@ import type { AsyncData } from '#app'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 import MobytUserStatus from '~/models/Organization/MobytUserStatus'
-import {FETCHING_STATUS} from "~/types/enum/data";
-
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 const { fetch } = useEntityFetch()
 const i18n = useI18n()

+ 3 - 4
components/Layout/Parameters/EntityTable.vue

@@ -3,7 +3,7 @@ A data table for the parameters page
 -->
 <template>
   <div class="container">
-    <UiLoadingPanel v-if="status===FETCHING_STATUS.PENDING" />
+    <UiLoadingPanel v-if="status === FETCHING_STATUS.PENDING" />
     <div v-else>
       <LayoutParametersTable
         :items="items"
@@ -34,7 +34,7 @@ import type { AssociativeArray } from '~/types/data'
 import type { ColumnDefinition } from '~/types/interfaces'
 import { useDeleteItem } from '~/composables/form/useDeleteItem'
 import { useEntityManager } from '~/composables/data/useEntityManager'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 const props = defineProps({
   /**
@@ -199,12 +199,11 @@ const goToCreatePage = () => {
 // Nettoyer les données lors du démontage du composant
 onBeforeUnmount(() => {
   // Nettoyer les références du store si nécessaire
-  if (process.client) {
+  if (import.meta.client) {
     clearNuxtData('/^' + props.model.entity + '_many_/')
     useRepo(props.model).flush()
   }
 })
-
 </script>
 
 <style scoped lang="scss"></style>

+ 1 - 1
components/Layout/Parameters/Table.vue

@@ -19,7 +19,7 @@ A data table for the parameters page
         </tr>
       </thead>
       <tbody v-if="items && items.length > 0">
-        <tr v-for="(item, i) in items" :key="item.id">
+        <tr v-for="item in items" :key="item.id">
           <td
             v-for="(col, index) in columnsDefinitions"
             :key="index"

+ 8 - 8
components/Layout/SubHeader/Breadcrumbs.vue

@@ -12,16 +12,19 @@ import UrlUtils from '~/services/utils/urlUtils'
 const runtimeConfig = useRuntimeConfig()
 const i18n = useI18n()
 const router = useRouter()
+const organizationProfile = useOrganizationProfileStore()
 
 const items: ComputedRef<Array<AnyJson>> = computed(() => {
   const crumbs: Array<AnyJson> = []
   const baseUrl =
     runtimeConfig.baseUrlAdminLegacy ?? runtimeConfig.public.baseUrlAdminLegacy
 
-  crumbs.push({
-    title: i18n.t('welcome'),
-    href: UrlUtils.join(baseUrl, '#', 'dashboard'),
-  })
+  if (!organizationProfile.isFreemiumProduct) {
+    crumbs.push({
+      title: i18n.t('welcome'),
+      href: UrlUtils.join(baseUrl, '#', 'dashboard'),
+    })
+  }
 
   const pathPart: Array<string> = UrlUtils.split(router.currentRoute.value.path)
 
@@ -29,13 +32,10 @@ const items: ComputedRef<Array<AnyJson>> = computed(() => {
 
   pathPart.forEach((part) => {
     path = UrlUtils.join(path, part)
-
     const match = router.resolve(path)
     if (match.name) {
       crumbs.push({
-        title: !parseInt(part, 10)
-          ? i18n.t(part + '_breadcrumbs')
-          : i18n.t('item'),
+        title: i18n.t(match.name + '_breadcrumbs'),
         exact: true,
         to: path,
       })

+ 2 - 2
components/Layout/SubHeader/PersonnalizedList.vue

@@ -26,7 +26,7 @@
           <v-text-field
             v-model="search"
             :label="$t('searchList')"
-            :loading="status==FETCHING_STATUS.PENDING"
+            :loading="status == FETCHING_STATUS.PENDING"
             density="compact"
             clear-icon="header-personalized"
           />
@@ -58,7 +58,7 @@ import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import type { AnyJson } from '~/types/data'
 import type ApiResource from '~/models/ApiResource'
 import UrlUtils from '~/services/utils/urlUtils'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 const btn: Ref = ref(null)
 

+ 4 - 1
components/Layout/Subheader.vue

@@ -17,7 +17,10 @@ Contient entre autres le breadcrumb, les commandes de changement d'année et les
 
       <v-card
         v-if="!organizationProfile.isFreemiumProduct"
-        class="d-flex flex-row align-center mr-6" :flat="true" tile>
+        class="d-flex flex-row align-center mr-6"
+        :flat="true"
+        tile
+      >
         <LayoutSubHeaderActivityYear
           v-if="smAndUp && !showDateTimeRange"
           class="activity-year"

+ 7 - 8
components/Ui/Button/Delete.vue

@@ -4,9 +4,9 @@ Bouton Delete avec modale de confirmation de la suppression
 
 <template>
   <main>
-    <div @click="onDeleteClicked()" class="div_to_clicked">
+    <div class="div_to_clicked" @click="onDeleteClicked()">
       <!-- Content -->
-      <slot/>
+      <slot />
     </div>
 
     <UiFormDeletionConfirmationDialog
@@ -18,10 +18,10 @@ Bouton Delete avec modale de confirmation de la suppression
 </template>
 
 <script setup lang="ts" generic="T extends ApiResource">
-import {type Ref, ref} from 'vue'
+import { type Ref, ref } from 'vue'
 import type ApiResource from '~/models/ApiResource'
 import { useDeleteItem } from '~/composables/form/useDeleteItem'
-import {useEntityManager} from "~/composables/data/useEntityManager";
+import { useEntityManager } from '~/composables/data/useEntityManager'
 
 const props = defineProps<{
   entity: T
@@ -34,7 +34,7 @@ const { em } = useEntityManager()
 const showDeletionConfirmationDialog: Ref<boolean> = ref(false)
 const itemToDelete: Ref<ApiResource | null> = ref(null)
 
-const onDeleteClicked =  () => {
+const onDeleteClicked = () => {
   itemToDelete.value = em.cast(em.getModel(props.entity), props.entity)
   showDeletionConfirmationDialog.value = true
 }
@@ -44,17 +44,16 @@ const onCancelClicked = () => {
 }
 
 const onDeleteConfirmed = async () => {
-  if(itemToDelete.value != null){
+  if (itemToDelete.value != null) {
     pageStore.loading = true
     await deleteItem(itemToDelete.value)
     pageStore.loading = false
   }
 }
-
 </script>
 
 <style scoped>
-.div_to_clicked{
+.div_to_clicked {
   cursor: pointer;
 }
 </style>

+ 25 - 23
components/Ui/DatePicker.vue

@@ -5,28 +5,28 @@ Sélecteur de dates
 -->
 
 <template>
-    <VueDatePicker
-      :model-value="modelValue"
-      :locale="locale"
-      :format="dateFormatPattern"
-      :format-locale="fnsLocale"
-      :range="range"
-      :multi-calendars="range"
-      :enable-time-picker="withTimePicker"
-      :auto-apply="autoApply"
-      :auto-position="true"
-      :start-date="today"
-      close-on-scroll
-      text-input
-      :readonly="readonly"
-      position="left"
-      :teleport="true"
-      :select-text="$t('select')"
-      :cancel-text="$t('cancel')"
-      input-class-name="date-picker-input"
-      @update:model-value="emit('update:modelValue', $event)"
-    />
-  </template>
+  <VueDatePicker
+    :model-value="modelValue"
+    :locale="locale"
+    :format="dateFormatPattern"
+    :format-locale="fnsLocale"
+    :range="range"
+    :multi-calendars="range"
+    :enable-time-picker="withTimePicker"
+    :auto-apply="autoApply"
+    :auto-position="true"
+    :start-date="today"
+    close-on-scroll
+    text-input
+    :readonly="readonly"
+    position="left"
+    :teleport="true"
+    :select-text="$t('select')"
+    :cancel-text="$t('cancel')"
+    input-class-name="date-picker-input"
+    @update:model-value="emit('update:modelValue', $event)"
+  />
+</template>
 
 <script setup lang="ts">
 import { useI18n } from 'vue-i18n'
@@ -70,7 +70,9 @@ const fnsLocale: ComputedRef<Locale> = computed(() =>
 )
 
 const dateFormatPattern: ComputedRef<string> = computed(() =>
-  props.withTimePicker ? DateUtils.getFormatPattern(locale.value as supportedLocales) : DateUtils.getShortFormatPattern(locale.value as supportedLocales),
+  props.withTimePicker
+    ? DateUtils.getFormatPattern(locale.value as supportedLocales)
+    : DateUtils.getShortFormatPattern(locale.value as supportedLocales),
 )
 
 const today = new Date()

+ 76 - 30
components/Ui/EventList.vue

@@ -1,52 +1,84 @@
 <template>
   <div>
-    <v-list  height="380">
-
-      <v-list-item
-        class="event-item"
-        v-for="event in events"
-        :key="event.id"
-        :subtitle="date.format(event.datetimeStart, 'fullDateWithWeekday')"
-        :title="event.name"
-      >
-        <template v-slot:prepend>
+    <v-list height="420">
+      <v-list-item v-for="event in events" :key="event.id" class="event-item">
+        <template #prepend>
           <v-avatar color="grey-lighten-1">
-            <UiImage :imageId="event.image" :width="50" />
+            <UiImage
+              :image-id="event.image"
+              :width="50"
+              :size="IMAGE_SIZE.SM"
+            />
           </v-avatar>
         </template>
 
-        <template v-slot:append>
-          <v-avatar  @click="$emit('edit', event.id)">
+        <template #default>
+          <div class="flex-grow-1" @click="$emit('edit', event.id)">
+            <span class="text-subtitle-1">{{ event.name }}</span>
+            <br />
+            <span class="text-subtitle-2">{{
+              date.format(event.datetimeStart, 'fullDateWithWeekday')
+            }}</span>
+          </div>
+        </template>
+
+        <template #append>
+          <v-avatar
+            class="edit-btn action-btn"
+            @click="$emit('edit', event.id)"
+          >
             <v-icon>fas fa-pencil</v-icon>
           </v-avatar>
 
-          <UiButtonDelete :entity="event">
+          <UiButtonDelete :entity="event" class="delete-btn action-btn">
             <v-avatar>
               <v-icon>fas fa-trash</v-icon>
             </v-avatar>
           </UiButtonDelete>
         </template>
       </v-list-item>
-
     </v-list>
 
-    <div class="d-flex justify-space-between px-4 py-2 pagination mb-5" v-if="events.length > 0">
-      <v-btn variant="flat" class="pagination-btn" @click="$emit('load', pagination.first)" :disabled="!pagination.first || !pagination.previous">
+    <div
+      v-if="events.length > 0"
+      class="d-flex justify-space-between px-4 py-2 pagination mb-5"
+    >
+      <v-btn
+        variant="flat"
+        class="pagination-btn"
+        :disabled="!pagination.first || !pagination.previous"
+        @click="$emit('load', pagination.first)"
+      >
         <v-avatar>
-        <v-icon>fas fa-angles-left</v-icon>
-      </v-avatar>
+          <v-icon>fas fa-angles-left</v-icon>
+        </v-avatar>
       </v-btn>
-      <v-btn variant="flat" class="pagination-btn" @click="$emit('load', pagination.previous)" :disabled="!pagination.previous">
+      <v-btn
+        variant="flat"
+        class="pagination-btn"
+        :disabled="!pagination.previous"
+        @click="$emit('load', pagination.previous)"
+      >
         <v-avatar>
           <v-icon>fas fa-angle-left</v-icon>
         </v-avatar>
       </v-btn>
-      <v-btn variant="flat" class="pagination-btn" @click="$emit('load', pagination.next)" :disabled="!pagination.next">
+      <v-btn
+        variant="flat"
+        class="pagination-btn"
+        :disabled="!pagination.next"
+        @click="$emit('load', pagination.next)"
+      >
         <v-avatar>
           <v-icon>fas fa-angle-right</v-icon>
         </v-avatar>
       </v-btn>
-      <v-btn variant="flat" class="pagination-btn" @click="$emit('load', pagination.last)" :disabled="pagination.last == 1">
+      <v-btn
+        variant="flat"
+        class="pagination-btn"
+        :disabled="pagination.last == 1"
+        @click="$emit('load', pagination.last)"
+      >
         <v-avatar>
           <v-icon>fas fa-angles-right</v-icon>
         </v-avatar>
@@ -57,13 +89,14 @@
 
 <script lang="ts" setup>
 import { defineProps, defineEmits } from 'vue'
-import type {Collection} from "pinia-orm";
-import Event from "~/models/Freemium/Event";
-import type {Pagination} from "~/types/data";
+import type { Collection } from 'pinia-orm'
+import type Event from '~/models/Freemium/Event'
+import type { Pagination } from '~/types/data'
 import { useDate } from 'vuetify'
+import {IMAGE_SIZE} from "~/types/enum/enums";
 
 const props = defineProps<{
-  events: Collection<Event>,
+  events: Collection<Event>
   pagination: Pagination
 }>()
 
@@ -71,14 +104,27 @@ const emits = defineEmits(['edit', 'load'])
 const date = useDate()
 </script>
 
-<style scoped>
+<style scoped lang="scss">
 .event-item {
   margin-top: 20px;
-  color: rgb(var(--v-theme-on-primary-alt));
+  color: rgb(var(--v-theme-on-neutral));
+  .text-subtitle-2 {
+    text-transform: capitalize !important;
+    color: rgb(var(--v-theme-on-neutral--sub));
+  }
 }
-
 .pagination-btn[disabled] {
   opacity: 0.3;
 }
-
+.edit-btn {
+  cursor: pointer;
+}
+.action-btn:hover {
+  background-color: rgb(var(--v-theme-on-neutral-soft--sub));
+  border-radius: 20px;
+}
+:deep(.v-list-item):hover {
+  cursor: pointer;
+  background-color: rgb(var(--v-theme-neutral-soft));
+}
 </style>

+ 13 - 10
components/Ui/Form.vue

@@ -60,7 +60,11 @@ de quitter si des données ont été modifiées.
     </v-form>
 
     <!-- Confirmation dialog -->
-    <LazyLayoutDialog :show="isConfirmationDialogShowing" :max-width="1000">
+    <LazyLayoutDialog
+      :show="isConfirmationDialogShowing"
+      :max-width="1000"
+      theme="danger"
+    >
       <template #dialogText>
         <v-card-title class="text-h5 theme-neutral">
           {{ $t('caution') }}
@@ -102,9 +106,9 @@ import type ApiModel from '~/models/ApiModel'
 import { usePageStore } from '~/stores/page'
 import type { AnyJson } from '~/types/data'
 import { useRefreshProfile } from '~/composables/data/useRefreshProfile'
-import Organization from "~/models/Freemium/Organization";
-import Event from "~/models/Freemium/Event";
-import Country from "~/models/Core/Country";
+import Organization from '~/models/Freemium/Organization'
+import type Event from '~/models/Freemium/Event'
+import Country from '~/models/Core/Country'
 
 const props = defineProps({
   /**
@@ -162,7 +166,6 @@ const props = defineProps({
   },
 })
 
-
 // ### Définitions
 const i18n = useI18n()
 const router = useRouter()
@@ -253,19 +256,19 @@ const submit = async (next: string | null = null) => {
     const err = error as {
       response?: {
         status: number
-        data: { violations?: Array<{ message: string; propertyPath: string }> }
+        _data: { violations?: Array<{ message: string; propertyPath: string }> }
       }
     }
     if (
       err.response &&
       err.response.status === 422 &&
-      err.response.data.violations
+      err.response._data.violations
     ) {
       // TODO: à revoir
       const violations: Array<string> = []
       let fields: AnyJson = {}
 
-      for (const violation of err.response.data.violations) {
+      for (const violation of err.response._data.violations) {
         violations.push(i18n.t(violation.message) as string)
         fields = Object.assign(fields, {
           [violation.propertyPath]: violation.message,
@@ -333,14 +336,14 @@ onBeforeRouteLeave(
 )
 
 onMounted(() => {
-  window.addEventListener('beforeunload', quitWithoutSaving )
+  window.addEventListener('beforeunload', quitWithoutSaving)
 })
 
 onBeforeUnmount(() => {
   window.removeEventListener('beforeunload', quitWithoutSaving)
 })
 
-function quitWithoutSaving(event: any){
+function quitWithoutSaving(event) {
   if (formStore.dirty === true) {
     event.returnValue = i18n.t('quit_without_saving_warning')
   }

+ 23 - 21
components/Ui/Form/Creation.vue

@@ -11,32 +11,34 @@
 </template>
 
 <script setup lang="ts" generic="T extends typeof ApiModel">
-
 import type { RouteLocationRaw } from 'vue-router'
 import type ApiModel from '~/models/ApiModel'
 import type { AnyJson } from '~/types/data'
 import { SUBMIT_TYPE } from '~/types/enum/enums'
 import { useEntityManager } from '~/composables/data/useEntityManager'
 
-const props = withDefaults(defineProps<{
-  model: T
-  /**
-   * Route de retour
-   */
-  goBackRoute?: RouteLocationRaw | null
-  /**
-   * La validation est en cours
-   */
-  validationPending?: boolean
-  /**
-   * Faut-il rafraîchir le profil à la soumission du formulaire ?
-   */
-  refreshProfile?: boolean
-}>(), {
-  goBackRoute: null,
-  validationPending: false,
-  refreshProfile: false
-})
+const props = withDefaults(
+  defineProps<{
+    model: T
+    /**
+     * Route de retour
+     */
+    goBackRoute?: RouteLocationRaw | null
+    /**
+     * La validation est en cours
+     */
+    validationPending?: boolean
+    /**
+     * Faut-il rafraîchir le profil à la soumission du formulaire ?
+     */
+    refreshProfile?: boolean
+  }>(),
+  {
+    goBackRoute: null,
+    validationPending: false,
+    refreshProfile: false,
+  },
+)
 
 const router = useRouter()
 const { em } = useEntityManager()
@@ -65,7 +67,7 @@ const quit = () => {
 // Nettoyer les données lors du démontage du composant
 onBeforeUnmount(() => {
   // Nettoyer les références du store si nécessaire
-  if (process.client) {
+  if (import.meta.client) {
     useRepo(props.model).flush()
   }
 })

+ 31 - 28
components/Ui/Form/Edition.vue

@@ -1,6 +1,6 @@
 <template>
   <LayoutContainer>
-    <UiLoadingPanel v-if="status==FETCHING_STATUS.PENDING" />
+    <UiLoadingPanel v-if="status == FETCHING_STATUS.PENDING" />
     <UiForm v-else v-model="entity" :submit-actions="submitActions">
       <template #form.button>
         <v-btn v-if="goBackRoute" class="theme-neutral mr-3" @click="quit">
@@ -20,33 +20,36 @@ import type ApiModel from '~/models/ApiModel'
 import type { AnyJson } from '~/types/data'
 import { SUBMIT_TYPE } from '~/types/enum/enums'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
-const props = withDefaults(defineProps<{
-  model: T
-  /**
-   * Id de l'objet
-   * Si non renseigné, le composant essaiera de l'extraire de la route actuelle
-   */
-  id?: number | null
-  /**
-   * Route de retour
-   */
-  goBackRoute?: RouteLocationRaw | null
-  /**
-   * La validation est en cours
-   */
-  validationPending?: boolean
-  /**
-   * Faut-il rafraîchir le profil à la soumission du formulaire ?
-   */
-  refreshProfile?: boolean
-}>(), {
-  id: null,
-  goBackRoute: null,
-  validationPending: false,
-  refreshProfile: false
-})
+const props = withDefaults(
+  defineProps<{
+    model: T
+    /**
+     * Id de l'objet
+     * Si non renseigné, le composant essaiera de l'extraire de la route actuelle
+     */
+    id?: number | null
+    /**
+     * Route de retour
+     */
+    goBackRoute?: RouteLocationRaw | null
+    /**
+     * La validation est en cours
+     */
+    validationPending?: boolean
+    /**
+     * Faut-il rafraîchir le profil à la soumission du formulaire ?
+     */
+    refreshProfile?: boolean
+  }>(),
+  {
+    id: null,
+    goBackRoute: null,
+    validationPending: false,
+    refreshProfile: false,
+  },
+)
 
 const { fetch } = useEntityFetch()
 const route = useRoute()
@@ -79,7 +82,7 @@ const quit = () => {
 // Nettoyer les données lors du démontage du composant
 onBeforeUnmount(() => {
   // Nettoyer les références du store si nécessaire
-  if (process.client) {
+  if (import.meta.client) {
     clearNuxtData('/^' + props.model.entity + '_' + props.id + '_/')
     useRepo(props.model).flush()
     // Forcer le garbage collection des objets Parameters

+ 11 - 3
components/Ui/Input/Autocomplete/Accesses.vue

@@ -36,7 +36,11 @@ import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import Access from '~/models/Access/Access'
 import Query from '~/services/data/Query'
 import OrderBy from '~/services/data/Filters/OrderBy'
-import {FETCHING_STATUS, ORDER_BY_DIRECTION, SEARCH_STRATEGY} from '~/types/enum/data'
+import {
+  FETCHING_STATUS,
+  ORDER_BY_DIRECTION,
+  SEARCH_STRATEGY,
+} from '~/types/enum/data'
 import PageFilter from '~/services/data/Filters/PageFilter'
 import InArrayFilter from '~/services/data/Filters/InArrayFilter'
 import SearchFilter from '~/services/data/Filters/SearchFilter'
@@ -199,7 +203,11 @@ const {
   refresh: refreshSearch,
 } = fetchCollection(UserSearchItem, null, querySearch)
 
-const pending = computed(() => statusSearch.value == FETCHING_STATUS.PENDING || statusActive.value == FETCHING_STATUS.PENDING)
+const pending = computed(
+  () =>
+    statusSearch.value == FETCHING_STATUS.PENDING ||
+    statusActive.value == FETCHING_STATUS.PENDING,
+)
 
 /**
  * Contenu de la liste autocomplete
@@ -256,7 +264,7 @@ const onUpdateModelValue = (event: Array<number>) => {
 // Nettoyer les données lors du démontage du composant
 onBeforeUnmount(() => {
   // Nettoyer les références du store si nécessaire
-  if (process.client) {
+  if (import.meta.client) {
     clearNuxtData('/^' + UserSearchItem + '_many_/')
     useRepo(UserSearchItem).flush()
   }

+ 71 - 23
components/Ui/Input/Autocomplete/ApiResources.vue

@@ -31,6 +31,7 @@ Exemple : on souhaite lister les lieux d'une structure
       :variant="variant"
       :readonly="readonly"
       :clearable="true"
+      :rules="rules"
       @update:model-value="onUpdateModelValue"
       @update:search="onUpdateSearch"
     />
@@ -38,18 +39,24 @@ Exemple : on souhaite lister les lieux d'une structure
 </template>
 
 <script setup lang="ts">
-import type {PropType, ComputedRef, Ref, WatchStopHandle} from 'vue'
+import type { PropType, ComputedRef, Ref, WatchStopHandle } from 'vue'
 import { computed } from 'vue'
 import * as _ from 'lodash-es'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import Query from '~/services/data/Query'
 import OrderBy from '~/services/data/Filters/OrderBy'
-import {FETCHING_STATUS, ORDER_BY_DIRECTION, SEARCH_STRATEGY} from '~/types/enum/data'
+import {
+  FETCHING_STATUS,
+  ORDER_BY_DIRECTION,
+  SEARCH_STRATEGY,
+} from '~/types/enum/data'
 import PageFilter from '~/services/data/Filters/PageFilter'
 import InArrayFilter from '~/services/data/Filters/InArrayFilter'
 import SearchFilter from '~/services/data/Filters/SearchFilter'
-import type ApiModel from "~/models/ApiModel";
-import PlaceSearchItem from "~/models/Custom/Search/PlaceSearchItem";
+import type ApiModel from '~/models/ApiModel'
+import PlaceSearchItem from '~/models/Custom/Search/PlaceSearchItem'
+import type { ApiFilter } from '~/types/data'
+import EqualFilter from '~/services/data/Filters/EqualFilter'
 
 const props = defineProps({
   /**
@@ -70,9 +77,25 @@ const props = defineProps({
   /**
    * Filtres à transmettre à la source de données
    */
-  query: {
-    type: Object as PropType<typeof Query>,
+  apiFilters: {
+    type: Array<ApiFilter>,
     required: false,
+    default: [],
+  },
+  /**
+   * Nombre de résultat
+   */
+  resultNumber: {
+    type: Number,
+    required: false,
+    default: 20,
+  } /**
+   * Autorise la pagination
+   * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-multiple
+   */,
+  pagination: {
+    type: Boolean,
+    default: true,
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
@@ -157,6 +180,15 @@ const props = defineProps({
     required: false,
     default: null,
   },
+  /**
+   * Règles de validation
+   * @see https://vuetify.cn/en/components/forms/#validation-with-submit-clear
+   */
+  rules: {
+    type: Array,
+    required: false,
+    default: () => [],
+  },
 })
 
 /**
@@ -180,18 +212,22 @@ const activeIds = computed(() => {
   return []
 })
 
-
 /**
  * Query transmise à l'API lors de l'initialisation afin de récupérer les items actifs
  */
 const queryActive = computed(() => {
-  return new Query(
-      new OrderBy(props.listLabel, ORDER_BY_DIRECTION.ASC),
-      new PageFilter(ref(1), ref(20)),
-      new InArrayFilter(props.listValue, activeIds),
-    )
+  const query = new Query(
+    new OrderBy(props.listLabel, ORDER_BY_DIRECTION.ASC),
+    new InArrayFilter(props.listValue, activeIds),
+  )
+  if (props.pagination) {
+    query.add(new PageFilter(ref(1), ref(props.resultNumber)))
   }
-)
+  for (const index in props.apiFilters) {
+    query.add(props.apiFilters[index])
+  }
+  return query
+})
 
 /**
  * On commence par fetcher les models déjà actifs, pour affichage des labels correspondant
@@ -199,10 +235,10 @@ const queryActive = computed(() => {
 const {
   data: collectionActive,
   status: statusActive,
-  refresh: refreshActive
+  refresh: refreshActive,
 } = fetchCollection(props.model, null, queryActive.value)
 
-const unwatch: WatchStopHandle = watch(activeIds, ()=>{
+const unwatch: WatchStopHandle = watch(activeIds, () => {
   refreshActive()
 })
 
@@ -216,9 +252,15 @@ const searchFilter: Ref<string | null> = ref(null)
  */
 const querySearch = new Query(
   new OrderBy(props.listLabel, ORDER_BY_DIRECTION.ASC),
-  new PageFilter(ref(1), ref(20)),
   new SearchFilter(props.listLabel, searchFilter, SEARCH_STRATEGY.IPARTIAL),
 )
+if (props.pagination) {
+  querySearch.add(new PageFilter(ref(1), ref(props.resultNumber)))
+}
+for (const index in props.apiFilters) {
+  querySearch.add(props.apiFilters[index])
+}
+
 /**
  * On fetch les résultats correspondants à la recherche faite par l'utilisateur
  */
@@ -229,17 +271,21 @@ const {
 } = fetchCollection(props.model, null, querySearch)
 
 //Le pending global dépend des deux recherche (actif, et globale)
-const pending = computed(() => statusSearch.value == FETCHING_STATUS.PENDING || statusActive.value == FETCHING_STATUS.PENDING)
+const pending = computed(
+  () =>
+    statusSearch.value == FETCHING_STATUS.PENDING ||
+    statusActive.value == FETCHING_STATUS.PENDING,
+)
 
 /**
  * Génère un ListItem à partir des props
  * @param searchItem
  */
-const item = (searchItem: any): ListItem => {
+const item = (searchItem): ListItem => {
   return {
     id: searchItem[props.listValue],
     title: searchItem[props.listLabel]
-      ? searchItem[props.listLabel]
+      ? i18n.t(searchItem[props.listLabel])
       : `(${i18n.t('missing_value')})`,
   }
 }
@@ -258,7 +304,9 @@ const items: ComputedRef<Array<ListItem>> = computed(() => {
     .map(item)
     .filter(
       (item) =>
-        !collectionActive.value!.items.find((other) => other[props.listValue] === item[props.listValue]),
+        !collectionActive.value!.items.find(
+          (other) => other[props.listValue] === item[props.listValue],
+        ),
     )
   return activeItems.concat(searchedItems)
 })
@@ -282,12 +330,12 @@ const refreshDebounced: _.DebouncedFunc<() => void> = _.debounce(async () => {
  */
 const onUpdateSearch = (event: string) => {
   let search = true
-  if(searchFilter.value === null){
+  if (searchFilter.value === null) {
     search = false
   }
 
   searchFilter.value = event
-  if(search){
+  if (search) {
     refreshDebounced()
   }
 }
@@ -308,7 +356,7 @@ const onUpdateModelValue = (event: Array<number>) => {
 // Nettoyer les données lors du démontage du composant
 onBeforeUnmount(() => {
   // Nettoyer les références du store si nécessaire
-  if (process.client) {
+  if (import.meta.client) {
     clearNuxtData('/^' + props.model.entity + '_many_/')
     useRepo(props.model).flush()
   }

+ 3 - 3
components/Ui/Input/Autocomplete/Enum.vue

@@ -3,7 +3,7 @@
     :model-value="modelValue"
     :field="field"
     :items="items"
-    :is-loading="status===FETCHING_STATUS.PENDING"
+    :is-loading="status === FETCHING_STATUS.PENDING"
     :return-object="false"
     item-title="label"
     item-value="value"
@@ -17,7 +17,7 @@ import type { ComputedRef, PropType } from 'vue'
 import { useEnumFetch } from '~/composables/data/useEnumFetch'
 import ArrayUtils from '~/services/utils/arrayUtils'
 import type { Enum } from '~/types/data'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 const emit = defineEmits(['update:model-value'])
 
@@ -74,7 +74,7 @@ const items: ComputedRef<Array<Enum>> = computed(() => {
 // Nettoyer les données lors du démontage du composant
 onBeforeUnmount(() => {
   // Nettoyer les références du store si nécessaire
-  if (process.client) {
+  if (import.meta.client) {
     clearNuxtData(props.enumName)
     // Forcer le garbage collection des objets Parameters
     enumItems.value = null

+ 17 - 15
components/Ui/Input/DatePicker.vue

@@ -10,8 +10,8 @@ Sélecteur de dates, à placer dans un composant `UiForm`
       </span>
 
       <v-validation
-        v-model="date"
         v-slot="{ errorMessages }"
+        v-model="date"
         :rules="rules"
         :error="error || !!fieldViolations"
         :error-messages="
@@ -19,18 +19,21 @@ Sélecteur de dates, à placer dans un composant `UiForm`
         "
         :validate-on="'lazy input'"
       >
-          <UiDatePicker
-            v-model="date"
-            :readonly="readonly"
-            :withTimePicker="withTimePicker"
-            class="date-picker"
-            @update:model-value="onUpdate($event)"
-          />
-
-        <div class="v-input__details error_message" v-if="errorMessages.value.length > 0">
+        <UiDatePicker
+          v-model="date"
+          :readonly="readonly"
+          :with-time-picker="withTimePicker"
+          class="date-picker"
+          @update:model-value="onUpdate($event)"
+        />
+
+        <div
+          v-if="errorMessages.value.length > 0"
+          class="v-input__details error_message"
+        >
           <div class="v-messages__message">
-            <span v-for="errorMessage in errorMessages.value">
-                {{ errorMessage }}
+            <span v-for="(msg, i) in errorMessages.value" :key="i">
+              {{ msg }}
             </span>
           </div>
         </div>
@@ -174,11 +177,10 @@ onBeforeUnmount(() => {
   }
 }
 
-.error_message{
+.error_message {
   padding-inline: 16px;
 }
-.v-messages__message{
+.v-messages__message {
   color: rgb(var(--v-theme-error));
-
 }
 </style>

+ 20 - 11
components/Ui/Input/DateTimePicker.vue

@@ -2,7 +2,11 @@
   <v-row>
     <v-col cols="12" md="6">
       <v-text-field
-        :model-value="dateModel ? _useDate.format(dateModel, 'fullDateWithWeekday') : undefined"
+        :model-value="
+          dateModel
+            ? _useDate.format(dateModel, 'fullDateWithWeekday')
+            : undefined
+        "
         :label="$t('choose_day')"
         prepend-icon="fas fa-calendar"
         :rules="rules"
@@ -49,8 +53,8 @@
           <v-time-picker
             :model-value="time"
             format="24hr"
-            @update:model-value="onUpdateTime($event)"
             scrollable
+            @update:model-value="onUpdateTime($event)"
           />
         </v-menu>
       </v-text-field>
@@ -63,7 +67,7 @@ import type { PropType, Ref } from 'vue'
 import { ref } from 'vue'
 import { useFieldViolation } from '~/composables/form/useFieldViolation'
 import { useDate } from 'vuetify'
-import DateUtils from "~/services/utils/dateUtils";
+import DateUtils from '~/services/utils/dateUtils'
 
 const props = defineProps({
   /**
@@ -149,20 +153,26 @@ const showMenuDate = ref(false)
 const _useDate = useDate()
 const { fieldViolations, updateViolationState } = useFieldViolation(props.field)
 
-const dateModel = computed(()=> props.modelValue ? new Date(props.modelValue) : undefined)
-const time = computed(()=>   props.modelValue ? _useDate.format(new Date(props.modelValue),'fullTime24h') : undefined)
+const dateModel = computed(() =>
+  props.modelValue ? new Date(props.modelValue) : undefined,
+)
+const time = computed(() =>
+  props.modelValue
+    ? _useDate.format(new Date(props.modelValue), 'fullTime24h')
+    : undefined,
+)
 const emit = defineEmits(['update:model-value'])
 
 const onUpdateDate = (event: string) => {
   updateViolationState()
-  let date = DateUtils.combineDateAndTime(event, time.value)
-  emit('update:model-value', date.toISOString().replace('.000Z', '+00:00'))
+  const date = DateUtils.combineDateAndTime(event, time.value)
+  emit('update:model-value', DateUtils.toIsoUtcOffset(date))
 }
 
 const onUpdateTime = (event: string) => {
   updateViolationState()
-  let date = DateUtils.combineDateAndTime(dateModel.value, event)
-  emit('update:model-value', date.toISOString().replace('.000Z', '+00:00'))
+  const date = DateUtils.combineDateAndTime(dateModel.value, event)
+  emit('update:model-value', DateUtils.toIsoUtcOffset(date))
 }
 
 onBeforeUnmount(() => {
@@ -170,5 +180,4 @@ onBeforeUnmount(() => {
 })
 </script>
 
-<style scoped lang="scss">
-</style>
+<style scoped lang="scss"></style>

+ 1 - 4
components/Ui/Input/Email.vue

@@ -9,9 +9,7 @@ Champs de saisie de type Text dédié à la saisie d'emails
     :readonly="readonly"
     :rules="rules"
     :error="error || !!fieldViolations"
-    :error-messages="
-        errorMessage || fieldViolations ? $t(fieldViolations) : ''
-      "
+    :error-messages="errorMessage || fieldViolations ? $t(fieldViolations) : ''"
     @update:model-value="onUpdate($event)"
   />
 </template>
@@ -77,7 +75,6 @@ const props = defineProps({
   },
 })
 
-
 const i18n = useI18n()
 
 const fieldLabel = props.label ?? props.field

+ 15 - 0
components/Ui/Input/Image.vue

@@ -5,6 +5,7 @@ Assistant de création d'image
 -->
 <template>
   <div class="input-image">
+    <label v-if="label || field" class="label">{{ $t(label ?? field) }}</label>
     <UiImage
       ref="uiImage"
       :image-id="modelValue"
@@ -132,6 +133,15 @@ const props = defineProps({
     required: false,
     default: null,
   },
+  /**
+   * Label du champ
+   * Si non défini, c'est le nom de propriété qui est utilisé
+   */
+  label: {
+    type: String,
+    required: false,
+    default: null,
+  },
   /**
    * Image par défaut en cas d'absence d'une image uploadée
    */
@@ -458,6 +468,11 @@ onBeforeUnmount(() => {
 </script>
 
 <style scoped lang="scss">
+.label {
+  font-size: 16px;
+  color: rgb(var(--v-theme-on-primary-alt));
+}
+
 :deep(.vue-advanced-cropper__stretcher) {
   height: auto !important;
   width: auto !important;

+ 1 - 2
components/Ui/Input/Number.vue

@@ -23,7 +23,7 @@ An input for numeric values
 
 <script setup lang="ts">
 import type { PropType, Ref } from 'vue'
-import {useFieldViolation} from "~/composables/form/useFieldViolation";
+import { useFieldViolation } from '~/composables/form/useFieldViolation'
 
 type ValidationRule = (value: string | number | null) => boolean | string
 
@@ -31,7 +31,6 @@ const props = defineProps({
   modelValue: {
     type: Number,
     required: true,
-    default: null,
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété

+ 8 - 12
components/Ui/Input/Phone.vue

@@ -7,21 +7,20 @@ Champs de saisie d'un numéro de téléphone
 
 <template>
   <v-phone-input
-    :model-value.number="modelValue"
+    :model-value="modelValue"
     :rules="rules"
     :variant="variant"
     :label="label || field ? $t(label ?? field) : undefined"
-    defaultCountry="FR"
+    default-country="FR"
+    :invalid-message="(n) => $t('invalid_phone_number', { example: n.example })"
     @update:model-value="onUpdate($event)"
-    :invalidMessage="(n) => $t('invalid_phone_number', { example: n.example})"
   />
 </template>
 
 <script setup lang="ts">
-
 type ValidationRule = (value: string | number | null) => boolean | string
 
-const props = defineProps({
+defineProps({
   modelValue: {
     type: String,
     required: true,
@@ -75,15 +74,12 @@ const props = defineProps({
 
 const emit = defineEmits(['update:modelValue'])
 
-const onUpdate = (event: string|null) => {
-  if(event === ''){
+const onUpdate = (event: string | null) => {
+  if (event === '') {
     event = null
   }
-  emit('update:model-value', event)
+  emit('update:modelValue', event)
 }
-
 </script>
 
-<style lang="scss">
-
-</style>
+<style lang="scss"></style>

+ 1 - 1
components/Ui/Input/TextArea.vue

@@ -22,7 +22,7 @@ Champs de saisie de bloc texte
 
 <script setup lang="ts">
 import { useFieldViolation } from '~/composables/form/useFieldViolation'
-import type {PropType} from "vue";
+import type { PropType } from 'vue'
 
 type ValidationRule = (value: string | number | null) => boolean | string
 

+ 105 - 62
components/Ui/Input/TreeSelect.vue

@@ -54,9 +54,7 @@ et sélectionner des éléments organisés en catégories et sous-catégories.
         closable
         @click:close="removeItem(item.raw.value!)"
       >
-        {{
-          selectedItemsMap[item.raw] || selectedItemsMap[item.raw.value]
-        }}
+        {{ selectedItemsMap[item.raw] || selectedItemsMap[item.raw.value] }}
       </v-chip>
       <span
         v-if="
@@ -152,7 +150,7 @@ et sélectionner des éléments organisés en catégories et sous-catégories.
 <script setup lang="ts">
 import StringUtils from '~/services/utils/stringUtils'
 import _ from 'lodash'
-import type {PropType} from "vue";
+import type { PropType } from 'vue'
 
 interface SelectItem {
   id: string
@@ -178,6 +176,11 @@ const props = defineProps({
     required: false,
     default: null,
   },
+  nbLevel: {
+    type: Number,
+    required: false,
+    default: 3,
+  },
   /**
    * Label du champ
    * Si non défini, c'est le nom de propriété qui est utilisé
@@ -203,7 +206,7 @@ const props = defineProps({
     >,
     required: false,
     default: 'outlined',
-  }
+  },
 })
 
 const searchInput = ref()
@@ -230,7 +233,9 @@ const onKeyDown = (event: KeyboardEvent) => {
   // 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)
+    ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(
+      event.key,
+    )
   ) {
     event.stopPropagation()
   }
@@ -250,9 +255,9 @@ const onKeyDown = (event: KeyboardEvent) => {
  * This avoids having to normalize labels during each search operation.
  */
 const normalizedItems = computed(() => {
-  return props.items.map(item => ({
+  return props.items.map((item) => ({
     ...item,
-    normalizedLabel: StringUtils.normalize(item.label)
+    normalizedLabel: StringUtils.normalize(item.label),
   }))
 })
 
@@ -270,20 +275,28 @@ const expandParentsOfSelectedItems = () => {
   expandedSubcategories.value.clear()
 
   for (const selectedId of props.modelValue) {
-    const item = normalizedItems.value.find(i => i.value === selectedId)
+    const item = normalizedItems.value.find((i) => i.value === selectedId)
     if (!item) continue
 
-    // Trouver la sous-catégorie
-    const subcategory = normalizedItems.value.find(i => i.id === item.parentId)
-    if (subcategory) {
-      expandedSubcategories.value.add(subcategory.id)
+    let parentId = null
+    if (props.nbLevel === 3) {
+      // Trouver la sous-catégorie
+      const subcategory = normalizedItems.value.find(
+        (i) => i.id === item.parentId,
+      )
+      if (subcategory) {
+        expandedSubcategories.value.add(subcategory.id)
+        parentId = subcategory.parentId
+      }
+    } else {
+      parentId = item.parentId
+    }
 
+    if (parentId) {
       // Trouver la catégorie
-      if (subcategory.parentId) {
-        const category = normalizedItems.value.find(i => i.id === subcategory.parentId)
-        if (category) {
-          expandedCategories.value.add(category.id)
-        }
+      const category = normalizedItems.value.find((i) => i.id === parentId)
+      if (category) {
+        expandedCategories.value.add(category.id)
       }
     }
   }
@@ -383,21 +396,33 @@ const onSearchInput = () => {
     // Trouver tous les éléments qui correspondent à la recherche
     const matchingItems = normalizedItems.value.filter(
       (item) =>
-        item.type === 'item' && item.level === 2 && itemMatchesSearch(item),
+        item.type === 'item' &&
+        item.level === props.nbLevel - 1 &&
+        itemMatchesSearch(item),
     )
-
     // Pour chaque élément correspondant, ajouter ses parents aux ensembles d'expansion
     for (const item of matchingItems) {
-      // Trouver et ajouter la sous-catégorie parente
-      const subcategory = normalizedItems.value.find((i) => i.id === item.parentId)
-      if (subcategory) {
-        expandedSubcategories.value.add(subcategory.id)
+      let category
+      if (props.nbLevel === 3) {
+        // Trouver et ajouter la sous-catégorie parente
+        const subcategory = normalizedItems.value.find(
+          (i) => i.id === item.parentId,
+        )
+        if (subcategory) {
+          expandedSubcategories.value.add(subcategory.id)
 
-        // Trouver et ajouter la catégorie parente
-        const category = normalizedItems.value.find((i) => i.id === subcategory.parentId)
-        if (category) {
-          expandedCategories.value.add(category.id)
+          // Trouver et ajouter la catégorie parente
+          category = normalizedItems.value.find(
+            (i) => i.id === subcategory.parentId,
+          )
         }
+      } else {
+        // Trouver et ajouter la catégorie parente
+        category = normalizedItems.value.find((i) => i.id === item.parentId)
+      }
+
+      if (category) {
+        expandedCategories.value.add(category.id)
       }
     }
   }
@@ -428,8 +453,7 @@ const anyWordStartsWith = (
 
   return normalizedText
     .split(' ')
-    .some(word => word.startsWith(normalizedSearch))
-
+    .some((word) => word.startsWith(normalizedSearch))
 }
 
 /**
@@ -457,44 +481,54 @@ const itemMatchesSearch = (item: SelectItem): boolean => {
   const normalizedSearch = StringUtils.normalize(searchText.value)
 
   // Find the item with normalized label from our computed property
-  const itemWithNormalizedLabel = normalizedItems.value.find(i => i.id === item.id)
+  const itemWithNormalizedLabel = normalizedItems.value.find(
+    (i) => i.id === item.id,
+  )
   if (!itemWithNormalizedLabel) return false
 
-  // Si c'est un élément de niveau 2, vérifier son label et les labels de ses parents
-  if (item.type === 'item' && item.level === 2) {
+  // Si c'est un élément de niveau nbLevel - 1, vérifier son label et les labels de ses parents
+  if (item.type === 'item' && item.level === props.nbLevel - 1) {
     // Vérifier le label de l'élément
-    if (anyWordStartsWith(itemWithNormalizedLabel.normalizedLabel!, normalizedSearch))
-      return true
-
-    // Trouver et vérifier le label de la sous-catégorie parente
-    const subcategory = normalizedItems.value.find((i) => i.id === item.parentId)
     if (
-      subcategory &&
       anyWordStartsWith(
-        subcategory.normalizedLabel!,
+        itemWithNormalizedLabel.normalizedLabel!,
         normalizedSearch,
       )
     )
       return true
 
-    // Trouver et vérifier le label de la catégorie parente
-    if (subcategory && subcategory.parentId) {
-      const category = normalizedItems.value.find((i) => i.id === subcategory.parentId)
+    let parentId = item.parentId
+    if (props.nbLevel === 3) {
+      // Trouver et vérifier le label de la sous-catégorie parente
+      const subcategory = normalizedItems.value.find((i) => i.id === parentId)
       if (
-        category &&
-        anyWordStartsWith(
-          category.normalizedLabel!,
-          normalizedSearch,
-        )
+        subcategory &&
+        anyWordStartsWith(subcategory.normalizedLabel!, normalizedSearch)
       )
         return true
+
+      // Trouver et vérifier le label de la catégorie parente
+      if (subcategory && subcategory.parentId) {
+        parentId = subcategory.parentId
+      }
     }
 
+    // Trouver et vérifier le label de la catégorie parente
+    const category = normalizedItems.value.find((i) => i.id === parentId)
+    if (
+      category &&
+      anyWordStartsWith(category.normalizedLabel!, normalizedSearch)
+    )
+      return true
+
     return false
   }
 
   // Pour les autres types d'éléments, vérifier simplement leur label
-  return anyWordStartsWith(itemWithNormalizedLabel.normalizedLabel!, normalizedSearch)
+  return anyWordStartsWith(
+    itemWithNormalizedLabel.normalizedLabel!,
+    normalizedSearch,
+  )
 }
 
 /**
@@ -505,7 +539,9 @@ const itemMatchesSearch = (item: SelectItem): boolean => {
 const findMatchingLevel2Items = (): SelectItem[] => {
   return normalizedItems.value.filter(
     (item) =>
-      item.type === 'item' && item.level === 2 && itemMatchesSearch(item),
+      item.type === 'item' &&
+      item.level === props.nbLevel - 1 &&
+      itemMatchesSearch(item),
   )
 }
 
@@ -522,12 +558,23 @@ const buildSearchResultsList = (matchingItems: SelectItem[]): SelectItem[] => {
   const addedSubcategoryIds = new Set<string>()
 
   for (const item of matchingItems) {
-    // Trouver la sous-catégorie parente
-    const subcategory = normalizedItems.value.find((i) => i.id === item.parentId)
-    if (!subcategory) continue
+    let parentId = item.parentId
+    if (props.nbLevel === 3) {
+      // Trouver la sous-catégorie parente
+      const subcategory = normalizedItems.value.find((i) => i.id === parentId)
+      if (!subcategory) continue
+
+      // Ajouter la sous-catégorie si elle n'est pas déjà présente
+      if (!addedSubcategoryIds.has(subcategory.id)) {
+        result.push(subcategory)
+        addedSubcategoryIds.add(subcategory.id)
+        expandedSubcategories.value.add(subcategory.id)
+      }
+      parentId = subcategory.parentId
+    }
 
     // Trouver la catégorie parente
-    const category = normalizedItems.value.find((i) => i.id === subcategory.parentId)
+    const category = normalizedItems.value.find((i) => i.id === parentId)
     if (!category) continue
 
     // Ajouter la catégorie si elle n'est pas déjà présente
@@ -537,13 +584,6 @@ const buildSearchResultsList = (matchingItems: SelectItem[]): SelectItem[] => {
       expandedCategories.value.add(category.id)
     }
 
-    // Ajouter la sous-catégorie si elle n'est pas déjà présente
-    if (!addedSubcategoryIds.has(subcategory.id)) {
-      result.push(subcategory)
-      addedSubcategoryIds.add(subcategory.id)
-      expandedSubcategories.value.add(subcategory.id)
-    }
-
     // Ajouter l'élément
     result.push(item)
   }
@@ -569,7 +609,10 @@ const processItemsRecursively = (
       result.push(item)
       if (expandedCategories.value.has(item.id)) {
         const subcategories = normalizedItems.value.filter(
-          (i) => i.parentId === item.id && i.type === 'subcategory',
+          (i) =>
+            i.parentId === item.id &&
+            ((props.nbLevel == 2 && i.type === 'item') ||
+              (props.nbLevel == 3 && i.type === 'subcategory')),
         )
         processItemsRecursively(subcategories, result, true)
       }

+ 3 - 5
components/Ui/Input/TreeSelect/EventCategories.vue

@@ -5,15 +5,15 @@
     :label="$t(label)"
     v-bind="$attrs"
     :loading="status === FETCHING_STATUS.PENDING"
-    @update:model-value="$emit('update:modelValue', $event)"
     :max-visible-chips="6"
+    @update:model-value="$emit('update:modelValue', $event)"
   />
 </template>
 
 <script setup lang="ts">
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import EventCategory from '~/models/Core/EventCategory'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 const props = defineProps({
   modelValue: {
@@ -31,7 +31,6 @@ const props = defineProps({
   },
 })
 
-
 const i18n = useI18n()
 
 const emit = defineEmits(['update:modelValue'])
@@ -137,11 +136,10 @@ const hierarchicalItems = computed(() => {
   return result
 })
 
-
 // Nettoyer les données lors du démontage du composant
 onBeforeUnmount(() => {
   // Nettoyer les références du store si nécessaire
-  if (process.client) {
+  if (import.meta.client) {
     clearNuxtData('/^' + EventCategory + '_many_/')
     useRepo(EventCategory).flush()
   }

+ 118 - 0
components/Ui/Input/TreeSelect/TypeOfPractices.vue

@@ -0,0 +1,118 @@
+<template>
+  <UiInputTreeSelect
+    :model-value="modelValue"
+    :items="hierarchicalItems"
+    :label="$t(label)"
+    v-bind="$attrs"
+    :loading="status === FETCHING_STATUS.PENDING"
+    :max-visible-chips="6"
+    :nb-level="2"
+    @update:model-value="$emit('update:modelValue', $event)"
+  />
+</template>
+
+<script setup lang="ts">
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import EventCategory from '~/models/Core/EventCategory'
+import { FETCHING_STATUS } from '~/types/enum/data'
+import TypeOfPractice from '~/models/Organization/TypeOfPractice'
+
+const props = defineProps({
+  modelValue: {
+    type: Array as PropType<string[]>,
+    required: true,
+  },
+  /**
+   * Label du champ
+   * Si non défini, c'est le nom de propriété qui est utilisé
+   */
+  label: {
+    type: String,
+    required: false,
+    default: null,
+  },
+})
+
+const i18n = useI18n()
+
+const emit = defineEmits(['update:modelValue'])
+
+const { fetchCollection } = useEntityFetch()
+
+const { data: typeOfPractices, status } = fetchCollection(TypeOfPractice)
+
+// Transform type of practices into hierarchical items for TreeSelect
+const hierarchicalItems = computed(() => {
+  if (!typeOfPractices.value || typeOfPractices.value.length === 0) {
+    return []
+  }
+
+  const result = []
+  const categoriesMap = new Map()
+
+  // First pass: collect all unique category
+  typeOfPractices.value.items.forEach((typeOfPractice) => {
+    if (!typeOfPractice.category || !typeOfPractice.name) return
+
+    // Create unique keys for categories
+    const categoryKey = typeOfPractice.category
+
+    // Add category if not already added
+    if (!categoriesMap.has(categoryKey)) {
+      categoriesMap.set(categoryKey, {
+        id: `category-${categoryKey}`,
+        label: i18n.t(typeOfPractice.category),
+        type: 'category',
+        level: 0,
+      })
+    }
+  })
+
+  // Convert categories map to array and sort alphabetically by label
+  const sortedCategories = Array.from(categoriesMap.values()).sort((a, b) =>
+    a.label.localeCompare(b.label),
+  )
+
+  // Add sorted families to result
+  sortedCategories.forEach((cat) => {
+    result.push(cat)
+  })
+
+  // Collect all type first, then sort and add to result
+  const types = []
+  typeOfPractices.value.items.forEach((typeOfPractice) => {
+    if (!typeOfPractice.category || !typeOfPractice.name) return
+
+    types.push({
+      id: `type-${typeOfPractice.id}`,
+      label: i18n.t(typeOfPractice.name),
+      value: typeOfPractice.id,
+      type: 'item',
+      parentId: `category-${typeOfPractice.category}`,
+      level: 1,
+    })
+  })
+
+  // Sort types alphabetically by label and add to result
+  types
+    .sort((a, b) => a.label.localeCompare(b.label))
+    .forEach((type) => {
+      result.push(type)
+    })
+
+  return result
+})
+
+// Nettoyer les données lors du démontage du composant
+onBeforeUnmount(() => {
+  // Nettoyer les références du store si nécessaire
+  if (import.meta.client) {
+    clearNuxtData('/^' + TypeOfPractice + '_many_/')
+    useRepo(TypeOfPractice).flush()
+  }
+})
+</script>
+
+<style scoped lang="scss">
+/* No specific styles needed */
+</style>

+ 78 - 59
components/Ui/MapLeaflet.client.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="map-container">
-    <v-skeleton-loader type="image" v-if="pending"></v-skeleton-loader>
+    <v-skeleton-loader v-if="pending" type="image"></v-skeleton-loader>
 
     <LMap
       v-show="!pending"
@@ -9,54 +9,55 @@
       :center="position"
       :use-global-leaflet="false"
     >
-
       <LTileLayer
         url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
-        attribution="&amp;copy; <a href=&quot;https://www.openstreetmap.org/&quot;>OpenStreetMap</a> contributors"
+        attribution='&amp;copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors'
         layer-type="base"
         name="OpenStreetMap"
       />
 
       <LMarker
         :lat-lng="position"
-        @update:latLng="onPositionUpdate($event)"
         :draggable="!readonly"
-         />
+        @update:lat-lng="onPositionUpdate($event)"
+      />
     </LMap>
 
     <v-btn
+      v-if="searchButton && !readonly"
       prepend-icon="fas fa-location-dot"
       class="mt-3"
-      v-if="searchButton && !readonly"
       @click="search()"
     >
-      {{$t('search_gps_button')}}
+      {{ $t('search_gps_button') }}
     </v-btn>
 
     <div v-if="!pending && gpsResponses.length > 0">
-      <div v-for="(gpsResponse, key) in gpsResponses" class="address_choices" @click="addressChoice(key)">
-        {{gpsResponse['displayName']}}
-        <v-btn
-          prepend-icon="fas fa-map-location"
-          @click="addressChoice(key)"
-        >Choisir</v-btn>
+      <div
+        v-for="(gpsResponse, key) in gpsResponses"
+        :key="key"
+        class="address_choices"
+        @click="addressChoice(key)"
+      >
+        {{ gpsResponse['displayName'] }}
+        <v-btn prepend-icon="fas fa-map-location" @click="addressChoice(key)"
+          >Choisir</v-btn
+        >
       </div>
     </div>
-
   </div>
 </template>
 
 <script setup lang="ts">
-
 import 'leaflet/dist/leaflet.css'
 import { LMap, LTileLayer, LMarker } from '@vue-leaflet/vue-leaflet'
-import {type ComputedRef, defineProps, type PropType} from 'vue'
-import {LatLng, type PointTuple} from 'leaflet'
-import {useAp2iRequestService} from "~/composables/data/useAp2iRequestService";
-import UrlUtils from "~/services/utils/urlUtils";
-import type {AnyJson, CollectionResponsePromise} from "~/types/data";
-import Country from "~/models/Core/Country";
-import {useEntityManager} from "~/composables/data/useEntityManager";
+import { type ComputedRef, defineProps, type PropType } from 'vue'
+import { LatLng, type PointTuple } from 'leaflet'
+import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
+import UrlUtils from '~/services/utils/urlUtils'
+import type { AnyJson, CollectionResponsePromise } from '~/types/data'
+import Country from '~/models/Core/Country'
+import { useEntityManager } from '~/composables/data/useEntityManager'
 
 const props = defineProps({
   latitude: {
@@ -67,34 +68,40 @@ const props = defineProps({
     type: Number as PropType<number | null>,
     required: true,
   },
-  streetAddress:{
+  streetAddress: {
     type: String as PropType<string | null>,
-    required: false
+    required: false,
+    default: null,
   },
-  streetAddressSecond:{
+  streetAddressSecond: {
     type: String as PropType<string | null>,
-    required: false
+    required: false,
+    default: null,
   },
-  streetAddressThird:{
+  streetAddressThird: {
     type: String as PropType<string | null>,
-    required: false
+    required: false,
+    default: null,
   },
-  postalCode:{
+  postalCode: {
     type: String as PropType<string | null>,
-    required: false
+    required: false,
+    default: null,
   },
-  addressCity:{
+  addressCity: {
     type: String as PropType<string | null>,
-    required: false
+    required: false,
+    default: null,
   },
-  addressCountryId:{
+  addressCountryId: {
     type: Number as PropType<number | null>,
-    required: false
+    required: false,
+    default: null,
   },
-  searchButton:{
+  searchButton: {
     type: Boolean,
     required: false,
-    default: false
+    default: false,
   },
   readonly: {
     type: Boolean,
@@ -105,11 +112,14 @@ const props = defineProps({
 
 const FRANCE_LATITUDE = 46.603354
 const FRANCE_LONGITUDE = 1.888334
-const {apiRequestService, pending} = useAp2iRequestService()
+const { apiRequestService, pending } = useAp2iRequestService()
 const { em } = useEntityManager()
 
-const position:ComputedRef<PointTuple> = computed(()=>{
-  return [props.latitude || FRANCE_LATITUDE, props.longitude || FRANCE_LONGITUDE]
+const position: ComputedRef<PointTuple> = computed(() => {
+  return [
+    props.latitude || FRANCE_LATITUDE,
+    props.longitude || FRANCE_LONGITUDE,
+  ]
 })
 
 const zoom = computed({
@@ -118,52 +128,61 @@ const zoom = computed({
   },
   set(newValue: string) {
     zoom.value = newValue
-  }
+  },
 })
 
 const emit = defineEmits(['update:latitude', 'update:longitude'])
 
-const onPositionUpdate = (event: LatLng):void => {
+const onPositionUpdate = (event: LatLng): void => {
   emit('update:latitude', event.lat)
   emit('update:longitude', event.lng)
 }
 
-const gpsResponses:Ref<Array<AnyJson>> = ref([])
+const gpsResponses: Ref<Array<AnyJson>> = ref([])
 const search = async () => {
   gpsResponses.value = []
   const baseUrl = UrlUtils.join('api', 'gps-coordinate-searching')
-  const query:AnyJson = {
-    'streetAddress': props.streetAddress,
-    'streetAddressSecond': props.streetAddressSecond,
-    'streetAddressThird': props.streetAddressThird,
-    'cp': props.postalCode,
-    'city': props.addressCity
+  const query: AnyJson = {
+    streetAddress: props.streetAddress,
+    streetAddressSecond: props.streetAddressSecond,
+    streetAddressThird: props.streetAddressThird,
+    cp: props.postalCode,
+    city: props.addressCity,
   }
 
-  if(props.addressCountryId){
-    const country:Country = em.find(Country, props.addressCountryId)
+  if (props.addressCountryId) {
+    const country: Country = em.find(Country, props.addressCountryId)
     query['country'] = country?.name
   }
 
   const url = UrlUtils.addQuery(baseUrl, query)
-  const responses:CollectionResponsePromise = await apiRequestService.get(url)
-
-  if(responses['member'].length > 0){
-    onPositionUpdate(new LatLng(responses['member'][0]['latitude'], responses['member'][0]['longitude']))
-    if(responses['member'].length > 1){
+  const responses: CollectionResponsePromise = await apiRequestService.get(url)
+
+  if (responses['member'].length > 0) {
+    onPositionUpdate(
+      new LatLng(
+        responses['member'][0]['latitude'],
+        responses['member'][0]['longitude'],
+      ),
+    )
+    if (responses['member'].length > 1) {
       zoom.value = 6
       gpsResponses.value = responses['member']
-    }else{
+    } else {
       zoom.value = 12
     }
   }
 }
 
-const addressChoice = (key:number):void => {
+const addressChoice = (key: number): void => {
   zoom.value = 12
-  onPositionUpdate(new LatLng(gpsResponses.value[key]['latitude'] as number, gpsResponses.value[key]['longitude']  as number))
+  onPositionUpdate(
+    new LatLng(
+      gpsResponses.value[key]['latitude'] as number,
+      gpsResponses.value[key]['longitude'] as number,
+    ),
+  )
 }
-
 </script>
 
 <style scoped lang="scss">

+ 12 - 6
composables/data/useAp2iRequestService.ts

@@ -77,19 +77,25 @@ export const useAp2iRequestService = () => {
       (response.status === 400 || response.status >= 404)
     ) {
       // @see https://developer.mozilla.org/fr/docs/Web/HTTP/Status
-      let errorMsg
+      const errorMsg = []
       if (error) {
-        errorMsg = error.message
+        errorMsg.push(error.message)
+      } else if (response._data && response._data.violations) {
+        for (const violation of response._data.violations) {
+          errorMsg.push(violation.message as string)
+        }
       } else if (response._data && response._data.detail) {
-        errorMsg = response._data.detail
+        errorMsg.push(response._data.detail)
       } else if (response.statusText) {
-        errorMsg = response.statusText
+        errorMsg.push(response.statusText)
       } else {
-        errorMsg = 'An error occured'
+        errorMsg.push('An error occured')
       }
 
       console.error('! Request error: ' + errorMsg)
-      usePageStore().addAlert(TYPE_ALERT.ALERT, [errorMsg])
+      usePageStore().addAlert(TYPE_ALERT.ALERT, errorMsg)
+
+      throw { response, error }
     }
   }
 

+ 9 - 11
composables/data/useEntityFetch.ts

@@ -13,13 +13,13 @@ import type Query from '~/services/data/Query'
 interface useEntityFetchReturnType {
   fetch: <T extends typeof ApiResource>(
     model: T,
-    id?: number | null
+    id?: number | null,
   ) => AsyncData<InstanceType<T> | null, Error | null>
 
   fetchCollection: <T extends typeof ApiResource>(
     model: T,
     parent?: T | null,
-    query?: typeof Query | Query| null,
+    query?: typeof Query | Query | null,
   ) => {
     data: ComputedRef<Collection<InstanceType<T>> | null>
     refresh: (
@@ -40,7 +40,10 @@ export const useEntityFetch = (
 ): useEntityFetchReturnType => {
   const { em } = useEntityManager()
 
-  const fetch = <T extends typeof ApiResource>(model: T, id?: number|null): AsyncData<InstanceType<T> | null, Error | null> => {
+  const fetch = <T extends typeof ApiResource>(
+    model: T,
+    id?: number | null,
+  ): AsyncData<InstanceType<T> | null, Error | null> => {
     return useAsyncData(
       model.entity + '_' + id + '_' + uuid4(),
       () => em.fetch(model, id),
@@ -48,17 +51,12 @@ export const useEntityFetch = (
     )
   }
 
-  const fetchCollection = <T extends typeof ApiResource> (
+  const fetchCollection = <T extends typeof ApiResource>(
     model: T,
     parent: T | null = null,
     query: Query | null = null,
-  )  => {
-    const {
-      data,
-      refresh,
-      error,
-      status
-    } = useAsyncData(
+  ) => {
+    const { data, refresh, error, status } = useAsyncData(
       model.entity + '_many_' + uuid4(),
       () => em.fetchCollection(model, parent, query),
       { lazy, deep: true },

+ 4 - 4
composables/data/useEntityManager.ts

@@ -2,15 +2,15 @@ import { useRepo } from 'pinia-orm'
 import EntityManager from '~/services/data/entityManager'
 import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
 import { useAccessProfileStore } from '~/stores/accessProfile'
-import type ApiResource from "~/models/ApiResource";
-import type {AsyncData} from "#app";
+import type ApiResource from '~/models/ApiResource'
+import type { AsyncData } from '#app'
 
 let entityManager: EntityManager | null = null
 
-interface useEntityManagerReturnType {
+interface _useEntityManagerReturnType {
   fetch: <T extends typeof ApiResource>(
     model: T,
-    id?: number | null
+    id?: number | null,
   ) => AsyncData<InstanceType<T> | null, Error | null>
 }
 

+ 1 - 1
composables/form/useDeleteItem.ts

@@ -4,7 +4,7 @@ import { TYPE_ALERT } from '~/types/enum/enums'
 import { useEntityManager } from '~/composables/data/useEntityManager'
 
 export function useDeleteItem() {
-  async function deleteItem <T extends ApiResource>(item: T) {
+  async function deleteItem<T extends ApiResource>(item: T) {
     const { em } = useEntityManager()
 
     try {

+ 4 - 6
composables/utils/useDownloadFile.ts

@@ -5,17 +5,15 @@ import type File from '~/models/Core/File'
 export const useDownloadFile = async (file: File) => {
   const { apiRequestService } = useAp2iRequestService()
 
-  const downloadUrl = `api/download/${file.id}`
+  const downloadUrl = `api/file/download/${file.id}`
 
-  const response = await apiRequestService.get(downloadUrl)
+  const response = (await apiRequestService.get(downloadUrl)) as unknown as Blob
 
-  const blobPart = await response.blob()
-
-  if (!response || blobPart.size === 0) {
+  if (!response || response.size === 0) {
     console.error('Error: file ' + file.id + ' not found')
   }
 
-  const blob = new Blob([blobPart], { type: response.type })
+  const blob = new Blob([response], { type: response.type })
 
   FileSaver.saveAs(blob, file.name ?? 'unknown')
 }

+ 3 - 3
composables/utils/useHomeUrl.ts

@@ -1,15 +1,15 @@
 import { useAdminUrl } from '~/composables/utils/useAdminUrl'
-import {useAbility} from "@casl/vue";
+import { useAbility } from '@casl/vue'
 export const useHomeUrl = () => {
   const ability = useAbility()
 
   let homeUrl = null
 
-  if(ability.can('display', 'freemium_dashboard_page')){
+  if (ability.can('display', 'freemium_dashboard_page')) {
     const router = useRouter()
     const to = router.resolve({ name: 'freemium_dashboard_page' })
     homeUrl = to.href
-  }else{
+  } else {
     const { makeAdminUrl } = useAdminUrl()
     homeUrl = makeAdminUrl('dashboard')
   }

+ 3 - 0
config/theme.ts

@@ -70,9 +70,11 @@ export const lightTheme: Theme = {
 
     neutral: '#e6e6e6',
     'on-neutral': '#666666',
+    'on-neutral--sub': '#a6a6a6',
     'on-neutral--clickable': '#00997d',
 
     'neutral-soft': '#f2f2f2',
+    'on-neutral-soft--sub': '#c9c9c9',
     'on-neutral-soft': '#333333',
 
     'neutral-very-soft': '#ffffff',
@@ -125,6 +127,7 @@ export const darkTheme: Theme = {
 
     neutral: '#324150',
     'on-neutral': '#cccccc',
+    'on-neutral--sub': '#a6a6a6',
     'on-neutral--clickable': '#a9e0d6',
 
     'neutral-soft': '#090c11',

+ 1 - 1
eslint.config.mjs

@@ -51,7 +51,7 @@ const customConfig = [
       'vue/multi-word-component-names': 0,
       '@typescript-eslint/no-inferrable-types': 0,
       '@typescript-eslint/no-extraneous-class': 0,
-      'vue/html-self-closing': 0
+      'vue/html-self-closing': 0,
     },
   },
   // Directory-specific configurations

+ 30 - 0
i18n/lang/fr/breadcrumbs.json

@@ -0,0 +1,30 @@
+{
+  "freemium_event_create_page_breadcrumbs": "Création d'un événement",
+  "freemium_dashboard_page_breadcrumbs": "Accueil",
+  "freemium_event_edit_page_breadcrumbs": "Édition d'un événement",
+  "freemium_organization_page_breadcrumbs": "Fiche de ma structure",
+  "new_education_timing_breadcrumbs": "Création",
+  "record_a_new_subdomain_breadcrumbs": "Nouveau sous-domaine",
+  "cycle_breadcrumbs": "Édition d'un cycle",
+  "educationTiming_breadcrumbs": "Détails",
+  "create_a_new_residence_area_breadcrumbs": "Création",
+  "edit_resident_area_breadcrumbs": "Détails",
+  "parameters_residence_areas_page_breadcrumbs": "Zone de résidence",
+  "activate_a_subdomain_breadcrumbs": "Activer un sous-domaine",
+  "parameters_attendances_page_breadcrumbs": "Absences",
+  "new_attendance_booking_reason_breadcrumbs": "Création d'un motif",
+  "attendanceBookingReason_breadcrumbs": "Édition d'un motif",
+  "parameters_bulletin_page_breadcrumbs": "Bulletins",
+  "parameters_education_notation_page_breadcrumbs": "Suivi pédagogique",
+  "parameters_education_timings_page_breadcrumbs": "Durée des cours",
+  "parameters_general_page_breadcrumbs": "Paramètres généraux",
+  "parameters_intranet_page_breadcrumbs": "Accès intranet",
+  "parameters_sms_page_breadcrumbs": "Option SMS",
+  "parameters_super_admin_page_breadcrumbs": "Compte super admin",
+  "parameters_teaching_page_breadcrumbs": "Enseignements",
+  "parameters_website_page_breadcrumbs": "Site internet",
+  "cmf_licence_page_breadcrumbs": "Licence CMF",
+  "my_settings_page_breadcrumbs": "Mes préférences",
+  "subscription_page_breadcrumbs": "Mon abonnement",
+  "parameters_breadcrumbs": "Préférences"
+}

+ 1 - 0
i18n/lang/fr/event_categories.json

@@ -1,4 +1,5 @@
 {
+  "BAL": "Bal",
   "CULTURAL_EVENT": "Evénements culturels",
   "INTERNAL_EVENT": "Evénements internes",
   "PEDAGOGIC_EVENT": "Evénements pédagogiques",

+ 31 - 26
i18n/lang/fr/general.json

@@ -1,5 +1,31 @@
 {
-  "warning_edit_place": "Si vous modifiez les informations de ce lieu et que ce lieu est lié à d'autre événements,alors les changements seront répercutés dans tous vos événements liés.",
+  "VARIOUS_ORCHESTRA": "Orchestre divers",
+  "MUSIC_TEACHING": "Enseignement musique",
+  "DRAMATIC_ARTS": "Enseignement arts dramatiques",
+  "DANCE_LESSONS": "Enseignement danse",
+  "CIRCUS_TRAINING": "Enseignement de cirque",
+  "ART_TEACHING": "Enseignement d'art",
+  "MAJORETTE_AND_TWIRLING": "Majorettes et Twirling",
+  "TAP_DANCE": "Claquettes",
+  "LATIN_DANCE": "Danse latines",
+  "FOLK_DANCE": "Danse folklorique",
+  "CLASSICAL_DANCE": "Danse classique",
+  "BREAK_DANCING": "Break dance",
+  "BALLROOM_DANCE": "Danse de salon",
+  "COMPANIES": "Compagnies",
+  "OTHER_TYPE": "Autres",
+  "type_of_practices": "Type de pratiques",
+  "welcome_freemium_title": "Mon compte opentalent",
+  "gender_event": "Genre",
+  "url_error": "Le lien n'est pas correct",
+  "organization_logo": "Logo de votre structure",
+  "event_image": "Affiche de l'événement",
+  "must_be_positive_or_egal_0": "La valeur doit être positive ou égale à 0",
+  "must_be_positive": "La valeur doit être positive",
+  "datetimeStart must be less than datetimeEnd": "La date de début doit être avant la date de fin",
+  "priceMini must be less than priceMaxi": "Le prix minimum doit être plus petit que le prix maximum",
+  "are_you_sure_to_process": "Êtes vous sûr(e) de vouloir continuer ?",
+  "warning_edit_place": "En modifiant ce lieu, tous les évènements déjà associés (y compris les évènements passés) seront automatiquement mis à jour avec les nouvelles informations.",
   "agenda_def": "Pour la promotion de votre structure et de vos événements",
   "manager_def": "Pour les fédérations, confédérations et institutions publiques",
   "school_def": "Pour tous les établissements d’enseignement artistique",
@@ -12,10 +38,8 @@
   "add_event": "Ajouter un événement",
   "my_organization": "Ma structure",
   "edit_organization": "Modifier la structure",
-  "dashboard_breadcrumbs": "Tableau de bord",
-  "freemium_breadcrumbs": "Freemium",
   "i_understand": "Je comprends",
-  "place_change_everywhere": "Les changements apportés seront appliqués aux autres événements",
+  "place_change_everywhere": "Attention : Modification d’un lieu existant",
   "event_categories_choices": "Choisissez à quelles catégories appartient votre événement",
   "search": "Rechercher",
   "others": "autres",
@@ -68,7 +92,6 @@
   "service_detail": "Détail des services",
   "my_settings_page": "Mes préférences",
   "allow_report_message": "Je souhaite recevoir les rapports d'envoi des emails que j'envoie",
-  "my-settings_breadcrumbs": "Mes paramètres",
   "message_settings": "Paramètres des messages",
   "rewards_list": "Configuration des distinctions",
   "access_rewards_list": "Gestion des distinctions",
@@ -77,11 +100,6 @@
   "afi_export": "Export AFI",
   "sdd_regie_export": "Export prélèvements SDD Régie",
   "item": "Détails",
-  "organization_breadcrumbs": "Fiche de la structure",
-  "subscription_breadcrumbs": "Mon abonnement",
-  "address_breadcrumbs": "Adresse postale",
-  "contact_points_breadcrumbs": "Points de contact",
-  "parameters_breadcrumbs": "Paramètres",
   "help_super_admin": "Le compte super-admin possède tous les droits de gestion sur votre logiciel. On l’utilise surtout pour la gestion de votre site internet et, à la première connexion au logiciel, afin de créer des comptes pour tous membres de votre structure. Enfin, il peut également être utile en cas de dépannage dans certaines situations particulières.",
   "yourWebsiteAddressIs": "L'adresse de votre site web est",
   "yourOpentalentWebsiteWillBeDeactivatedOnceYouLlHaveSaved": "Votre site web Opentalent sera désactivé une fois que vous aurez enregistré",
@@ -148,6 +166,8 @@
   "CATEGORY_OTHER": "Autres activités",
   "CATEGORY_CHORUS": "Chorale / Groupe vocal",
   "CATEGORY_BAND": "Ensemble",
+  "CATEGORY_DANCES": "Danses",
+  "CATEGORY_TEACHING": "Enseignements",
   "BRASS_BAND": "Brass band",
   "HUNTING_HORNS": "Trompes de chasse",
   "PHILHARMONIC_ORCHESTRA": "Orchestre philharmonique",
@@ -366,7 +386,7 @@
   "mobilPhone": "Portable",
   "mobilPhoneInvalid": "Portable invalide",
   "actions": "Actions",
-  "twitter": "Lien Twitter",
+  "twitter": "Lien X",
   "facebook": "Lien Facebook",
   "instagram": "Lien Instagram",
   "image": "Image",
@@ -500,7 +520,6 @@
   "network": "Réseau",
   "schedule": "Agenda",
   "attendances": "Absences",
-  "attendances_breadcrumbs": "Absences",
   "equipment": "Parc matériel",
   "basicompta_admin": "Comptabilité BasiCompta",
   "education_state": "Suivi pédagogique",
@@ -565,7 +584,6 @@
   "tree_menu": "Gestion de l'arbre",
   "website": "Site internet",
   "parameters_website_page": "Site internet",
-  "website_breadcrumbs": "Site internet",
   "websiteList": "Site(s) internet",
   "advanced_modification": "Administration site internet",
   "simple_modification": "Modifications simplifiées",
@@ -706,19 +724,14 @@
   "show_adherents_list_and_their_coordinates": "Autoriser l'affichage de la liste des adhérents de votre structure, avec leurs coordonnées, dans le compte utilisateur de vos membres.",
   "students_are_also_association_members": "Les élèves sont adhérents également de l'association",
   "parameters_general_page": "Paramètres généraux",
-  "general_parameters_breadcrumbs": "Paramètres généraux",
   "teaching": "Enseignements",
   "parameters_teaching_page": "Enseignements",
-  "teaching_breadcrumbs": "Enseignements",
   "intranet_access": "Accès intranet (professeurs, élèves...)",
   "parameters_intranet_page": "Accès intranet",
-  "intranet_breadcrumbs": "Accès intranet",
   "educationNotations": "Suivi pédagogique",
   "parameters_education_notation_page": "Suivi pédagogique",
-  "education_notation_breadcrumbs": "Suivi pédagogique",
   "bulletin": "Bulletins",
   "parameters_bulletin_page": "Bulletins",
-  "bulletin_breadcrumbs": "Bulletins",
   "educationTimings": "Durée des cours (en minutes)",
   "new_education_timing": "Nouvelle durée de cours",
   "residenceAreas": "Zones de résidence",
@@ -728,10 +741,8 @@
   "sms_option_configuration": "Configuration de l'option SMS",
   "sms_option_configuration_notice": "Pour utiliser l'option SMS, renseignez les informations d'identification Mobyt de votre structure",
   "sms_option_configuration_tip": "Pour utiliser l'option SMS, renseignez les informations d'identification Mobyt de votre structure",
-  "sms_breadcrumbs": "SMS",
   "super_admin": "Compte super-admin",
   "parameters_super_admin_page": "Compte super-admin",
-  "super_admin_breadcrumbs": "Compte super-admin",
   "an_error_happened": "Une erreur s'est produite",
   "your_website": "Votre site web",
   "your_website_address_is": "L'adresse de votre site internet est",
@@ -740,8 +751,6 @@
   "your_subdomains": "Vos sous-domaines",
   "other_website": "Autre site internet",
   "Not Found": "Données non trouvée",
-  "subdomains_breadcrumbs": "Sous-domaines",
-  "new_breadcrumbs": "Nouveau",
   "validation_pending": "Validation en cours",
   "The subdomain is already active": "Le sous-domaine est déjà actif",
   "Not a valid subdomain": "Le sous-domaine est invalide",
@@ -779,16 +788,12 @@
   "Europe/Paris": "Europe/Paris",
   "licenceQrCode": "QrCode pour la licence",
   "parameters_education_timings_page": "Durée des cours",
-  "education_timings_breadcrumbs": "Durée des cours",
   "create_a_new_residence_area": "Créer une nouvelle zone de résidence",
-  "residence_areas_breadcrumbs": "Zones de résidence",
   "edit_resident_area": "Éditer la zone de résidence",
   "super_admin_explanation_text": "Le compte super-admin possède tous les droits de gestion sur votre logiciel. On l’utilise entre autre pour la gestion de votre site internet, pour créer les comptes des membres de votre structure à la première connexion au logiciel, ou dans des situations de dépannage.",
   "exit": "Quitter",
   "max_size_4_mb": "Taille maximum: 4 MO",
   "file_too_large": "Le fichier est trop volumineux",
-  "cycles_breadcrumbs": "Enseignements",
-  "cmf_licence_structure_breadcrumbs": "Licence CMF - Structure",
   "no_recorded_subdomain": "Aucun sous-domaine enregistré",
   "no_admin_access_recorded": "Aucun compte super-admin enregistré",
   "redirecting": "Redirection en cours",

+ 109 - 62
layouts/freemium.vue

@@ -9,7 +9,6 @@
       <LayoutHeader />
 
       <v-main class="main">
-
         <!-- Page will be rendered here-->
         <div>
           <LayoutSubheader />
@@ -22,73 +21,121 @@
 
       <!-- Footer -->
       <v-footer class="white--text footer theme-secondary">
-
-          <v-row v-if="smAndUp">
-            <v-col cols="12" sm="3">
-              <p class="text-h6 font-weight-bold text-center">{{$t('DÉCOUVREZ NOS SOLUTIONS')}}</p>
-            </v-col>
-            <v-col cols="12" sm="2" class="text-center text-product">
-              <a href="https://logiciels.opentalent.fr/opentalent-artist" target="_blank">
-                <img src="/images/Opentalent_Artist-Blanc.png" height="70" class="mb-2" />
-                <p>{{$t('artist_def')}}</p>
-              </a>
-            </v-col>
-            <v-col cols="12" sm="2" class="text-center text-product">
-              <a href="https://logiciels.opentalent.fr/opentalent-school" target="_blank">
-                <img src="/images/Opentalent_School-Blanc.png" height="70" class="mb-2" />
-                <p>{{$t('school_def')}}</p>
-              </a>
-            </v-col>
-            <v-col cols="12" sm="2" class="text-center text-product">
-              <a href="https://logiciels.opentalent.fr/opentalent-manager" target="_blank">
-                <img src="/images/Opentalent_Manager-Blanc.png" height="70" class="mb-2" />
-                <p>{{$t('manager_def')}}</p>
-              </a>
-            </v-col>
-            <v-col cols="12" sm="2" class="text-center text-product">
-              <a href="https://opentalent.fr/" target="_blank">
-                <img src="/images/OT_Logo_Agenda.png" height="70" class="mb-2" />
-                <p>{{$t('agenda_def')}}</p>
-              </a>
-            </v-col>
-          </v-row>
-
-          <v-row v-else>
-            <v-col cols="12">
-              <p class="text-h6 font-weight-bold text-center">{{$t('DÉCOUVREZ NOS SOLUTIONS')}}</p>
-            </v-col>
-            <v-col cols="3" class="text-center text-product">
-              <a href="https://logiciels.opentalent.fr/opentalent-artist" target="_blank">
-                <img src="/images/Opentalent_Artist_Griffe.png" height="70" class="mb-2" />
-              </a>
-            </v-col>
-            <v-col cols="3" class="text-center text-product">
-              <a href="https://logiciels.opentalent.fr/opentalent-school" target="_blank">
-                <img src="/images/Opentalent_School_Griffe.png" height="70" class="mb-2" />
-              </a>
-            </v-col>
-            <v-col cols="3" class="text-center text-product">
-              <a href="https://logiciels.opentalent.fr/opentalent-manager" target="_blank">
-                <img src="/images/Opentalent_Manager_Griffe.png" height="70" class="mb-2" />
-              </a>
-            </v-col>
-            <v-col cols="3" class="text-center text-product">
-              <a href="https://opentalent.fr" target="_blank">
-                <img src="/images/Opentalent_Griffe.png" height="70" class="mb-2" />
-              </a>
-            </v-col>
-          </v-row>
+        <v-row v-if="smAndUp">
+          <v-col cols="12" sm="3">
+            <p class="text-h6 font-weight-bold text-center">
+              {{ $t('DÉCOUVREZ NOS SOLUTIONS') }}
+            </p>
+          </v-col>
+          <v-col cols="12" sm="2" class="text-center text-product">
+            <a
+              href="https://logiciels.opentalent.fr/opentalent-artist"
+              target="_blank"
+            >
+              <img
+                src="/images/Opentalent_Artist-Blanc.png"
+                height="70"
+                class="mb-2"
+              />
+              <p>{{ $t('artist_def') }}</p>
+            </a>
+          </v-col>
+          <v-col cols="12" sm="2" class="text-center text-product">
+            <a
+              href="https://logiciels.opentalent.fr/opentalent-school"
+              target="_blank"
+            >
+              <img
+                src="/images/Opentalent_School-Blanc.png"
+                height="70"
+                class="mb-2"
+              />
+              <p>{{ $t('school_def') }}</p>
+            </a>
+          </v-col>
+          <v-col cols="12" sm="2" class="text-center text-product">
+            <a
+              href="https://logiciels.opentalent.fr/opentalent-manager"
+              target="_blank"
+            >
+              <img
+                src="/images/Opentalent_Manager-Blanc.png"
+                height="70"
+                class="mb-2"
+              />
+              <p>{{ $t('manager_def') }}</p>
+            </a>
+          </v-col>
+          <v-col cols="12" sm="2" class="text-center text-product">
+            <a href="https://opentalent.fr/" target="_blank">
+              <img src="/images/OT_Logo_Agenda.png" height="70" class="mb-2" />
+              <p>{{ $t('agenda_def') }}</p>
+            </a>
+          </v-col>
+        </v-row>
+
+        <v-row v-else>
+          <v-col cols="12">
+            <p class="text-h6 font-weight-bold text-center">
+              {{ $t('DÉCOUVREZ NOS SOLUTIONS') }}
+            </p>
+          </v-col>
+          <v-col cols="3" class="text-center text-product">
+            <a
+              href="https://logiciels.opentalent.fr/opentalent-artist"
+              target="_blank"
+            >
+              <img
+                src="/images/Opentalent_Artist_Griffe.png"
+                height="70"
+                class="mb-2"
+              />
+            </a>
+          </v-col>
+          <v-col cols="3" class="text-center text-product">
+            <a
+              href="https://logiciels.opentalent.fr/opentalent-school"
+              target="_blank"
+            >
+              <img
+                src="/images/Opentalent_School_Griffe.png"
+                height="70"
+                class="mb-2"
+              />
+            </a>
+          </v-col>
+          <v-col cols="3" class="text-center text-product">
+            <a
+              href="https://logiciels.opentalent.fr/opentalent-manager"
+              target="_blank"
+            >
+              <img
+                src="/images/Opentalent_Manager_Griffe.png"
+                height="70"
+                class="mb-2"
+              />
+            </a>
+          </v-col>
+          <v-col cols="3" class="text-center text-product">
+            <a href="https://opentalent.fr" target="_blank">
+              <img
+                src="/images/Opentalent_Griffe.png"
+                height="70"
+                class="mb-2"
+              />
+            </a>
+          </v-col>
+        </v-row>
       </v-footer>
 
       <LazyLayoutAlertContainer />
-
     </v-app>
   </div>
 </template>
 
 <script setup lang="ts">
 import { useLayoutStore } from '~/stores/layout'
-import {useDisplay} from "vuetify";
+import { useDisplay } from 'vuetify'
 
 const { smAndUp, sm } = useDisplay()
 const layoutStore = useLayoutStore()
@@ -109,7 +156,7 @@ const pageTitle = computed(() => i18n.t(route.name || 'freemium_page'))
 }
 
 .main {
-  flex: 1 0 auto;  /* Prend tout l’espace restant */
+  flex: 1 0 auto; /* Prend tout l’espace restant */
 }
 
 .v-footer {
@@ -119,11 +166,11 @@ const pageTitle = computed(() => i18n.t(route.name || 'freemium_page'))
   padding-bottom: 20px;
   flex: 0 0 auto !important;
 
-  .text-product{
+  .text-product {
     font-size: 13px;
   }
 
-  a{
+  a {
     color: white;
     text-decoration: none;
   }

+ 3 - 1
middleware/routing.global.ts

@@ -16,10 +16,12 @@ export default defineNuxtRouteMiddleware((to, _) => {
     const name: string = routeName?.toString() ?? ''
 
     // <<- TODO: remove after 2.6 release
+    const inDevPages = []
+
     const runtimeConfig = useRuntimeConfig()
     if (
       runtimeConfig.public.env === 'production' &&
-      name === 'cmf_licence_page'
+      inDevPages.includes(name)
     ) {
       const { redirectToHome } = useRedirect()
       redirectToHome()

+ 2 - 2
models/Billing/ResidenceArea.ts

@@ -1,6 +1,6 @@
 import { Str, Uid } from 'pinia-orm/dist/decorators'
 import ApiModel from '~/models/ApiModel'
-import {Assert} from "~/models/decorators";
+import { Assert } from '~/models/decorators'
 
 /**
  * Ap2i Model : ResidenceArea
@@ -14,6 +14,6 @@ export default class ResidenceArea extends ApiModel {
   declare id: number | string
 
   @Str(null)
-  @Assert({'nullable': false, 'max':255})
+  @Assert({ nullable: false, max: 255 })
   declare label: string | null
 }

+ 2 - 2
models/Booking/AttendanceBookingReason.ts

@@ -1,6 +1,6 @@
 import { Str, Uid } from 'pinia-orm/dist/decorators'
 import ApiModel from '~/models/ApiModel'
-import {Assert} from "~/models/decorators";
+import { Assert } from '~/models/decorators'
 
 /**
  * Motif d'absence ou de retard
@@ -14,6 +14,6 @@ export default class AttendanceBookingReason extends ApiModel {
   declare id: number | string
 
   @Str(null)
-  @Assert({'nullable': false, 'max':255})
+  @Assert({ nullable: false, max: 255 })
   declare reason: string | null
 }

+ 18 - 0
models/Booking/EventGender.ts

@@ -0,0 +1,18 @@
+import { Uid, Str } from 'pinia-orm/dist/decorators'
+import ApiModel from '~/models/ApiModel'
+
+/**
+ * AP2i Model : EventGender
+ **/
+export default class EventGender extends ApiModel {
+  static entity = 'event_genders'
+
+  @Uid()
+  declare id: number | string | null
+
+  @Str('')
+  declare name: string
+
+  @Str('')
+  declare type: 'PEDAGOGIC_EVENT' | 'CULTURAL_EVENT' | 'INTERNAL_EVENT'
+}

+ 2 - 2
models/Education/Cycle.ts

@@ -1,6 +1,6 @@
 import { Num, Str, Uid } from 'pinia-orm/dist/decorators'
 import ApiModel from '~/models/ApiModel'
-import {Assert} from "~/models/decorators";
+import { Assert } from '~/models/decorators'
 
 /**
  * AP2i Model: Cycle
@@ -14,7 +14,7 @@ export default class Cycle extends ApiModel {
   declare id: number | string | null
 
   @Str(null)
-  @Assert({'nullable': false, 'max':255})
+  @Assert({ nullable: false, max: 255 })
   declare label: string | null
 
   @Str(null)

+ 2 - 2
models/Education/EducationTiming.ts

@@ -1,6 +1,6 @@
 import { Num, Uid } from 'pinia-orm/dist/decorators'
 import ApiModel from '~/models/ApiModel'
-import {Assert} from "~/models/decorators";
+import { Assert } from '~/models/decorators'
 
 /**
  * AP2i Model : EducationTiming
@@ -14,6 +14,6 @@ export default class EducationTiming extends ApiModel {
   declare id: number | string
 
   @Num(null)
-  @Assert({'nullable': false, 'type' : 'integer'})
+  @Assert({ nullable: false, type: 'integer' })
   declare timing: number
 }

+ 19 - 10
models/Freemium/Event.ts

@@ -1,11 +1,11 @@
-import { Str, Uid } from 'pinia-orm/dist/decorators'
+import { Str, Uid, Attr, Num } from 'pinia-orm/dist/decorators'
 import ApiModel from '~/models/ApiModel'
-import {Assert, IriEncoded} from '~/models/decorators'
-import {Attr, Num} from "pinia-orm/decorators";
-import File from "~/models/Core/File";
-import Place from "~/models/Place/Place";
-import Country from "~/models/Core/Country";
-import Category from "~/models/Core/Category";
+import { Assert, IriEncoded } from '~/models/decorators'
+import File from '~/models/Core/File'
+import Place from '~/models/Place/Place'
+import Country from '~/models/Core/Country'
+import Category from '~/models/Core/Category'
+import EventGender from '~/models/Booking/EventGender'
 
 /**
  * AP2i Model : Freemium / Event
@@ -18,15 +18,15 @@ export default class Event extends ApiModel {
   declare id: number | string | null
 
   @Str(null)
-  @Assert({'nullable': false, 'max':255})
+  @Assert({ nullable: false, max: 255 })
   declare name: string | null
 
   @Str(null)
-  @Assert({'nullable': false})
+  @Assert({ nullable: false })
   declare datetimeStart: string
 
   @Str(null)
-  @Assert({'nullable': false})
+  @Assert({ nullable: false })
   declare datetimeEnd: string
 
   @Str(null)
@@ -37,9 +37,11 @@ export default class Event extends ApiModel {
   declare image: number | null
 
   @Str(null)
+  @Assert({ max: 255, type: 'url' })
   declare url: string
 
   @Str(null)
+  @Assert({ max: 255, type: 'url' })
   declare urlTicket: string
 
   @Attr(null)
@@ -78,12 +80,19 @@ export default class Event extends ApiModel {
   declare pricing: string | null
 
   @Num(null)
+  @Assert({ nullable: true, positive: 'positive' })
   declare priceMini: number | null
 
   @Num(null)
+  @Assert({ nullable: true, positive: 'positive' })
   declare priceMaxi: number | null
 
   @Attr(() => [])
   @IriEncoded(Category)
   declare categories: Array<string> | null
+
+  @Attr(null)
+  @IriEncoded(EventGender)
+  @Assert({ nullable: false })
+  declare gender: number | null
 }

+ 23 - 11
models/Freemium/Organization.ts

@@ -1,9 +1,9 @@
-import { Str, Uid } from 'pinia-orm/dist/decorators'
+import { Str, Uid, Attr, Bool, Num } from 'pinia-orm/dist/decorators'
 import ApiModel from '~/models/ApiModel'
-import {Assert, IdLess, IriEncoded} from '~/models/decorators'
-import {Attr, Bool, Num} from "pinia-orm/decorators";
-import Country from "~/models/Core/Country";
-import File from "~/models/Core/File";
+import { Assert, IdLess, IriEncoded } from '~/models/decorators'
+import Country from '~/models/Core/Country'
+import File from '~/models/Core/File'
+import TypeOfPractice from '~/models/Organization/TypeOfPractice'
 
 /**
  * AP2i Model : Freemium / Organization
@@ -17,14 +17,22 @@ export default class Organization extends ApiModel {
   declare id: number | string | null
 
   @Str(null)
-  @Assert({'nullable': false, 'max':128})
+  @Assert({ nullable: false, max: 128 })
   declare name: string | null
 
+  @Str('ARTISTIC_PRACTICE_ONLY')
+  @Assert({ nullable: false })
+  declare principalType: string | null
+
+  @Str('ASSOCIATION_LAW_1901')
+  @Assert({ nullable: false })
+  declare legalStatus: string | null
+
   @Str(null)
   declare description: string | null
 
   @Str(null)
-  @Assert({'nullable': false, 'type' : 'email'})
+  @Assert({ nullable: false, type: 'email' })
   declare email: string | null
 
   @Str(null)
@@ -56,19 +64,19 @@ export default class Organization extends ApiModel {
   declare longitude: number | null
 
   @Str(null)
-  @Assert({'max':255})
+  @Assert({ max: 255, type: 'url' })
   declare facebook: string
 
   @Str(null)
-  @Assert({'max':255})
+  @Assert({ max: 255, type: 'url' })
   declare twitter: string
 
   @Str(null)
-  @Assert({'max':255})
+  @Assert({ max: 255, type: 'url' })
   declare youtube: string
 
   @Str(null)
-  @Assert({'max':255})
+  @Assert({ max: 255, type: 'url' })
   declare instagram: string
 
   @Bool(true)
@@ -77,4 +85,8 @@ export default class Organization extends ApiModel {
   @Attr(null)
   @IriEncoded(File)
   declare logo: number | null
+
+  @Attr(() => [])
+  @IriEncoded(TypeOfPractice)
+  declare typeOfPractices: Array<string> | null
 }

+ 4 - 5
models/Freemium/Place.ts

@@ -1,8 +1,7 @@
-import { Str, Uid } from 'pinia-orm/dist/decorators'
+import { Str, Uid, Attr, Num } 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";
+import { Assert, IriEncoded } from '~/models/decorators'
+import Country from '~/models/Core/Country'
 
 /**
  * AP2i Model : Freemium / Place
@@ -18,7 +17,7 @@ export default class Place extends ApiModel {
   declare EventId: number | null
 
   @Str(null)
-  @Assert({'nullable': false, 'max':255})
+  @Assert({ nullable: false, max: 255 })
   declare name: string | null
 
   @Str(null)

+ 2 - 2
models/Organization/Parameters.ts

@@ -1,7 +1,7 @@
 import { Bool, Num, Str, Uid, Attr } from 'pinia-orm/dist/decorators'
 import ApiModel from '~/models/ApiModel'
 import Access from '~/models/Access/Access'
-import {Assert, IriEncoded} from '~/models/decorators'
+import { Assert, IriEncoded } from '~/models/decorators'
 import File from '~/models/Core/File'
 
 /**
@@ -141,7 +141,7 @@ export default class Parameters extends ApiModel {
   declare notifyAdministrationAbsence: boolean
 
   @Num(2, { notNullable: true })
-  @Assert({'nullable': false, 'type' : 'integer'})
+  @Assert({ nullable: false, type: 'integer' })
   declare numberConsecutiveAbsences: number
 
   @Bool(false, { notNullable: false })

+ 5 - 1
nuxt.config.ts

@@ -218,7 +218,11 @@ export default defineNuxtConfig({
       {
         code: 'fr',
         iso: 'fr-FR',
-        files: ['fr/general.json', 'fr/event_categories.json'],
+        files: [
+          'fr/general.json',
+          'fr/event_categories.json',
+          'fr/breadcrumbs.json',
+        ],
         name: 'Français',
       },
     ],

+ 0 - 251
pages/freemium/dashboard.vue

@@ -1,251 +0,0 @@
-<template>
-  <v-container fluid class="inner-container">
-    <v-row>
-      <!-- Bloc événements -->
-      <v-col cols="12" md="7">
-        <v-card>
-          <v-tabs v-model="tab" class="tabs-title">
-            <v-tab value="future">{{$t('futur_event')}}</v-tab>
-            <v-tab value="past">{{$t('past_event')}}</v-tab>
-          </v-tabs>
-
-          <v-btn color="primary" to="events/new" class="ml-5 mt-5">{{$t('add_event')}}</v-btn>
-
-          <v-tabs-window v-model="tab">
-            <v-tabs-window-item value="future">
-              <UiLoadingPanel v-if="statusUpcomingEvents == FETCHING_STATUS.PENDING" />
-
-              <UiEventList
-                v-if="statusUpcomingEvents == FETCHING_STATUS.SUCCESS && upcomingEvents?.items"
-                :events="upcomingEvents.items"
-                :pagination="upcomingEvents.pagination"
-                @load="loadUpcomingEvents"
-                @edit="editEvent"
-              />
-              <span v-if="upcomingEvents.items.length == 0" class="no_event">
-                {{$t('no_future_event')}}
-              </span>
-            </v-tabs-window-item>
-            <v-tabs-window-item value="past">
-              <UiLoadingPanel v-if="statusPastEvents == FETCHING_STATUS.PENDING" />
-
-              <UiEventList
-                v-if="statusPastEvents == FETCHING_STATUS.SUCCESS && pastEvents?.items"
-                :events="pastEvents.items"
-                :pagination="pastEvents.pagination"
-                @load="loadPastEvents"
-                @edit="editEvent"
-              />
-              <span v-if="pastEvents.items.length == 0" class="no_event">
-                {{$t('no_past_event')}}
-              </span>
-            </v-tabs-window-item>
-          </v-tabs-window>
-
-        </v-card>
-      </v-col>
-
-      <!-- Bloc structure -->
-      <v-col cols="12" md="5">
-        <v-card v-if="statusOrganization == FETCHING_STATUS.SUCCESS" class="pa-5">
-          <v-card-title class="text-h6" >
-            <v-icon icon="fa fa-hotel" class="text-button icon-hotel"  />
-            <span class="organization_title">{{$t('my_organization')}}</span>
-          </v-card-title>
-          <v-card-text>
-            <div><strong>{{$t('name')}} :</strong> {{ organization?.name }}</div>
-            <div><strong>{{$t('email')}} :</strong> {{ organization?.email }}</div>
-          </v-card-text>
-        </v-card>
-
-        <v-btn block class="mb-2 btn btn_edit_orga" to="organization">
-          <i class="fa fa-pen mr-2" />{{$t('edit_organization')}}
-        </v-btn>
-
-        <v-btn block class="text-black btn btn_trial" @click="startTrial">
-          <span><v-icon icon="fa fa-ticket" /> {{$t('try_premium_light')}}<br /> {{$t('30_days_free')}}</span>
-        </v-btn>
-
-      </v-col>
-    </v-row>
-
-  </v-container>
-
-  <LayoutDialogTrialAlreadyDid
-    :show="showDialogTrialAlReadyDid"
-    @close-dialog="showDialogTrialAlReadyDid = false"
-  />
-
-</template>
-
-<script setup lang="ts">
-
-import Query from "~/services/data/Query";
-
-definePageMeta({
-  name: 'freemium_dashboard_page',
-})
-
-import {type Ref, ref} from 'vue'
-import {useEntityFetch} from "~/composables/data/useEntityFetch";
-import Organization from "~/models/Freemium/Organization";
-import Event from "~/models/Freemium/Event";
-import type {AsyncData} from "#app";
-import OrderBy from "~/services/data/Filters/OrderBy";
-import {FETCHING_STATUS, ORDER_BY_DIRECTION, TIME_STRATEGY} from "~/types/enum/data";
-import PageFilter from "~/services/data/Filters/PageFilter";
-import TimeFilter from "~/services/data/Filters/TimeFilter";
-import Country from "~/models/Core/Country";
-import DateUtils from "~/services/utils/dateUtils";
-import UrlUtils from "~/services/utils/urlUtils";
-import {useApiLegacyRequestService} from "~/composables/data/useApiLegacyRequestService";
-import {useAdminUrl} from "~/composables/utils/useAdminUrl";
-
-//Ref Définition
-const runtimeConfig = useRuntimeConfig()
-const { fetch, fetchCollection } = useEntityFetch()
-const { apiRequestService } = useApiLegacyRequestService()
-const {makeAdminUrl} = useAdminUrl()
-const tab = ref(null)
-const upcomingPage = ref(1)
-const pastPage = ref(1)
-const showDialogTrialAlReadyDid: Ref<boolean> = ref(false)
-
-//Fetch
-const { data: organization, status:statusOrganization } = fetch(Organization)
-const { data: upcomingEvents, status: statusUpcomingEvents, refresh: refreshUpcomingEvents } = fetchEvents()
-const { data: pastEvents, status: statusPastEvents, refresh: refreshPastEvents } = fetchEvents(true)
-
-/**
- * Charge une page des événements à venir
- * @param pageNumber
- */
-function loadUpcomingEvents(pageNumber: number) {
-  upcomingPage.value = pageNumber
-  refreshPastEvents()
-}
-
-/**
- * Cahrge une page des événements passées
- * @param pageNumber
- */
-function loadPastEvents(pageNumber: number) {
-  pastPage.value = pageNumber
-  refreshPastEvents()
-}
-
-/**
- * Redirige vers la page d'édition d'un événement
- * @param eventId
- */
-function editEvent(eventId: number) {
-  navigateTo(UrlUtils.join('events', eventId))
-}
-
-/**
- * Récupère la liste des événements
- * @param past
- */
-function fetchEvents(past:boolean = false){
-  const today = computed(() => DateUtils.formatIsoShortDate(new Date()))
-  const query =
-    new Query(
-      new OrderBy('datetimeStart', past ? ORDER_BY_DIRECTION.DESC : ORDER_BY_DIRECTION.ASC),
-      new PageFilter(past ? pastPage : upcomingPage, ref(5)),
-      new TimeFilter('datetimeStart', today, past ? TIME_STRATEGY.BEFORE : TIME_STRATEGY.AFTER)
-    )
-
-  return fetchCollection(Event, null, query)
-}
-
-/**
- * Action lorsque l'on souhaite démarrer l'essai
- */
-async function startTrial() {
-  try {
-    await apiRequestService.get('/trial/is_available')
-    await navigateTo(makeAdminUrl('trial'), {
-      external: true,
-    })
-  } catch (error) {
-    showDialogTrialAlReadyDid.value = true
-  }
-}
-
-/**
- * Nettoyage du store
- */
-onUnmounted(() => {
-  useRepo(Organization).flush()
-  useRepo(Event).flush()
-  useRepo(Country).flush()
-})
-
-</script>
-
-<style scoped lang="scss">
-
-.tabs-title{
-  margin-top: 20px;
-  padding-left: 20px;
-  background-color: rgb(var(--v-theme-neutral));
-  .v-tab--selected{
-    color: rgb(var(--v-theme-on-neutral--clickable));
-  }
-}
-
-
-.v-card {
-  margin-bottom: 16px;
-  color: rgb(var(--v-theme-on-primary-alt));
-}
-
-.v-card-text{
-  div{
-    line-height: 2;
-  }
-}
-
-.organization_title{
-  font-weight: 500;
-}
-
-.icon-hotel{
-  margin: 0 5px 4px 0;
-}
-
-.btn {
-  border: 1px solid;
-  cursor: pointer;
-}
-
-.inner-container {
-  margin: 0 auto;
-  padding: 30px;
-}
-.btn_trial {
-  height: 55px;
-  background-color: rgb(var(--v-theme-x-create-btn));
-  color: #000;
-
-  span {
-    text-align: center;
-    line-height: 1.2; /* optionnel : pour resserrer ou espacer */
-  }
-  .v-icon {
-    transform: rotate(135deg); /* angle en degrés */
-    font-size: 16px;
-    padding-right: 5px;
-    margin: 0 5px 4px 0;
-  }
-}
-
-.btn_edit_orga{
-  color: rgb(var(--v-theme-on-primary-alt)) !important;
-}
-
-.no_event{
-  padding: 25px;
-  font-size: 16px;
-}
-</style>

+ 2 - 5
pages/freemium/events/[id].vue

@@ -2,7 +2,7 @@
   <UiFormEdition
     class="inner-container"
     :model="Event"
-    go-back-route="/freemium/dashboard"
+    go-back-route="/freemium"
   >
     <template #default="{ entity }">
       <FormFreemiumEvent v-if="entity !== null" :entity="entity" />
@@ -11,18 +11,15 @@
 </template>
 
 <script setup lang="ts">
-import Event from "~/models/Freemium/Event";
+import Event from '~/models/Freemium/Event'
 
 definePageMeta({
   name: 'freemium_event_edit_page',
 })
-
 </script>
 
 <style scoped lang="scss">
-
 .inner-container {
   max-width: 1200px;
 }
-
 </style>

+ 2 - 5
pages/freemium/events/new.vue

@@ -2,7 +2,7 @@
   <UiFormCreation
     class="inner-container"
     :model="Event"
-    go-back-route="/freemium/dashboard"
+    go-back-route="/freemium"
   >
     <template #default="{ entity }">
       <FormFreemiumEvent :entity="entity" />
@@ -11,18 +11,15 @@
 </template>
 
 <script setup lang="ts">
-import Event from "~/models/Freemium/Event";
+import Event from '~/models/Freemium/Event'
 
 definePageMeta({
   name: 'freemium_event_create_page',
 })
-
 </script>
 
 <style scoped lang="scss">
-
 .inner-container {
   max-width: 1200px;
 }
-
 </style>

+ 316 - 0
pages/freemium/index.vue

@@ -0,0 +1,316 @@
+<template>
+  <div>
+    <v-container fluid class="inner-container">
+      <v-row>
+        <v-col cols="12" class="text-h6 text-uppercase font-weight-bold">{{
+          $t('welcome_freemium_title')
+        }}</v-col>
+        <!-- Bloc événements -->
+        <v-col cols="12" md="7">
+          <v-card>
+            <v-tabs v-model="tab" class="tabs-title">
+              <v-tab value="future" class="text-none">{{
+                $t('futur_event')
+              }}</v-tab>
+              <v-tab value="past" class="text-none">{{
+                $t('past_event')
+              }}</v-tab>
+            </v-tabs>
+
+            <v-tabs-window v-model="tab">
+              <v-tabs-window-item value="future">
+                <v-btn
+                  color="primary"
+                  to="freemium/events/new"
+                  class="ml-5 mt-5"
+                  >{{ $t('add_event') }}</v-btn
+                >
+
+                <div v-if="statusUpcomingEvents == FETCHING_STATUS.PENDING">
+                  <v-col cols="12" class="loader">
+                    <v-skeleton-loader
+                      v-for="i in 5"
+                      :key="i"
+                      class="mx-auto"
+                      type="avatar, subtitle"
+                    ></v-skeleton-loader>
+                  </v-col>
+                </div>
+
+                <span v-if="upcomingEvents?.items.length == 0" class="no_event">
+                  {{ $t('no_future_event') }}
+                </span>
+                <UiEventList
+                  v-if="
+                    statusUpcomingEvents == FETCHING_STATUS.SUCCESS &&
+                    upcomingEvents?.items
+                  "
+                  :events="upcomingEvents.items"
+                  :pagination="upcomingEvents.pagination"
+                  @load="loadUpcomingEvents"
+                  @edit="editEvent"
+                />
+              </v-tabs-window-item>
+              <v-tabs-window-item value="past">
+                <div v-if="statusPastEvents == FETCHING_STATUS.PENDING">
+                  <v-col cols="12" class="loader">
+                    <v-skeleton-loader
+                      v-for="i in 5"
+                      :key="i"
+                      class="mx-auto"
+                      type="avatar, subtitle"
+                    ></v-skeleton-loader>
+                  </v-col>
+                </div>
+                <span v-if="pastEvents?.items.length == 0" class="no_event">
+                  {{ $t('no_past_event') }}
+                </span>
+                <UiEventList
+                  v-if="
+                    statusPastEvents == FETCHING_STATUS.SUCCESS &&
+                    pastEvents?.items
+                  "
+                  :events="pastEvents.items"
+                  :pagination="pastEvents.pagination"
+                  @load="loadPastEvents"
+                  @edit="editEvent"
+                />
+              </v-tabs-window-item>
+            </v-tabs-window>
+          </v-card>
+        </v-col>
+
+        <!-- Bloc structure -->
+        <v-col cols="12" md="5">
+          <v-card
+            v-if="statusOrganization == FETCHING_STATUS.SUCCESS"
+            class="pa-5"
+          >
+            <v-card-title class="text-h6">
+              <v-icon icon="fa fa-hotel" class="text-button icon-hotel" />
+              <span class="organization_title">{{
+                $t('my_organization')
+              }}</span>
+            </v-card-title>
+            <v-card-text>
+              <div>
+                <strong>{{ $t('name') }} :</strong> {{ organization?.name }}
+              </div>
+              <div>
+                <strong>{{ $t('email') }} :</strong> {{ organization?.email }}
+              </div>
+            </v-card-text>
+          </v-card>
+
+          <v-btn
+            block
+            prepend-icon="fa-solid fa-pen"
+            class="my-5 text-black"
+            to="freemium/organization"
+          >
+            {{ $t('edit_organization') }}
+          </v-btn>
+
+          <v-btn block class="text-black btn btn_trial" @click="startTrial">
+            <span
+              ><v-icon icon="fa fa-ticket" /> {{ $t('try_premium_light')
+              }}<br />
+              {{ $t('30_days_free') }}</span
+            >
+          </v-btn>
+        </v-col>
+      </v-row>
+    </v-container>
+
+    <LayoutDialogTrialAlreadyDid
+      :show="showDialogTrialAlReadyDid"
+      @close-dialog="showDialogTrialAlReadyDid = false"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import Query from '~/services/data/Query'
+
+import { type Ref, ref } from 'vue'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import Organization from '~/models/Freemium/Organization'
+import Event from '~/models/Freemium/Event'
+import OrderBy from '~/services/data/Filters/OrderBy'
+import {
+  FETCHING_STATUS,
+  ORDER_BY_DIRECTION,
+  TIME_STRATEGY,
+} from '~/types/enum/data'
+import PageFilter from '~/services/data/Filters/PageFilter'
+import TimeFilter from '~/services/data/Filters/TimeFilter'
+import Country from '~/models/Core/Country'
+import DateUtils from '~/services/utils/dateUtils'
+import UrlUtils from '~/services/utils/urlUtils'
+import { useApiLegacyRequestService } from '~/composables/data/useApiLegacyRequestService'
+import { useAdminUrl } from '~/composables/utils/useAdminUrl'
+
+definePageMeta({
+  name: 'freemium_dashboard_page',
+})
+
+//Ref Définition
+const { fetch, fetchCollection } = useEntityFetch()
+const { apiRequestService } = useApiLegacyRequestService()
+const { makeAdminUrl } = useAdminUrl()
+const tab = ref(null)
+const upcomingPage = ref(1)
+const pastPage = ref(1)
+const showDialogTrialAlReadyDid: Ref<boolean> = ref(false)
+
+//Fetch
+const { data: organization, status: statusOrganization } = fetch(Organization)
+const {
+  data: upcomingEvents,
+  status: statusUpcomingEvents,
+  refresh: refreshUpcomingEvents,
+} = fetchEvents()
+const {
+  data: pastEvents,
+  status: statusPastEvents,
+  refresh: refreshPastEvents,
+} = fetchEvents(true)
+
+/**
+ * Charge une page des événements à venir
+ * @param pageNumber
+ */
+function loadUpcomingEvents(pageNumber: number) {
+  upcomingPage.value = pageNumber
+  refreshUpcomingEvents()
+}
+
+/**
+ * Cahrge une page des événements passées
+ * @param pageNumber
+ */
+function loadPastEvents(pageNumber: number) {
+  pastPage.value = pageNumber
+  refreshPastEvents()
+}
+
+/**
+ * Redirige vers la page d'édition d'un événement
+ * @param eventId
+ */
+function editEvent(eventId: number) {
+  navigateTo(UrlUtils.join('freemium/events', eventId))
+}
+
+/**
+ * Récupère la liste des événements
+ * @param past
+ */
+function fetchEvents(past: boolean = false) {
+  const today = computed(() => DateUtils.formatIsoShortDate(new Date()))
+  const query = new Query(
+    new OrderBy(
+      'datetimeStart',
+      past ? ORDER_BY_DIRECTION.DESC : ORDER_BY_DIRECTION.ASC,
+    ),
+    new PageFilter(past ? pastPage : upcomingPage, ref(5)),
+    new TimeFilter(
+      'datetimeStart',
+      today,
+      past ? TIME_STRATEGY.BEFORE : TIME_STRATEGY.AFTER,
+    ),
+  )
+
+  return fetchCollection(Event, null, query)
+}
+
+/**
+ * Action lorsque l'on souhaite démarrer l'essai
+ */
+async function startTrial() {
+  try {
+    await apiRequestService.get('/trial/is_available')
+    await navigateTo(makeAdminUrl('trial'), {
+      external: true,
+    })
+  } catch (error) {
+    showDialogTrialAlReadyDid.value = true
+  }
+}
+
+/**
+ * Nettoyage du store
+ */
+onUnmounted(() => {
+  useRepo(Organization).flush()
+  useRepo(Event).flush()
+  useRepo(Country).flush()
+})
+</script>
+
+<style scoped lang="scss">
+.tabs-title {
+  padding-left: 20px;
+  background-color: rgb(var(--v-theme-neutral-soft));
+  .v-tab--selected {
+    color: rgb(var(--v-theme-neutral-soft--sub));
+  }
+}
+
+.v-card {
+  margin-bottom: 16px;
+  color: rgb(var(--v-theme-on-primary-alt));
+}
+
+.v-card-text {
+  div {
+    line-height: 2;
+  }
+}
+
+.organization_title {
+  font-weight: 500;
+}
+
+.icon-hotel {
+  margin: 0 5px 4px 0;
+}
+
+.btn {
+  border: 1px solid;
+  cursor: pointer;
+}
+
+.inner-container {
+  margin: 0 auto;
+  padding: 30px;
+}
+.btn_trial {
+  height: 55px;
+  background-color: rgb(var(--v-theme-x-create-btn));
+  color: #000;
+
+  span {
+    text-align: center;
+    line-height: 1.2; /* optionnel : pour resserrer ou espacer */
+  }
+  .v-icon {
+    transform: rotate(135deg); /* angle en degrés */
+    font-size: 16px;
+    padding-right: 5px;
+    margin: 0 5px 4px 0;
+  }
+}
+
+.no_event {
+  padding: 25px;
+  font-size: 16px;
+  display: block;
+}
+.loader {
+  height: 500px;
+  .v-skeleton-loader {
+    margin-bottom: 20px;
+  }
+}
+</style>

+ 160 - 111
pages/freemium/organization.vue

@@ -1,115 +1,167 @@
 <template>
-      <UiFormEdition :model="Organization"
-                     class="inner-container"
-      >
-        <template #default="{ entity : organization }">
-          <div v-if="organization">
-            <LayoutCommonSection>
-              <v-row>
-                <v-col cols="12">
-                  <h4 class="mb-8">{{ $t('general_informations') }}</h4>
-
-                  <UiInputText v-model="organization.name" field="name" :rules="getAsserts('name')" />
-
-                  <UiInputTextArea v-model="organization.description" field="description"  />
-
-                  <UiInputImage
-                    v-model="organization.logo"
-                    field="logo"
-                    :width="120"
-                    :cropping-enabled="true"
-                  />
-                </v-col>
-
-              </v-row>
-            </LayoutCommonSection>
-
-            <LayoutCommonSection>
-              <v-row>
-                <v-col cols="12">
-                  <h4 class="mb-8">{{ $t('coordinate') }}</h4>
-
-                  <UiInputText v-model="organization.email" field="email" :rules="getAsserts('email')" />
-
-                  <UiInputPhone v-model="organization.tel" field="tel"/>
-
-                </v-col>
-              </v-row>
-            </LayoutCommonSection>
-
-            <LayoutCommonSection>
-              <v-row>
-                <v-col cols="12">
-                  <h4 class="mb-8">{{ $t('postal_address') }}</h4>
-
-                  <UiInputText v-model="organization.streetAddress" field="streetAddress" />
-
-                  <UiInputText v-model="organization.streetAddressSecond" field="streetAddressSecond" />
-
-                  <UiInputText v-model="organization.streetAddressThird" field="streetAddressThird" />
-
-                  <UiInputText v-model="organization.postalCode" field="postalCode" />
-
-                  <UiInputText v-model="organization.addressCity" field="addressCity" />
-
-                  <UiInputAutocompleteApiResources
-                    v-model="organization.addressCountry"
-                    field="addressCountry"
-                    :model="Country"
-                    listValue="id"
-                    listLabel="name"
-                  />
-
-                  <client-only>
-                    <UiMapLeaflet
-                      v-model:latitude="organization.latitude"
-                      v-model:longitude="organization.longitude"
-                      :streetAddress="organization.streetAddress"
-                      :streetAddressSecond="organization.streetAddressSecond"
-                      :streetAddressThird="organization.streetAddressThird"
-                      :postalCode="organization.postalCode"
-                      :addressCity="organization.addressCity"
-                      :addressCountryId="organization.addressCountry"
-                      :searchButton="true"
-                    ></UiMapLeaflet>
-                  </client-only>
-
-                </v-col>
-              </v-row>
-            </LayoutCommonSection>
-
-            <LayoutCommonSection>
-              <v-row>
-                <v-col cols="12">
-
-                  <h4 class="mb-8">{{ $t('communication_params') }}</h4>
-
-                  <UiInputText v-model="organization.facebook" field="facebook" />
-
-                  <UiInputText v-model="organization.twitter" field="twitter" />
-
-                  <UiInputText v-model="organization.youtube" field="youtube" />
-
-                  <UiInputText v-model="organization.instagram" field="instagram" />
-
-                  <UiInputCheckbox
-                    v-model="organization.portailVisibility"
-                    field="portailVisibility"
-                  />
-                </v-col>
-              </v-row>
-            </LayoutCommonSection>
-          </div>
-        </template>
-      </UiFormEdition>
-
+  <UiFormEdition :model="Organization" class="inner-container">
+    <template #default="{ entity: organization }">
+      <div v-if="organization">
+        <LayoutCommonSection>
+          <v-row>
+            <v-col cols="12">
+              <h4 class="mb-8">{{ $t('general_informations') }}</h4>
+
+              <UiInputText
+                v-model="organization.name"
+                field="name"
+                :rules="getAsserts('name')"
+              />
+
+              <UiInputAutocompleteEnum
+                v-model="organization.legalStatus"
+                enum-name="organization_legal"
+                field="legalStatus"
+              />
+
+              <UiInputAutocompleteEnum
+                v-model="organization.principalType"
+                enum-name="organization_principal_type_short_list"
+                field="principalType"
+              />
+
+              <UiInputTreeSelectTypeOfPractices
+                v-model="organization.typeOfPractices"
+                label="type_of_practices"
+              />
+
+              <UiInputTextArea
+                v-model="organization.description"
+                field="description"
+              />
+
+              <UiInputImage
+                v-model="organization.logo"
+                field="logo"
+                label="organization_logo"
+                :width="120"
+                :cropping-enabled="true"
+              />
+            </v-col>
+          </v-row>
+        </LayoutCommonSection>
+
+        <LayoutCommonSection>
+          <v-row>
+            <v-col cols="12">
+              <h4 class="mb-8">{{ $t('coordinate') }}</h4>
+
+              <UiInputText
+                v-model="organization.email"
+                field="email"
+                :rules="getAsserts('email')"
+              />
+
+              <UiInputPhone v-model="organization.tel" field="tel" />
+            </v-col>
+          </v-row>
+        </LayoutCommonSection>
+
+        <LayoutCommonSection>
+          <v-row>
+            <v-col cols="12">
+              <h4 class="mb-8">{{ $t('postal_address') }}</h4>
+
+              <UiInputText
+                v-model="organization.streetAddress"
+                field="streetAddress"
+              />
+
+              <UiInputText
+                v-model="organization.streetAddressSecond"
+                field="streetAddressSecond"
+              />
+
+              <UiInputText
+                v-model="organization.streetAddressThird"
+                field="streetAddressThird"
+              />
+
+              <UiInputText
+                v-model="organization.postalCode"
+                field="postalCode"
+              />
+
+              <UiInputText
+                v-model="organization.addressCity"
+                field="addressCity"
+              />
+
+              <UiInputAutocompleteApiResources
+                v-model="organization.addressCountry"
+                field="addressCountry"
+                :model="Country"
+                list-value="id"
+                list-label="name"
+              />
+
+              <client-only>
+                <UiMapLeaflet
+                  v-model:latitude="organization.latitude"
+                  v-model:longitude="organization.longitude"
+                  :street-address="organization.streetAddress"
+                  :street-address-second="organization.streetAddressSecond"
+                  :street-address-third="organization.streetAddressThird"
+                  :postal-code="organization.postalCode"
+                  :address-city="organization.addressCity"
+                  :address-country-id="organization.addressCountry"
+                  :search-button="true"
+                ></UiMapLeaflet>
+              </client-only>
+            </v-col>
+          </v-row>
+        </LayoutCommonSection>
+
+        <LayoutCommonSection>
+          <v-row>
+            <v-col cols="12">
+              <h4 class="mb-8">{{ $t('communication_params') }}</h4>
+
+              <UiInputText
+                v-model="organization.facebook"
+                field="facebook"
+                :rules="getAsserts('facebook')"
+              />
+
+              <UiInputText
+                v-model="organization.twitter"
+                field="twitter"
+                :rules="getAsserts('twitter')"
+              />
+
+              <UiInputText
+                v-model="organization.youtube"
+                field="youtube"
+                :rules="getAsserts('youtube')"
+              />
+
+              <UiInputText
+                v-model="organization.instagram"
+                field="instagram"
+                :rules="getAsserts('instagram')"
+              />
+
+              <UiInputCheckbox
+                v-model="organization.portailVisibility"
+                field="portailVisibility"
+              />
+            </v-col>
+          </v-row>
+        </LayoutCommonSection>
+      </div>
+    </template>
+  </UiFormEdition>
 </template>
 
 <script setup lang="ts">
-
-import Organization from "~/models/Freemium/Organization";
-import {getAssertUtils} from "~/services/asserts/getAssertUtils";
-import Country from "~/models/Core/Country";
+import Organization from '~/models/Freemium/Organization'
+import { getAssertUtils } from '~/services/asserts/getAssertUtils'
+import Country from '~/models/Core/Country'
 
 definePageMeta({
   name: 'freemium_organization_page',
@@ -121,13 +173,10 @@ onUnmounted(() => {
 })
 
 const getAsserts = (key) => getAssertUtils(Organization.getAsserts(), key)
-
 </script>
 
 <style scoped lang="scss">
-
 .inner-container {
   max-width: 1200px;
 }
-
 </style>

+ 5 - 3
pages/my-settings.vue

@@ -8,8 +8,11 @@ Page 'Mes préférences'
         <UiExpansionPanel title="message_settings" icon="fas fa-inbox">
           <v-container fluid class="container">
             <v-row>
-              <UiFormEdition :model="Preferences" :id="accessProfileStore.preferencesId">
-                <template #default="{ entity : preferences }">
+              <UiFormEdition
+                :id="accessProfileStore.preferencesId"
+                :model="Preferences"
+              >
+                <template #default="{ entity: preferences }">
                   <div v-if="preferences">
                     <v-row>
                       <v-col cols="12">
@@ -46,7 +49,6 @@ if (accessProfileStore.preferencesId === null) {
 }
 
 const openedPanels: Ref<Array<number>> = ref([0])
-
 </script>
 
 <style scoped lang="scss"></style>

+ 4 - 2
pages/parameters/attendance_booking_reasons/[id].vue

@@ -6,7 +6,10 @@
         go-back-route="/parameters/attendances"
       >
         <template #default="{ entity }">
-          <FormParameterAttendanceBookingReason v-if="entity !== null" :entity="entity" />
+          <FormParameterAttendanceBookingReason
+            v-if="entity !== null"
+            :entity="entity"
+          />
         </template>
       </UiFormEdition>
     </LayoutCommonSection>
@@ -18,5 +21,4 @@ import AttendanceBookingReason from '~/models/Booking/AttendanceBookingReason'
 definePageMeta({
   name: 'attendanceBookingReason',
 })
-
 </script>

+ 0 - 1
pages/parameters/attendance_booking_reasons/new.vue

@@ -19,5 +19,4 @@ import AttendanceBookingReason from '~/models/Booking/AttendanceBookingReason'
 definePageMeta({
   name: 'new_attendance_booking_reason',
 })
-
 </script>

+ 4 - 6
pages/parameters/attendances.vue

@@ -2,12 +2,11 @@
   <div>
     <LayoutCommonSection>
       <h4>{{ $t('configuration') }}</h4>
-      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
-        <template #default="{ entity : parameters }">
+      <UiFormEdition :id="organizationProfile.parametersId" :model="Parameters">
+        <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"
@@ -60,9 +59,9 @@
 </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";
+import { getAssertUtils } from '~/services/asserts/getAssertUtils'
 
 definePageMeta({
   name: 'parameters_attendances_page',
@@ -75,5 +74,4 @@ if (organizationProfile.parametersId === null) {
 }
 
 const getAsserts = (key) => getAssertUtils(Parameters.getAsserts(), key)
-
 </script>

+ 2 - 3
pages/parameters/bulletin.vue

@@ -1,8 +1,8 @@
 <template>
   <LayoutContainer>
     <LayoutCommonSection>
-      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
-        <template #default="{ entity : parameters }">
+      <UiFormEdition :id="organizationProfile.parametersId" :model="Parameters">
+        <template #default="{ entity: parameters }">
           <div v-if="parameters">
             <v-row>
               <v-col cols="12">
@@ -83,7 +83,6 @@ definePageMeta({
   name: 'parameters_bulletin_page',
 })
 
-
 const organizationProfile = useOrganizationProfileStore()
 
 if (organizationProfile.parametersId === null) {

+ 1 - 2
pages/parameters/cycles/[id].vue

@@ -16,12 +16,11 @@
 </template>
 <script setup lang="ts">
 import Cycle from '~/models/Education/Cycle'
-import {getAssertUtils} from "~/services/asserts/getAssertUtils";
+import { getAssertUtils } from '~/services/asserts/getAssertUtils'
 
 definePageMeta({
   name: 'cycle',
 })
 
 const getAsserts = (key) => getAssertUtils(Cycle.getAsserts(), key)
-
 </script>

+ 5 - 5
pages/parameters/education_notation.vue

@@ -1,8 +1,8 @@
 <template>
   <LayoutContainer>
     <LayoutCommonSection>
-      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
-        <template #default="{ entity : parameters }">
+      <UiFormEdition :id="organizationProfile.parametersId" :model="Parameters">
+        <template #default="{ entity: parameters }">
           <div v-if="parameters">
             <v-row>
               <v-col cols="12">
@@ -25,7 +25,9 @@
                 />
 
                 <UiInputAutocompleteEnum
-                  v-if="organizationProfile.hasModule('AdvancedEducationNotation')"
+                  v-if="
+                    organizationProfile.hasModule('AdvancedEducationNotation')
+                  "
                   v-model="parameters.advancedEducationNotationType"
                   enum-name="advanced_education_notation"
                   field="advancedEducationNotationType"
@@ -53,7 +55,6 @@
       </UiFormEdition>
     </LayoutCommonSection>
   </LayoutContainer>
-
 </template>
 
 <script setup lang="ts">
@@ -69,7 +70,6 @@ const organizationProfile = useOrganizationProfileStore()
 if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
-
 </script>
 
 <style scoped lang="scss"></style>

+ 4 - 2
pages/parameters/education_timings/[id].vue

@@ -6,7 +6,10 @@
         go-back-route="/parameters/education_timings"
       >
         <template #default="{ entity }">
-          <FormParameterEducationTiming v-if="entity !== null" :entity="entity" />
+          <FormParameterEducationTiming
+            v-if="entity !== null"
+            :entity="entity"
+          />
         </template>
       </UiFormEdition>
     </LayoutCommonSection>
@@ -18,5 +21,4 @@ import EducationTiming from '~/models/Education/EducationTiming'
 definePageMeta({
   name: 'educationTiming',
 })
-
 </script>

+ 0 - 1
pages/parameters/education_timings/index.vue

@@ -22,7 +22,6 @@ const organizationProfile = useOrganizationProfileStore()
 if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
-
 </script>
 
 <style scoped lang="scss"></style>

+ 0 - 1
pages/parameters/education_timings/new.vue

@@ -19,5 +19,4 @@ import EducationTiming from '~/models/Education/EducationTiming'
 definePageMeta({
   name: 'new_education_timing',
 })
-
 </script>

+ 11 - 12
pages/parameters/general_parameters.vue

@@ -1,16 +1,16 @@
 <template>
   <LayoutContainer>
     <LayoutCommonSection>
-      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
-        <template #default="{ entity : parameters }">
+      <UiFormEdition :id="organizationProfile.parametersId" :model="Parameters">
+        <template #default="{ entity: parameters }">
           <div v-if="parameters">
             <v-row>
               <v-col cols="12">
                 <UiInputDatePicker
                   v-if="
-                organizationProfile.isSchool ||
-                organizationProfile.isManagerProduct
-              "
+                    organizationProfile.isSchool ||
+                    organizationProfile.isManagerProduct
+                  "
                   v-model="parameters.financialDate"
                   field="financialDate"
                   label="start_date_of_financial_season"
@@ -59,9 +59,9 @@
 
                 <UiInputCheckbox
                   v-if="
-                organizationProfile.isSchool &&
-                organizationProfile.isAssociation
-              "
+                    organizationProfile.isSchool &&
+                    organizationProfile.isAssociation
+                  "
                   v-model="parameters.studentsAreAdherents"
                   field="studentsAreAdherents"
                   label="students_are_also_association_members"
@@ -71,9 +71,9 @@
                   v-if="organizationProfile.isCMFCentralService"
                   class="d-flex flex-column"
                 >
-              <span class="mb-1 v-label" style="font-size: 12px"
-              >{{ $t('licenceQrCode') }}
-              </span>
+                  <span class="mb-1 v-label" style="font-size: 12px"
+                    >{{ $t('licenceQrCode') }}
+                  </span>
                   <UiInputImage
                     v-model="parameters.qrCode"
                     field="qrCode"
@@ -103,7 +103,6 @@ const organizationProfile = useOrganizationProfileStore()
 if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
-
 </script>
 
 <style scoped lang="scss"></style>

+ 2 - 3
pages/parameters/intranet.vue

@@ -1,8 +1,8 @@
 <template>
   <LayoutContainer>
     <LayoutCommonSection>
-      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
-        <template #default="{ entity : parameters }">
+      <UiFormEdition :id="organizationProfile.parametersId" :model="Parameters">
+        <template #default="{ entity: parameters }">
           <div v-if="parameters">
             <v-row>
               <v-col cols="12">
@@ -66,7 +66,6 @@ const organizationProfile = useOrganizationProfileStore()
 if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
-
 </script>
 
 <style scoped lang="scss"></style>

+ 0 - 1
pages/parameters/residence_areas/[id].vue

@@ -19,5 +19,4 @@ import ResidenceArea from '~/models/Billing/ResidenceArea'
 definePageMeta({
   name: 'edit_resident_area',
 })
-
 </script>

+ 0 - 1
pages/parameters/residence_areas/index.vue

@@ -15,7 +15,6 @@ import ResidenceArea from '~/models/Billing/ResidenceArea'
 definePageMeta({
   name: 'parameters_residence_areas_page',
 })
-
 </script>
 
 <style scoped lang="scss"></style>

+ 0 - 1
pages/parameters/residence_areas/new.vue

@@ -19,5 +19,4 @@ import ResidenceArea from '~/models/Billing/ResidenceArea'
 definePageMeta({
   name: 'create_a_new_residence_area',
 })
-
 </script>

+ 13 - 10
pages/parameters/sms.vue

@@ -1,12 +1,15 @@
 <template>
   <div>
     <LayoutCommonSection>
-      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
-        <template #default="{ entity : parameters }">
+      <UiFormEdition :id="organizationProfile.parametersId" :model="Parameters">
+        <template #default="{ entity: parameters }">
           <div v-if="parameters">
             <v-row>
               <v-col cols="12">
-                <UiInputText v-model="parameters.usernameSMS" field="usernameSMS" />
+                <UiInputText
+                  v-model="parameters.usernameSMS"
+                  field="usernameSMS"
+                />
               </v-col>
               <v-col cols="12">
                 <UiInputText
@@ -17,7 +20,8 @@
               </v-col>
               <v-col cols="12">
                 <div class="mb-3">
-                  {{ $t('smsSenderName') }} : <b>{{ parameters.smsSenderName }}</b>
+                  {{ $t('smsSenderName') }} :
+                  <b>{{ parameters.smsSenderName }}</b>
                 </div>
               </v-col>
             </v-row>
@@ -29,10 +33,10 @@
         <v-btn
           class="theme-info btn"
           :href="
-                runtimeConfig.public.fileStorageBaseUrl +
-                '/Bon_de_commande/' +
-                (organizationProfile.isCmf ? 'SMS_CMF.pdf' : 'SMS_Public.pdf')
-              "
+            runtimeConfig.public.fileStorageBaseUrl +
+            '/Bon_de_commande/' +
+            (organizationProfile.isCmf ? 'SMS_CMF.pdf' : 'SMS_Public.pdf')
+          "
           target="_blank"
         >
           {{ $t('buy_more_sms_credits') }}
@@ -42,9 +46,9 @@
   </div>
 </template>
 <script setup lang="ts">
-
 import Parameters from '~/models/Organization/Parameters'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
 
 definePageMeta({
   name: 'parameters_sms_page',
@@ -58,7 +62,6 @@ const organizationProfile = useOrganizationProfileStore()
 if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
-
 </script>
 
 <style scoped lang="scss">

+ 1 - 1
pages/parameters/subdomains/[id].vue

@@ -45,7 +45,7 @@ import { usePageStore } from '~/stores/page'
 import { TYPE_ALERT } from '~/types/enum/enums'
 import { useRefreshProfile } from '~/composables/data/useRefreshProfile'
 import { useRouteUtils } from '~/composables/utils/useRouteUtils'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 definePageMeta({
   name: 'activate_a_subdomain',

+ 8 - 10
pages/parameters/super_admin.vue

@@ -10,17 +10,17 @@
         </div>
       </div>
 
-      <UiFormEdition :model="AdminAccess" :id="accessProfile.id" class="w-100">
-        <template #default="{ entity : adminAccess }">
+      <UiFormEdition :id="accessProfile.id" :model="AdminAccess" class="w-100">
+        <template #default="{ entity: adminAccess }">
           <div v-if="adminAccess">
             <v-table class="mb-4">
               <tbody>
-              <tr>
-                <td>{{ $t('username') }} :</td>
-                <td>
-                  <b>{{ adminAccess.username }}</b>
-                </td>
-              </tr>
+                <tr>
+                  <td>{{ $t('username') }} :</td>
+                  <td>
+                    <b>{{ adminAccess.username }}</b>
+                  </td>
+                </tr>
               </tbody>
             </v-table>
 
@@ -33,7 +33,6 @@
           </div>
         </template>
       </UiFormEdition>
-
     </LayoutCommonSection>
   </div>
 </template>
@@ -50,7 +49,6 @@ const accessProfile = useAccessProfileStore()
 if (accessProfile.id === null) {
   throw new Error('Missing access profile id')
 }
-
 </script>
 
 <style scoped lang="scss">

+ 7 - 5
pages/parameters/teaching.vue

@@ -3,8 +3,8 @@
     <LayoutCommonSection>
       <h4>{{ $t('configuration') }}</h4>
 
-      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
-        <template #default="{ entity : parameters }">
+      <UiFormEdition :id="organizationProfile.parametersId" :model="Parameters">
+        <template #default="{ entity: parameters }">
           <div v-if="parameters">
             <v-row>
               <v-col cols="12">
@@ -45,7 +45,7 @@ import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 import type { AnyJson } from '~/types/data'
 import { useEnumFetch } from '~/composables/data/useEnumFetch'
 import { TABLE_ACTION } from '~/types/enum/enums'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 definePageMeta({
   name: 'parameters_teaching_page',
@@ -65,7 +65,9 @@ const { data: cycleEnum, status: enumStatus } = fetchEnum('education_cycle')
 const { data: cycles, status: cyclesStatus } = fetchCollection(Cycle)
 
 const pending: ComputedRef<boolean> = computed(
-  () => enumStatus.value == FETCHING_STATUS.PENDING || cyclesStatus.value == FETCHING_STATUS.PENDING,
+  () =>
+    enumStatus.value == FETCHING_STATUS.PENDING ||
+    cyclesStatus.value == FETCHING_STATUS.PENDING,
 )
 
 const orderedCycles: ComputedRef<AnyJson> = computed(() => {
@@ -111,7 +113,7 @@ const goToCycleEditPage = (item: object) => {
 // Nettoyer les données lors du démontage du composant
 onBeforeUnmount(() => {
   // Nettoyer les références du store si nécessaire
-  if (process.client) {
+  if (import.meta.client) {
     clearNuxtData('/^' + Cycle.entity + '_many_/')
     useRepo(Cycle).flush()
   }

+ 1 - 1
pages/parameters/website.vue

@@ -130,7 +130,7 @@ import Subdomain from '~/models/Organization/Subdomain'
 import type ApiResource from '~/models/ApiResource'
 import EqualFilter from '~/services/data/Filters/EqualFilter'
 import Query from '~/services/data/Query'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 definePageMeta({
   name: 'parameters_website_page',

+ 1 - 1
pages/subscription.vue

@@ -485,7 +485,7 @@ import { useApiLegacyRequestService } from '~/composables/data/useApiLegacyReque
 import { usePageStore } from '~/stores/page'
 import { DOLIBARR_BILLING_DOC_TYPE } from '~/types/enum/enums'
 import LayoutMobytStatus from '~/components/Layout/MobytStatus.vue'
-import {FETCHING_STATUS} from "~/types/enum/data";
+import { FETCHING_STATUS } from '~/types/enum/data'
 
 // meta
 definePageMeta({

+ 6 - 6
plugins/vPhoneInput.ts

@@ -1,11 +1,11 @@
-import 'flag-icons/css/flag-icons.min.css';
-import 'v-phone-input/dist/v-phone-input.css';
-import { createVPhoneInput } from 'v-phone-input';
+import 'flag-icons/css/flag-icons.min.css'
+import 'v-phone-input/dist/v-phone-input.css'
+import { createVPhoneInput } from 'v-phone-input'
 
 export default defineNuxtPlugin((nuxtApp) => {
   const vPhoneInput = createVPhoneInput({
     countryIconMode: 'svg',
-  });
+  })
 
-  nuxtApp.vueApp.use(vPhoneInput);
-});
+  nuxtApp.vueApp.use(vPhoneInput)
+})

+ 16 - 12
services/asserts/AssertRuleRegistry.ts

@@ -1,31 +1,35 @@
-import type {AssertRule} from "~/types/interfaces";
-import { MaxAssert } from './MaxAssert';
-import { NullableAssert } from './NullableAssert';
-import { TypeAssert } from './TypeAssert';
+import type { AssertRule } from '~/types/interfaces'
+import { MaxAssert } from './MaxAssert'
+import { NullableAssert } from './NullableAssert'
+import { TypeAssert } from './TypeAssert'
+import { PositiveAssert } from './PositiveAssert'
 
 export class AssertRuleRegistry {
-  private rules: AssertRule[] = [];
+  private rules: AssertRule[] = []
 
   constructor() {
     this.rules = [
       new MaxAssert(),
       new NullableAssert(),
       new TypeAssert(),
-    ];
+      new PositiveAssert(),
+    ]
   }
 
-  getValidators(asserts: Record<string, any>): ((value: any) => true | string)[] {
-    const allRules: ((value: any) => true | string)[] = [];
+  getValidators(
+    asserts: Record<string, unknown>,
+  ): ((value: unknown) => true | string)[] {
+    const allRules: ((value: unknown) => true | string)[] = []
 
     for (const key in asserts) {
-      const criteria = asserts[key];
+      const criteria = asserts[key]
 
-      const rule = this.rules.find(r => r.supports(key));
+      const rule = this.rules.find((r) => r.supports(key))
       if (rule) {
-        allRules.push(rule.createRule(criteria));
+        allRules.push(rule.createRule(criteria))
       }
     }
 
-    return allRules;
+    return allRules
   }
 }

+ 7 - 5
services/asserts/MaxAssert.ts

@@ -1,12 +1,14 @@
-import type {AssertRule} from "~/types/interfaces";
+import type { AssertRule } from '~/types/interfaces'
 
 export class MaxAssert implements AssertRule {
   supports(key: string): boolean {
-    return key === 'max';
+    return key === 'max'
   }
 
-  createRule(criteria: number): (value: string) => true | string {
-    return (value: string) =>
-      value === null || value.length <= criteria || `Maximum ${criteria} caractères`;
+  createRule(criteria: number): (value: unknown) => true | string {
+    return (value: unknown) =>
+      value === null ||
+      value.length <= criteria ||
+      `Maximum ${criteria} caractères`
   }
 }

+ 9 - 8
services/asserts/NullableAssert.ts

@@ -1,15 +1,16 @@
-import type {AssertRule} from "~/types/interfaces";
-import { useI18n } from 'vue-i18n';
+import type { AssertRule } from '~/types/interfaces'
+import { useI18n } from 'vue-i18n'
 
 export class NullableAssert implements AssertRule {
   supports(key: string): boolean {
-    return key === 'nullable';
+    return key === 'nullable'
   }
 
-  createRule(criteria: boolean): (value: any) => true | string {
-
-    const { t } = useI18n();
-    return (value: any) =>
-      !criteria ? value !== null && !!value || t('please_enter_a_value') : true
+  createRule(criteria: boolean): (value: unknown) => true | string {
+    const { t } = useI18n()
+    return (value: unknown) =>
+      !criteria
+        ? (value !== null && !!value) || t('please_enter_a_value')
+        : true
   }
 }

+ 22 - 0
services/asserts/PositiveAssert.ts

@@ -0,0 +1,22 @@
+import type { AssertRule } from '~/types/interfaces'
+import { useI18n } from 'vue-i18n'
+
+export class PositiveAssert implements AssertRule {
+  supports(key: string): boolean {
+    return key === 'positive'
+  }
+
+  createRule(criteria: string): (value: unknown) => true | string {
+    const { t } = useI18n()
+
+    if (criteria === 'positive') {
+      return (value: number) =>
+        value === null || value > 0 || t(`must_be_positive`)
+    } else if (criteria === 'positive_or_zero') {
+      return (value: number) =>
+        value === null || value >= 0 || t(`must_be_positive_or_egal_0`)
+    }
+
+    return () => true
+  }
+}

+ 19 - 11
services/asserts/TypeAssert.ts

@@ -1,26 +1,34 @@
-import type {AssertRule} from "~/types/interfaces";
-import { useI18n } from 'vue-i18n';
-import ValidationUtils from "~/services/utils/validationUtils";
+import type { AssertRule } from '~/types/interfaces'
+import { useI18n } from 'vue-i18n'
+import ValidationUtils from '~/services/utils/validationUtils'
 
 export class TypeAssert implements AssertRule {
   supports(key: string): boolean {
-    return key === 'type';
+    return key === 'type'
   }
 
-  createRule(criteria: string): (value: any) => true | string {
+  createRule(criteria: string): (value: unknown) => true | string {
     const validationUtils = new ValidationUtils()
-    const { t } = useI18n();
+    const { t } = useI18n()
 
     if (criteria === 'email') {
-      return (email: string) =>
-        validationUtils.validEmail(email) || t('email_error');
+      return (email: unknown) =>
+        (typeof email === 'string' && validationUtils.validEmail(email)) ||
+        t('email_error')
+    }
+
+    if (criteria === 'url') {
+      return (url: unknown) =>
+        url === null ||
+        (typeof url === 'string' && validationUtils.validUrl(url)) ||
+        t('url_error')
     }
 
     if (criteria === 'integer') {
-      return (value: any) =>
-        Number.isInteger(value) || t('need_to_be_integer');
+      return (value: unknown) =>
+        Number.isInteger(value as number) || t('need_to_be_integer')
     }
 
-    return () => true;
+    return () => true
   }
 }

+ 5 - 5
services/asserts/getAssertUtils.ts

@@ -1,8 +1,8 @@
-import { AssertRuleRegistry } from './AssertRuleRegistry';
+import { AssertRuleRegistry } from './AssertRuleRegistry'
 
-export function getAssertUtils(asserts: Record<string, any>, key: string) {
-  if (!asserts || !(key in asserts)) return [];
+export function getAssertUtils(asserts: Record<string, unknown>, key: string) {
+  if (!asserts || !(key in asserts)) return []
 
-  const registry = new AssertRuleRegistry();
-  return registry.getValidators(asserts[key]);
+  const registry = new AssertRuleRegistry()
+  return registry.getValidators(asserts[key])
 }

+ 1 - 1
services/data/Filters/EqualFilter.ts

@@ -24,7 +24,7 @@ export default class EqualFilter extends AbstractFilter implements ApiFilter {
     this.filterValue = value
   }
 
-  public applyToPiniaOrmQuery <T extends ApiResource>(
+  public applyToPiniaOrmQuery<T extends ApiResource>(
     query: PiniaOrmQuery<T>,
   ): PiniaOrmQuery<T> {
     const filterValue = RefUtils.castToRef(

+ 2 - 2
services/data/Filters/InArrayFilter.ts

@@ -30,7 +30,7 @@ export default class InArrayFilter extends AbstractFilter implements ApiFilter {
     this.filterValue = value
   }
 
-  public applyToPiniaOrmQuery <T extends ApiResource>(
+  public applyToPiniaOrmQuery<T extends ApiResource>(
     query: PiniaOrmQuery<T>,
   ): PiniaOrmQuery<T> {
     const filterValue = RefUtils.castToRef(
@@ -59,7 +59,7 @@ export default class InArrayFilter extends AbstractFilter implements ApiFilter {
       filterValue.value = [filterValue.value]
     }
 
-    filterValue.value = filterValue.value.filter((value)=> value !== null)
+    filterValue.value = filterValue.value.filter((value) => value !== null)
 
     if (!filterValue.value.length > 0) {
       return ''

+ 1 - 1
services/data/Filters/OrderBy.ts

@@ -20,7 +20,7 @@ export default class OrderBy implements ApiFilter {
     this.mode = mode
   }
 
-  public applyToPiniaOrmQuery <T extends ApiResource>(
+  public applyToPiniaOrmQuery<T extends ApiResource>(
     query: PiniaOrmQuery<T>,
   ): PiniaOrmQuery<T> {
     return query.orderBy(

+ 1 - 1
services/data/Filters/PageFilter.ts

@@ -16,7 +16,7 @@ export default class PageFilter implements ApiFilter {
     this.itemsPerPage = itemsPerPage
   }
 
-  public applyToPiniaOrmQuery <T extends ApiResource>(
+  public applyToPiniaOrmQuery<T extends ApiResource>(
     query: PiniaOrmQuery<T>,
   ): PiniaOrmQuery<T> {
     const page = RefUtils.castToRef(this.page, false)

+ 1 - 1
services/data/Filters/SearchFilter.ts

@@ -63,7 +63,7 @@ export default class SearchFilter extends AbstractFilter implements ApiFilter {
     }
   }
 
-  public applyToPiniaOrmQuery <T extends ApiResource>(
+  public applyToPiniaOrmQuery<T extends ApiResource>(
     query: PiniaOrmQuery<T>,
   ): PiniaOrmQuery<T> {
     const filterValue = RefUtils.castToRef(

+ 8 - 8
services/data/Filters/TimeFilter.ts

@@ -1,11 +1,11 @@
-import type {Query as PiniaOrmQuery} from 'pinia-orm'
-import type {Ref} from 'vue'
-import type {ApiFilter} from '~/types/data'
+import type { Query as PiniaOrmQuery } from 'pinia-orm'
+import type { Ref } from 'vue'
+import type { ApiFilter } from '~/types/data'
 import type ApiResource from '~/models/ApiResource'
-import {TIME_STRATEGY} from '~/types/enum/data'
+import { TIME_STRATEGY } from '~/types/enum/data'
 import AbstractFilter from '~/services/data/Filters/AbstractFilter'
-import RefUtils from "~/services/utils/refUtils";
-import DateUtils from "~/services/utils/dateUtils";
+import RefUtils from '~/services/utils/refUtils'
+import DateUtils from '~/services/utils/dateUtils'
 
 export default class TimeFilter extends AbstractFilter implements ApiFilter {
   field: string
@@ -37,7 +37,7 @@ export default class TimeFilter extends AbstractFilter implements ApiFilter {
       return false
     }
 
-    switch (this.mode){
+    switch (this.mode) {
       case TIME_STRATEGY.BEFORE:
         return DateUtils.isBefore(value, filterValue.value, false)
       case TIME_STRATEGY.AFTER:
@@ -51,7 +51,7 @@ export default class TimeFilter extends AbstractFilter implements ApiFilter {
     }
   }
 
-  public applyToPiniaOrmQuery <T extends ApiResource>(
+  public applyToPiniaOrmQuery<T extends ApiResource>(
     query: PiniaOrmQuery<T>,
   ): PiniaOrmQuery<T> {
     const filterValue = RefUtils.castToRef(

Some files were not shown because too many files changed in this diff