Vincent 7 месяцев назад
Родитель
Сommit
8376a33e66
75 измененных файлов с 784 добавлено и 1486 удалено
  1. 26 0
      components/Form/Parameter/AttendanceBookingReason.vue
  2. 25 0
      components/Form/Parameter/EducationTiming.vue
  3. 26 0
      components/Form/Parameter/ResidenceArea.vue
  4. 4 0
      components/Layout/Alert/Content.vue
  5. 3 3
      components/Layout/AlertBar/Cotisation.vue
  6. 2 2
      components/Layout/AlertBar/OnlineRegistration.vue
  7. 2 2
      components/Layout/AlertBar/RegistrationStatus.vue
  8. 2 2
      components/Layout/Header/HomeBtn.vue
  9. 4 4
      components/Layout/Header/Notification.vue
  10. 13 3
      components/Layout/Parameters/EntityTable.vue
  11. 2 2
      components/Layout/Parameters/Table.vue
  12. 2 2
      components/Layout/SubHeader/PersonnalizedList.vue
  13. 0 56
      components/Ui/Card.vue
  14. 0 55
      components/Ui/Collection.vue
  15. 0 86
      components/Ui/DataTable.vue
  16. 14 5
      components/Ui/Form.vue
  17. 9 2
      components/Ui/Form/Creation.vue
  18. 13 2
      components/Ui/Form/Edition.vue
  19. 7 17
      components/Ui/Input/Autocomplete.vue
  20. 12 3
      components/Ui/Input/Autocomplete/Accesses.vue
  21. 12 3
      components/Ui/Input/Autocomplete/ApiResources.vue
  22. 12 2
      components/Ui/Input/Autocomplete/Enum.vue
  23. 5 1
      components/Ui/Input/Checkbox.vue
  24. 5 1
      components/Ui/Input/Combobox.vue
  25. 5 1
      components/Ui/Input/DatePicker.vue
  26. 17 5
      components/Ui/Input/Email.vue
  27. 34 4
      components/Ui/Input/Number.vue
  28. 5 1
      components/Ui/Input/Text.vue
  29. 5 1
      components/Ui/Input/TextArea.vue
  30. 17 15
      composables/data/useEntityFetch.ts
  31. 1 2
      composables/form/useFieldViolation.ts
  32. 2 2
      composables/utils/useHomeUrl.ts
  33. 1 0
      i18n/lang/fr.json
  34. 2 0
      models/Billing/ResidenceArea.ts
  35. 2 0
      models/Booking/AttendanceBookingReason.ts
  36. 2 0
      models/Education/Cycle.ts
  37. 2 0
      models/Education/EducationTiming.ts
  38. 2 1
      models/Organization/Parameters.ts
  39. 16 18
      pages/my-settings.vue
  40. 0 77
      pages/organization.vue.off
  41. 0 580
      pages/organization/index.vue.off
  42. 1 12
      pages/parameters/attendance_booking_reasons/[id].vue
  43. 1 21
      pages/parameters/attendance_booking_reasons/new.vue
  44. 31 43
      pages/parameters/attendances.vue
  45. 70 75
      pages/parameters/bulletin.vue
  46. 8 7
      pages/parameters/cycles/[id].vue
  47. 46 50
      pages/parameters/education_notation.vue
  48. 1 12
      pages/parameters/education_timings/[id].vue
  49. 1 0
      pages/parameters/education_timings/index.vue
  50. 1 21
      pages/parameters/education_timings/new.vue
  51. 70 74
      pages/parameters/general_parameters.vue
  52. 42 47
      pages/parameters/intranet.vue
  53. 1 13
      pages/parameters/residence_areas/[id].vue
  54. 1 0
      pages/parameters/residence_areas/index.vue
  55. 1 19
      pages/parameters/residence_areas/new.vue
  56. 24 38
      pages/parameters/sms.vue
  57. 6 2
      pages/parameters/subdomains/[id].vue
  58. 4 0
      pages/parameters/subdomains/new.vue
  59. 23 29
      pages/parameters/super_admin.vue
  60. 28 19
      pages/parameters/teaching.vue
  61. 9 4
      pages/parameters/website.vue
  62. 10 9
      pages/subscription.vue
  63. 1 1
      services/asserts/MaxAssert.ts
  64. 2 1
      services/asserts/NullableAssert.ts
  65. 5 0
      services/asserts/TypeAssert.ts
  66. 3 3
      services/data/Filters/EqualFilter.ts
  67. 3 3
      services/data/Filters/InArrayFilter.ts
  68. 3 3
      services/data/Filters/OrderBy.ts
  69. 3 3
      services/data/Filters/PageFilter.ts
  70. 3 3
      services/data/Filters/SearchFilter.ts
  71. 44 0
      services/data/Filters/TimeFilter.ts
  72. 3 3
      services/data/Query.ts
  73. 5 5
      services/data/entityManager.ts
  74. 12 1
      stores/page.ts
  75. 5 5
      types/data.d.ts

+ 26 - 0
components/Form/Parameter/AttendanceBookingReason.vue

@@ -0,0 +1,26 @@
+<template>
+  <v-container :fluid="true" class="container">
+    <v-row>
+      <v-col cols="12" sm="6">
+        <UiInputText
+          v-model="entity.reason"
+          field="reason"
+          type="string"
+          :rules="getAsserts('reason')"
+        />
+      </v-col>
+    </v-row>
+  </v-container>
+</template>
+
+<script setup lang="ts">
+import AttendanceBookingReason from "~/models/Booking/AttendanceBookingReason";
+import {getAssertUtils} from "~/services/asserts/getAssertUtils";
+
+defineProps<{
+  entity: AttendanceBookingReason
+}>()
+
+const getAsserts = (key) => getAssertUtils(AttendanceBookingReason.getAsserts(), key)
+
+</script>

+ 25 - 0
components/Form/Parameter/EducationTiming.vue

@@ -0,0 +1,25 @@
+<template>
+  <v-container :fluid="true" class="container">
+    <v-row>
+      <v-col cols="12" sm="6">
+        <UiInputNumber
+          v-model="entity.timing"
+          field="educationTiming"
+          :rules="getAsserts('timing')"
+        />
+      </v-col>
+    </v-row>
+  </v-container>
+</template>
+
+<script setup lang="ts">
+import {getAssertUtils} from "~/services/asserts/getAssertUtils";
+import EducationTiming from "~/models/Education/EducationTiming";
+
+defineProps<{
+  entity: EducationTiming
+}>()
+
+const getAsserts = (key) => getAssertUtils(EducationTiming.getAsserts(), key)
+
+</script>

+ 26 - 0
components/Form/Parameter/ResidenceArea.vue

@@ -0,0 +1,26 @@
+<template>
+  <v-container :fluid="true" class="container">
+    <v-row>
+      <v-col cols="12" sm="6">
+        <UiInputText
+          v-model="entity.label"
+          field="label"
+          type="string"
+          :rules="getAsserts('label')"
+        />
+      </v-col>
+    </v-row>
+  </v-container>
+</template>
+
+<script setup lang="ts">
+import ResidenceArea from "~/models/Billing/ResidenceArea";
+import {getAssertUtils} from "~/services/asserts/getAssertUtils";
+
+defineProps<{
+  entity: ResidenceArea
+}>()
+
+const getAsserts = (key) => getAssertUtils(ResidenceArea.getAsserts(), key)
+
+</script>

+ 4 - 0
components/Layout/Alert/Content.vue

@@ -78,6 +78,10 @@ const onMouseOut = () => {
 }
 
 clearAlert()
+
+onUnmounted(() => {
+  pageStore.cancelRemoveAlert()
+})
 </script>
 
 <style scoped></style>

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

@@ -34,7 +34,7 @@ if (!organizationProfile.id) {
 }
 
 const { fetch } = useEntityFetch()
-const { data: cotisation, pending } = await fetch(
+const { data: cotisation, status } = await fetch(
   Cotisation,
   organizationProfile.id,
 )
@@ -45,7 +45,7 @@ interface Alert {
 }
 
 const cotisationYear: ComputedRef<number | null> = computed(() => {
-  if (pending.value || cotisation.value === null) {
+  if (status.value == 'pending' || cotisation.value === null) {
     return null
   }
 
@@ -53,7 +53,7 @@ const cotisationYear: ComputedRef<number | null> = computed(() => {
 })
 
 const alert: ComputedRef<Alert | null> = computed(() => {
-  if (pending.value || cotisation.value === null) {
+  if (status.value == 'pending' || cotisation.value === null) {
     return null
   }
 

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

@@ -22,14 +22,14 @@ const { fetch } = useEntityFetch()
 
 const accessProfile = useAccessProfileStore()
 
-const { data: registrationAvailability, pending } = fetch(
+const { data: registrationAvailability, status } = fetch(
   RegistrationAvailability,
   accessProfile.id ?? 0,
 )
 
 const show: ComputedRef<boolean> = computed(() => {
   return (
-    !pending &&
+    status.value == 'success' &&
     (registrationAvailability.value as RegistrationAvailability).available
   )
 })

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

@@ -5,7 +5,7 @@ Barre d'alerte quand au statut (l'avancement) de l'inscription en ligne de l'uti
 
 <template>
   <UiSystemBar
-    v-if="!pending && message"
+    v-if="status == 'success' && message"
     :text="$t(message)"
     icon="fas fa-id-card"
     class="theme-secondary"
@@ -22,7 +22,7 @@ const { fetch } = useEntityFetch()
 
 const accessProfile = useAccessProfileStore()
 
-const { data: registrationStatus, pending } = fetch(
+const { data: registrationStatus, status } = fetch(
   RegistrationStatus,
   accessProfile.currentAccessId ?? 0,
 )

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

@@ -4,8 +4,8 @@
       ref="btn"
       icon="fas fa-home"
       size="small"
-      :href="!$can('display', 'freemium_events_page') ? homeUrl : undefined"
-      :to="$can('display', 'freemium_events_page') ? homeUrl : undefined"
+      :href="!$can('display', 'freemium_dashboard_page') ? homeUrl : undefined"
+      :to="$can('display', 'freemium_dashboard_page') ? homeUrl : undefined"
       class="on-primary"
     />
     <v-tooltip :activator="btn" :text="$t('welcome')" location="bottom" />

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

@@ -54,7 +54,7 @@
           <span v-intersect="onLastNotificationIntersect" />
 
           <v-row
-            v-if="pending"
+            v-if="status=='pending'"
             class="fill-height mt-3 mb-3"
             align="center"
             justify="center"
@@ -116,7 +116,7 @@ const query = new Query(new PageFilter(page, itemsPerPage))
 
 const {
   data: collection,
-  pending,
+  status,
   refresh,
 } = fetchCollection(Notification, null, query)
 
@@ -162,12 +162,12 @@ const onLastNotificationIntersect = (isIntersecting: boolean) => {
  */
 const update = async () => {
   if (
-    !pending.value &&
+    status.value !== 'pending' &&
     pagination.value &&
     pagination.value.next &&
     pagination.value.next > 0
   ) {
-    pending.value = true
+    status.value = 'pending'
     page.value = pagination.value.next
 
     await refresh()

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

@@ -3,7 +3,7 @@ A data table for the parameters page
 -->
 <template>
   <div class="container">
-    <UiLoadingPanel v-if="pending" />
+    <UiLoadingPanel v-if="status==='pending'" />
     <div v-else>
       <LayoutParametersTable
         :items="items"
@@ -99,7 +99,7 @@ const { em } = useEntityManager()
 
 const { fetchCollection } = useEntityFetch()
 
-const { data: collection, pending } = fetchCollection(props.model)
+const { data: collection, status } = fetchCollection(props.model)
 
 const { deleteItem } = useDeleteItem()
 
@@ -123,7 +123,7 @@ const columns: ComputedRef<Array<ColumnDefinition>> = computed(() => {
  * map it according to the configuration.
  */
 const items: ComputedRef<Array<ApiResource> | null> = computed(() => {
-  if (pending.value || collection.value === null) {
+  if (status.value === 'pending' || collection.value === null) {
     return null
   }
 
@@ -194,6 +194,16 @@ const onDeleteConfirmed = async () => {
 const goToCreatePage = () => {
   navigateTo(UrlUtils.join(actionsRoute.value, 'new'))
 }
+
+// Nettoyer les données lors du démontage du composant
+onBeforeUnmount(() => {
+  // Nettoyer les références du store si nécessaire
+  if (process.client) {
+    clearNuxtData('/^' + props.model.entity + '_many_/')
+    useRepo(props.model).flush()
+  }
+})
+
 </script>
 
 <style scoped lang="scss"></style>

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

@@ -18,8 +18,8 @@ A data table for the parameters page
           <td>{{ $t('actions') }}</td>
         </tr>
       </thead>
-      <tbody v-if="items.length > 0">
-        <tr v-for="(item, i) in items" :key="i">
+      <tbody v-if="items && items.length > 0">
+        <tr v-for="(item, i) in items" :key="item.id">
           <td
             v-for="(col, index) in columnsDefinitions"
             :key="index"

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

@@ -26,7 +26,7 @@
           <v-text-field
             v-model="search"
             :label="$t('searchList')"
-            :loading="pending"
+            :loading="status=='pending'"
             density="compact"
             clear-icon="header-personalized"
           />
@@ -63,7 +63,7 @@ const btn: Ref = ref(null)
 
 const { fetchCollection } = useEntityFetch()
 
-const { data: collection, pending } = fetchCollection(PersonalizedList)
+const { data: collection, status } = fetchCollection(PersonalizedList)
 
 const i18n = useI18n()
 

+ 0 - 56
components/Ui/Card.vue

@@ -1,56 +0,0 @@
-<!--
-Container de type Card
--->
-
-<template>
-  <v-card elevation="2" outlined shaped min-height="200">
-    <!-- Titre -->
-    <v-card-title>
-      <slot name="card.title" />
-    </v-card-title>
-
-    <!-- Texte -->
-    <v-card-text>
-      <slot name="card.text" />
-    </v-card-text>
-
-    <!-- Actions -->
-    <v-card-actions>
-      <v-spacer />
-
-      <v-btn :icon="true">
-        <NuxtLink :to="link" class="no-decoration">
-          <v-icon>mdi-pencil</v-icon>
-        </NuxtLink>
-      </v-btn>
-
-      <UiButtonDelete v-if="withDeleteAction" :entity="entity" />
-
-      <slot name="card.action" />
-    </v-card-actions>
-  </v-card>
-</template>
-
-<script setup lang="ts">
-const props = defineProps({
-  link: {
-    type: String,
-    required: true,
-  },
-  model: {
-    type: Object,
-    required: true,
-  },
-  entity: {
-    type: Object,
-    required: true,
-  },
-  withDeleteAction: {
-    type: Boolean,
-    required: false,
-    default: true,
-  },
-})
-</script>
-
-<style scoped></style>

+ 0 - 55
components/Ui/Collection.vue

@@ -1,55 +0,0 @@
-<!-- Permet de requêter une subResource et de donner son contenu à un slot -->
-
-<template>
-  <main>
-    <v-skeleton-loader v-if="pending" :type="loaderType" />
-    <div v-else>
-      <!-- Content -->
-      <slot name="list.item" v-bind="{ items: collection?.items }" />
-
-      <!-- New button -->
-      <v-btn v-if="newLink" class="theme-primary float-right">
-        <NuxtLink :to="newLink" class="no-decoration">
-          <v-icon>fa-plus-circle</v-icon>
-          <span>{{ $t('add') }}</span>
-        </NuxtLink>
-      </v-btn>
-    </div>
-    <slot />
-  </main>
-</template>
-
-<script setup lang="ts">
-import { computed, toRefs } from 'vue'
-import type { ComputedRef, ToRefs } from 'vue'
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
-import type { Collection } from '~/types/data'
-
-const props = defineProps({
-  model: {
-    type: Object,
-    required: true,
-  },
-  parent: {
-    type: Object,
-    required: false,
-    default: () => null,
-  },
-  loaderType: {
-    type: String,
-    required: false,
-    default: 'text',
-  },
-  newLink: {
-    type: String,
-    required: false,
-    default: null,
-  },
-})
-
-const { model, parent }: ToRefs = toRefs(props)
-
-const { fetchCollection } = useEntityFetch()
-
-const { data: collection, pending } = fetchCollection(model.value, parent.value)
-</script>

+ 0 - 86
components/Ui/DataTable.vue

@@ -1,86 +0,0 @@
-<!--
-Tableau interactif conçu pour l'affichage d'une collection d'entités
-
-@see https://vuetifyjs.com/en/components/data-tables/
--->
-
-<template>
-  <v-col cols="12" sm="12">
-    <v-data-table
-      :headers="headersWithItem"
-      :items="collection.items"
-      :server-items-length="collection.pagination.totalItems"
-      :loading="$fetchState.pending"
-      class="elevation-1"
-    >
-      <template
-        v-for="(header, index) in headersWithItem"
-        :key="index"
-        #[header.item]="slotProps"
-      >
-        <slot :name="header.item" v-bind="slotProps">
-          {{ slotProps.item[header.value] }}
-        </slot>
-      </template>
-
-      <template #item.actions="{ item }">
-        <v-icon small class="mr-2" @click="editItem(item)"> mdi-pencil </v-icon>
-        <v-icon small @click="deleteItem(item)"> mdi-delete </v-icon>
-      </template>
-    </v-data-table>
-  </v-col>
-</template>
-
-<script setup lang="ts">
-import { ref, toRefs } from 'vue'
-import type { Ref } from 'vue'
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
-import type ApiResource from '~/models/ApiResource'
-import type { AnyJson } from '~/types/data'
-
-const props = defineProps({
-  parent: {
-    type: Object,
-    required: true,
-  },
-  model: {
-    type: Object,
-    required: true,
-  },
-  headers: {
-    type: Array,
-    required: true,
-  },
-})
-
-const { parent, model, headers } = toRefs(props)
-
-interface TableHeader {
-  value: string
-  item?: string
-  [key: string]: string | undefined
-}
-
-const headersWithItem = computed(() => {
-  return headers.value.map((header: TableHeader) => {
-    header.item = 'item.' + header.value
-    return header
-  })
-})
-
-const totalEntries: Ref<number> = ref(0)
-const entries: Ref<Array<AnyJson>> = ref(Array<AnyJson>())
-
-const { fetchCollection } = useEntityFetch()
-
-const { data: collection, pending } = await fetchCollection(
-  model.value as typeof ApiResource,
-  parent.value as ApiResource,
-)
-
-const itemId: Ref<number> = ref(0)
-
-const editItem = (item: AnyJson) => {
-  itemId.value = item.id
-}
-</script>

+ 14 - 5
components/Ui/Form.vue

@@ -102,6 +102,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";
 
 const props = defineProps({
   /**
@@ -330,13 +333,19 @@ onBeforeRouteLeave(
 )
 
 onMounted(() => {
-  window.addEventListener('beforeunload', (event) => {
-    if (formStore.dirty === true) {
-      event.returnValue = i18n.t('quit_without_saving_warning')
-    }
-  })
+  window.addEventListener('beforeunload', quitWithoutSaving )
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('beforeunload', quitWithoutSaving)
 })
 
+function quitWithoutSaving(event: any){
+  if (formStore.dirty === true) {
+    event.returnValue = i18n.t('quit_without_saving_warning')
+  }
+}
+
 /**
  * Quitte le formulaire sans enregistrer
  */

+ 9 - 2
components/Ui/Form/Creation.vue

@@ -41,8 +41,7 @@ const props = withDefaults(defineProps<{
 const router = useRouter()
 const { em } = useEntityManager()
 
-// @ts-expect-error Pour une raison que j'ignore, le type Ref<ApiModel> met en erreur la prop entity de UiForm...
-const entity: ApiModel = reactive(em.newInstance(props.model))
+const entity = reactive(em.newInstance(props.model)) as InstanceType<T>
 
 const submitActions = computed(() => {
   const actions: AnyJson = {}
@@ -62,6 +61,14 @@ const quit = () => {
 
   router.push(props.goBackRoute)
 }
+
+// Nettoyer les données lors du démontage du composant
+onBeforeUnmount(() => {
+  // Nettoyer les références du store si nécessaire
+  if (process.client) {
+    useRepo(props.model).flush()
+  }
+})
 </script>
 
 <style scoped lang="scss"></style>

+ 13 - 2
components/Ui/Form/Edition.vue

@@ -1,6 +1,6 @@
 <template>
   <LayoutContainer>
-    <UiLoadingPanel v-if="pending" />
+    <UiLoadingPanel v-if="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">
@@ -54,7 +54,7 @@ const router = useRouter()
 const entityId =
   props.id !== null ? props.id : parseInt(route.params.id as string)
 
-const { data: entity, pending } = fetch(props.model, entityId)
+const { data: entity, status } = fetch(props.model, entityId)
 
 const submitActions = computed(() => {
   const actions: AnyJson = {}
@@ -74,6 +74,17 @@ const quit = () => {
 
   router.push(props.goBackRoute)
 }
+
+// Nettoyer les données lors du démontage du composant
+onBeforeUnmount(() => {
+  // Nettoyer les références du store si nécessaire
+  if (process.client) {
+    clearNuxtData('/^' + props.model.entity + '_' + props.id + '_/')
+    useRepo(props.model).flush()
+    // Forcer le garbage collection des objets Parameters
+    entity.value = null
+  }
+})
 </script>
 
 <style scoped lang="scss"></style>

+ 7 - 17
components/Ui/Input/Autocomplete.vue

@@ -48,8 +48,7 @@ Liste déroulante avec autocompletion, à placer dans un composant `UiForm`
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue'
-import type { ComputedRef, Ref, PropType } from 'vue'
+import type { Ref, PropType } from 'vue'
 import { useFieldViolation } from '~/composables/form/useFieldViolation'
 import ObjectUtils from '~/services/utils/objectUtils'
 import type { AnyJson } from '~/types/data'
@@ -265,24 +264,10 @@ const emit = defineEmits([
 ])
 
 const onUpdate = (event: string) => {
-  updateViolationState(event)
+  updateViolationState()
   emit('update:model-value', event)
 }
 
-/**
- * Items à afficher
- * TODO: à revoir
- */
-const items: ComputedRef<Array<AnyJson>> = computed(() => {
-  const _items: Array<AnyJson> = props.items
-  return _items
-  // if (props.group !== null) {
-  //   _items = groupItems(props.items)
-  // }
-  //
-  // return prepareGroups(_items)
-})
-
 /**
  * On construit l'Array à double entrée contenant les groups (headers) et les items
  * TODO: à revoir
@@ -371,6 +356,11 @@ const prepareItem = (item: object): AnyJson => {
     slotTextDisplay: slotTextDisplay.join(' '),
   })
 }
+
+onUnmounted(() => {
+  updateViolationState()
+  search.value = null
+})
 </script>
 
 <style scoped lang="scss">

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

@@ -176,7 +176,7 @@ const queryActive = new Query(
   new InArrayFilter('id', activeIds),
 )
 
-const { data: collectionActive, pending: pendingActive } = fetchCollection(
+const { data: collectionActive, status: statusActive } = fetchCollection(
   UserSearchItem,
   null,
   queryActive,
@@ -196,11 +196,11 @@ const querySearch = new Query(
  */
 const {
   data: collectionSearch,
-  pending: pendingSearch,
+  status: statusSearch,
   refresh: refreshSearch,
 } = fetchCollection(UserSearchItem, null, querySearch)
 
-const pending = computed(() => pendingSearch.value || pendingActive.value)
+const pending = computed(() => statusSearch.value == 'pending' || statusActive.value == 'pending')
 
 /**
  * Contenu de la liste autocomplete
@@ -253,6 +253,15 @@ const onUpdateModelValue = (event: Array<number>) => {
   }
   emit('update:model-value', 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) {
+    clearNuxtData('/^' + UserSearchItem + '_many_/')
+    useRepo(UserSearchItem).flush()
+  }
+})
 </script>
 
 <style scoped lang="scss">

+ 12 - 3
components/Ui/Input/Autocomplete/ApiResources.vue

@@ -176,7 +176,7 @@ const queryActive = new Query(
 
 const {
   data: collectionActive,
-  pending: pendingActive
+  status: statusActive
 } = fetchCollection(props.model, null, queryActive)
 
 
@@ -199,11 +199,11 @@ const querySearch = new Query(
  */
 const {
   data: collectionSearch,
-  pending: pendingSearch,
+  status: statusSearch,
   refresh: refreshSearch,
 } = fetchCollection(props.model, null, querySearch)
 
-const pending = computed(() => pendingSearch.value || pendingActive.value)
+const pending = computed(() => statusSearch.value == 'pending' || statusActive.value == 'pending')
 
 /**
  * Contenu de la liste autocomplete
@@ -256,6 +256,15 @@ const onUpdateModelValue = (event: Array<number>) => {
   }
   emit('update:model-value', 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) {
+    clearNuxtData('/^' + props.model.entity + '_many_/')
+    useRepo(props.model).flush()
+  }
+})
 </script>
 
 <style scoped lang="scss">

+ 12 - 2
components/Ui/Input/Autocomplete/Enum.vue

@@ -3,7 +3,7 @@
     :model-value="modelValue"
     :field="field"
     :items="items"
-    :is-loading="pending"
+    :is-loading="status==='pending'"
     :return-object="false"
     item-title="label"
     item-value="value"
@@ -61,7 +61,7 @@ const props = defineProps({
 
 const { fetch } = useEnumFetch()
 
-const { data: enumItems, pending } = fetch(props.enumName)
+const { data: enumItems, status } = fetch(props.enumName)
 
 const items: ComputedRef<Array<Enum>> = computed(() => {
   if (!enumItems.value) {
@@ -69,6 +69,16 @@ const items: ComputedRef<Array<Enum>> = computed(() => {
   }
   return ArrayUtils.sortObjectsByProp(enumItems.value, 'label') as Array<Enum>
 })
+
+// Nettoyer les données lors du démontage du composant
+onBeforeUnmount(() => {
+  // Nettoyer les références du store si nécessaire
+  if (process.client) {
+    clearNuxtData(props.enumName)
+    // Forcer le garbage collection des objets Parameters
+    enumItems.value = null
+  }
+})
 </script>
 
 <style scoped lang="scss"></style>

+ 5 - 1
components/Ui/Input/Checkbox.vue

@@ -88,9 +88,13 @@ const fieldLabel: string = props.label ?? props.field
 const emit = defineEmits(['update:model-value'])
 
 const onUpdate = (event: boolean) => {
-  updateViolationState(event)
+  updateViolationState()
   emit('update:model-value', event)
 }
+
+onUnmounted(() => {
+  updateViolationState()
+})
 </script>
 
 <style scoped lang="scss">

+ 5 - 1
components/Ui/Input/Combobox.vue

@@ -100,9 +100,13 @@ const fieldLabel: string = props.label ?? props.field
 const emit = defineEmits(['update:model-value'])
 
 const onUpdate = (event: string) => {
-  updateViolationState(event)
+  updateViolationState()
   emit('update:model-value', event)
 }
+
+onUnmounted(() => {
+  updateViolationState()
+})
 </script>
 
 <style scoped></style>

+ 5 - 1
components/Ui/Input/DatePicker.vue

@@ -101,10 +101,14 @@ const date: Ref<Date | undefined> = ref(
 )
 
 const onUpdate = (event: string) => {
-  updateViolationState(event)
+  updateViolationState()
   date.value = event ? new Date(event) : undefined
   emit('update:model-value', date.value ? formatISO(date.value) : undefined)
 }
+
+onUnmounted(() => {
+  updateViolationState()
+})
 </script>
 
 <style scoped lang="scss">

+ 17 - 5
components/Ui/Input/Email.vue

@@ -7,10 +7,12 @@ Champs de saisie de type Text dédié à la saisie d'emails
     :data="data"
     :label="$t(fieldLabel)"
     :readonly="readonly"
-    :error="error || !!violation"
-    :error-messages="errorMessage || violation ? $t(violation) : ''"
     :rules="rules"
-    @update="onChange"
+    :error="error || !!fieldViolations"
+    :error-messages="
+        errorMessage || fieldViolations ? $t(fieldViolations) : ''
+      "
+    @update:model-value="onUpdate($event)"
   />
 </template>
 
@@ -75,13 +77,12 @@ const props = defineProps({
   },
 })
 
-const { emit } = useNuxtApp()
 
 const i18n = useI18n()
 
 const fieldLabel = props.label ?? props.field
 
-const { violation, onChange } = useFieldViolation(props.field, emit)
+const { fieldViolations, updateViolationState } = useFieldViolation(props.field)
 
 const validationUtils = useValidationUtils()
 
@@ -92,4 +93,15 @@ const rules = [
 if (props.required) {
   rules.push((email: string) => !!email || i18n.t('required'))
 }
+
+const emit = defineEmits(['update:model-value'])
+
+const onUpdate = (event: string) => {
+  updateViolationState()
+  emit('update:model-value', event)
+}
+
+onUnmounted(() => {
+  updateViolationState()
+})
 </script>

+ 34 - 4
components/Ui/Input/Number.vue

@@ -9,16 +9,21 @@ An input for numeric values
     :model-value.number="modelValue"
     :label="label || field ? $t(label ?? field) : undefined"
     :rules="rules"
-    hide-details
     type="number"
     :variant="variant"
     density="compact"
+    :error="error || !!fieldViolations"
+    :error-messages="
+      errorMessage || (fieldViolations ? $t(fieldViolations) : '')
+    "
     @update:model-value="onModelUpdate($event)"
+    @change="onChange($event)"
   />
 </template>
 
 <script setup lang="ts">
 import type { PropType, Ref } from 'vue'
+import {useFieldViolation} from "~/composables/form/useFieldViolation";
 
 type ValidationRule = (value: string | number | null) => boolean | string
 
@@ -87,6 +92,22 @@ const props = defineProps({
     required: false,
     default: () => [],
   },
+  /**
+   * Le champ est-il actuellement en état d'erreur
+   */
+  error: {
+    type: Boolean,
+    required: false,
+    default: false,
+  },
+  /**
+   * Si le champ est en état d'erreur, quel est le message d'erreur?
+   */
+  errorMessage: {
+    type: String,
+    required: false,
+    default: null,
+  },
 })
 
 /**
@@ -95,6 +116,8 @@ const props = defineProps({
 // eslint-disable-next-line
 const input: Ref<any> = ref(null)
 
+const { fieldViolations, updateViolationState } = useFieldViolation(props.field)
+
 /**
  * Cast the value to a number, or fallback on default value
  * @param val
@@ -125,14 +148,17 @@ const keepInRange = (val: number) => {
   return val
 }
 
-const emit = defineEmits(['update:modelValue'])
+const emit = defineEmits(['update:modelValue', 'change'])
 
 const onModelUpdate = (event: string) => {
-  // props.modelValue = keepInRange(cast(event))
-  // emitUpdate()
   emit('update:modelValue', keepInRange(cast(event)))
 }
 
+const onChange = (event: Event | undefined) => {
+  updateViolationState()
+  emit('change', event)
+}
+
 /**
  * Setup min and max values at the input level
  */
@@ -145,4 +171,8 @@ onMounted(() => {
     inputElement.max = props.max
   }
 })
+
+onUnmounted(() => {
+  updateViolationState()
+})
 </script>

+ 5 - 1
components/Ui/Input/Text.vue

@@ -145,9 +145,13 @@ const onUpdate = (event: string) => {
 }
 
 const onChange = (event: Event | undefined) => {
-  updateViolationState(event)
+  updateViolationState()
   emit('change', event)
 }
+
+onUnmounted(() => {
+  updateViolationState()
+})
 </script>
 
 <style scoped lang="scss">

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

@@ -98,9 +98,13 @@ const onUpdate = (event: string) => {
 }
 
 const onChange = (event: Event | undefined) => {
-  updateViolationState(event)
+  updateViolationState()
   emit('change', event)
 }
+
+onUnmounted(() => {
+  updateViolationState()
+})
 </script>
 
 <style scoped lang="scss">

+ 17 - 15
composables/data/useEntityFetch.ts

@@ -9,7 +9,6 @@ import { useEntityManager } from '~/composables/data/useEntityManager'
 import type ApiResource from '~/models/ApiResource'
 import type { Collection } from '~/types/data'
 import type Query from '~/services/data/Query'
-import type {Collection as PiniaOrmCollection} from "pinia-orm";
 
 interface useEntityFetchReturnType {
   fetch: <T extends typeof ApiResource>(
@@ -17,16 +16,15 @@ interface useEntityFetchReturnType {
     id?: number | null
   ) => AsyncData<InstanceType<T> | null, Error | null>
 
-  fetchCollection: (
-    model: typeof ApiResource,
-    parent?: ApiResource | null,
+  fetchCollection: <T extends typeof ApiResource>(
+    model: T,
+    parent?: T | null,
     query?: typeof Query | Query| null,
   ) => {
-    data: ComputedRef<Collection | null>
-    pending: Ref<boolean>
+    data: ComputedRef<Collection<InstanceType<T>> | null>
     refresh: (
       opts?: AsyncDataExecuteOptions,
-    ) => Promise<ComputedRef<Collection> | null>
+    ) => Promise<ComputedRef<Collection<InstanceType<T>>> | null>
     error: Ref<Error | null>
     status: Ref<AsyncDataRequestStatus>
   }
@@ -37,25 +35,30 @@ interface useEntityFetchReturnType {
   ) => ComputedRef<null | T>
 }
 
-// TODO: améliorer le typage des fonctions sur le modèle de getRef
 export const useEntityFetch = (
   lazy: boolean = false,
 ): useEntityFetchReturnType => {
   const { em } = useEntityManager()
 
-  const fetch = <T extends typeof ApiResource>(model: T, id?: number|null): AsyncData<InstanceType<T> | null, Error | null> =>
-    useAsyncData(
+  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),
       { lazy },
     )
+  }
 
-  const fetchCollection = (
-    model: typeof ApiResource,
-    parent: ApiResource | null = null,
+  const fetchCollection = <T extends typeof ApiResource> (
+    model: T,
+    parent: T | null = null,
     query: Query | null = null,
   )  => {
-    const { data, pending, refresh, error, status } = useAsyncData(
+    const {
+      data,
+      refresh,
+      error,
+      status
+    } = useAsyncData(
       model.entity + '_many_' + uuid4(),
       () => em.fetchCollection(model, parent, query),
       { lazy, deep: true },
@@ -63,7 +66,6 @@ export const useEntityFetch = (
 
     return {
       data: computed(() => (data.value !== null ? data.value.value : null)),
-      pending,
       refresh,
       error,
       status,

+ 1 - 2
composables/form/useFieldViolation.ts

@@ -15,9 +15,8 @@ export function useFieldViolation(field: string) {
 
   /**
    * Lorsque la valeur d'un champ change, on supprime le fait qu'il puisse être "faux" dans le store
-   * @param field
    */
-  function updateViolationState(field: string) {
+  function updateViolationState() {
     useFormStore().setViolations(
       _.omit(useFormStore().violations, field) as string[],
     )

+ 2 - 2
composables/utils/useHomeUrl.ts

@@ -5,9 +5,9 @@ export const useHomeUrl = () => {
 
   let homeUrl = null
 
-  if(ability.can('display', 'freemium_events_page')){
+  if(ability.can('display', 'freemium_dashboard_page')){
     const router = useRouter()
-    const to = router.resolve({ name: 'freemium_events_page' })
+    const to = router.resolve({ name: 'freemium_dashboard_page' })
     homeUrl = to.href
   }else{
     const { makeAdminUrl } = useAdminUrl()

+ 1 - 0
i18n/lang/fr.json

@@ -1,4 +1,5 @@
 {
+  "need_to_be_integer": "Doit être un nombre entier",
   "freemium_event_create_page": "Créer un nouvel événement",
   "search_gps_button": "Mettre à jour les coordonnées géographique.",
   "invalid_phone_number": "Numéro de téléphone non valide (exemple {example})",

+ 2 - 0
models/Billing/ResidenceArea.ts

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

+ 2 - 0
models/Booking/AttendanceBookingReason.ts

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

+ 2 - 0
models/Education/Cycle.ts

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

+ 2 - 0
models/Education/EducationTiming.ts

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

+ 2 - 1
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 { IriEncoded } from '~/models/decorators'
+import {Assert, IriEncoded} from '~/models/decorators'
 import File from '~/models/Core/File'
 
 /**
@@ -141,6 +141,7 @@ export default class Parameters extends ApiModel {
   declare notifyAdministrationAbsence: boolean
 
   @Num(2, { notNullable: true })
+  @Assert({'nullable': false, 'type' : 'integer'})
   declare numberConsecutiveAbsences: number
 
   @Bool(false, { notNullable: false })

+ 16 - 18
pages/my-settings.vue

@@ -8,18 +8,21 @@ Page 'Mes préférences'
         <UiExpansionPanel title="message_settings" icon="fas fa-inbox">
           <v-container fluid class="container">
             <v-row>
-              <UiLoadingPanel v-if="pending" />
-              <UiForm v-else v-model="preferences" action-position="bottom">
-                <v-row>
-                  <v-col cols="12">
-                    <UiInputCheckbox
-                      v-model="preferences.messageReport"
-                      field="messageReport"
-                      label="allow_report_message"
-                    />
-                  </v-col>
-                </v-row>
-              </UiForm>
+              <UiFormEdition :model="Preferences" :id="accessProfileStore.preferencesId">
+                <template #default="{ entity : preferences }">
+                  <div v-if="preferences">
+                    <v-row>
+                      <v-col cols="12">
+                        <UiInputCheckbox
+                          v-model="preferences.messageReport"
+                          field="messageReport"
+                          label="allow_report_message"
+                        />
+                      </v-col>
+                    </v-row>
+                  </div>
+                </template>
+              </UiFormEdition>
             </v-row>
           </v-container>
         </UiExpansionPanel>
@@ -30,7 +33,6 @@ Page 'Mes préférences'
 
 <script setup lang="ts">
 import type { Ref } from 'vue'
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import Preferences from '~/models/Access/Preferences'
 import { useAccessProfileStore } from '~/stores/accessProfile'
 
@@ -43,12 +45,8 @@ if (accessProfileStore.preferencesId === null) {
   throw new Error("Missing access preference's id")
 }
 
-const { fetch } = useEntityFetch()
 const openedPanels: Ref<Array<number>> = ref([0])
-const { data: preferences, pending } = await fetch(
-  Preferences,
-  accessProfileStore.preferencesId,
-)
+
 </script>
 
 <style scoped lang="scss"></style>

+ 0 - 77
pages/organization.vue.off

@@ -1,77 +0,0 @@
-<!-- Page de détails de l'organization courante -->
-
-<template>
-  <div>
-    <LayoutContainer v-if="organization">
-      <!-- Définit le contenu des trois slots du header de la page -->
-      <LayoutBannerTop>
-        <template #block1>
-          {{ organization.name }}
-        </template>
-        <template #block2> N°Siret : {{ organization.siretNumber }} </template>
-        <template #block3>
-          {{ organization.description }}
-        </template>
-      </LayoutBannerTop>
-
-      <!-- Rend le contenu de la page -->
-      <NuxtPage />
-    </LayoutContainer>
-    <LayoutContainer v-else> Pending : {{ emPending }} </LayoutContainer>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { computed, ComputedRef } from 'vue'
-import { useEntityManager } from '~/composables/data/useEntityManager'
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
-import { useOrganizationProfileStore } from '~/stores/organizationProfile'
-import Organization from '~/models/Organization/Organization'
-import ContactPoint from '~/models/Core/ContactPoint'
-import BankAccount from '~/models/Core/BankAccount'
-import OrganizationAddressPostal from '~/models/Organization/OrganizationAddressPostal'
-import AddressPostal from '~/models/Core/AddressPostal'
-import Country from '~/models/Core/Country'
-import TypeOfPractice from '~/models/Organization/TypeOfPractice'
-import Network from '~/models/Network/Network'
-import NetworkOrganization from '~/models/Network/NetworkOrganization'
-import OrganizationArticle from '~/models/Organization/OrganizationArticle'
-import File from '~/models/Core/File'
-
-const { em, pending: emPending } = useEntityManager()
-
-const id: number | null = useOrganizationProfileStore().id
-if (id === null) {
-  throw new Error('Missing organization id')
-}
-
-const { fetch } = useEntityFetch()
-
-const { pending } = fetch(Organization, id)
-
-// Get file from store
-const organization: ComputedRef<Organization> = computed(() => {
-  return em.find(Organization, id)
-})
-
-// TODO: restaurer le middleware  (peut-être utiliser beforeMount?)
-// middleware({ $ability, redirect }) {
-//   if(!$ability.can('display', 'organization_page'))
-//     return redirect('/error')
-// }
-
-onBeforeUnmount(() => {
-  console.log('flush store') // TODO: vérifier le bon fonctionnement
-  em.flush(Organization)
-  em.flush(ContactPoint)
-  em.flush(BankAccount)
-  em.flush(OrganizationAddressPostal)
-  em.flush(AddressPostal)
-  em.flush(Country)
-  em.flush(TypeOfPractice)
-  em.flush(Network)
-  em.flush(NetworkOrganization)
-  em.flush(OrganizationArticle)
-  em.flush(File)
-})
-</script>

+ 0 - 580
pages/organization/index.vue.off

@@ -1,580 +0,0 @@
-<!--
-Contenu de la page pages/organization.vue
-Contient toutes les informations sur l'organization courante
--->
-<template>
-  <LayoutContainer>
-    <UiForm
-      v-if="!pending"
-      :model="models().Organization"
-      :entity="organization"
-    >
-      <template #form.input="{ model, entity: organization }">
-        <v-expansion-panels :value="panel" focusable accordion>
-          <!-- Description -->
-          <UiExpansionPanel id="description" icon="fa-info">
-            <v-container fluid class="container">
-              <v-row>
-                <v-col cols="12" sm="6">
-                  <UiInputText
-                    v-model="organization.name"
-                    field="name"
-                    :rules="rules.name"
-                  />
-                </v-col>
-
-                <v-col cols="12" sm="6">
-                  <UiInputText v-model="organization.acronym" field="acronym" />
-                </v-col>
-
-                <v-col
-                  v-if="organizationProfile.isInsideNetwork()"
-                  cols="12"
-                  sm="6"
-                >
-                  <UiInputText
-                    v-model="organization.identifier"
-                    :label="
-                      organizationProfile.isCmf()
-                        ? 'identifierCmf'
-                        : 'identifierFfec'
-                    "
-                    field="identifier"
-                  />
-                </v-col>
-
-                <v-col v-if="organizationProfile.isFfec()" cols="12" sm="6">
-                  <UiInputText
-                    v-model="organization.ffecApproval"
-                    field="ffecApproval"
-                  />
-                </v-col>
-
-                <v-col cols="12" sm="6">
-                  <UiInputText
-                    v-model="organization.description"
-                    field="description"
-                  />
-                </v-col>
-
-                <v-col cols="12" sm="6">
-                  <div>
-                    <span>{{ $t('logo') }}</span>
-                    <UiHelp right>
-                      <p v-html="$t('logo_upload')" />
-                    </UiHelp>
-                  </div>
-                  <UiImage
-                    :image-id="getIdFromUri(organization.logo)"
-                    :width="200"
-                    field="logo"
-                    :owner-id="id"
-                  ></UiImage>
-                </v-col>
-
-                <v-col
-                  v-if="!organizationProfile.isManagerProduct()"
-                  cols="12"
-                  sm="6"
-                >
-                  <!--                  <UiInputEnum field="principalType" v-model="organization.principalType" enum-type="organization_principal_type"/>-->
-                </v-col>
-
-                <!--                <v-col v-if="!organizationProfile.isFfec() && !organizationProfile.isManagerProduct() && !organizationProfile.isArtist()" cols="12" sm="6">-->
-                <!--                  <UiInputEnum field="schoolCategory" v-model="organization.schoolCategory" enum-type="organization_school_cat"/>-->
-                <!--                </v-col>-->
-
-                <!--                <v-col v-if="organizationProfile.isFfec()" cols="12" sm="6">-->
-                <!--                  <UiInputEnum field="typeEstablishment" v-model="organization.typeEstablishment" enum-type="organization_type_establishment"/>-->
-                <!--                </v-col>-->
-
-                <!--                <v-col v-if="organization.typeEstablishment === 'MULTIPLE'" cols="12" sm="6">-->
-                <!--                  <UiInputEnum field="typeEstablishmentDetail" v-model="organization.typeEstablishmentDetail" enum-type="organization_type_establishment_detail" />-->
-                <!--                </v-col>-->
-
-                <v-col v-if="organizationProfile.isCmf()" cols="12" sm="6">
-                  <div class="d-flex flex-row">
-                    <!--                    <UiInputAutocomplete-->
-                    <!--                      field="typeOfPractices"-->
-                    <!--                      :items="typeOfPractices"-->
-                    <!--                      :isLoading="typeOfPracticesFetchingState.pending"-->
-                    <!--                      :item-text="['name']"-->
-                    <!--                      :data="getIdsFromUris(organization.typeOfPractices)"-->
-                    <!--                      :translate="true"-->
-                    <!--                      :multiple="true"-->
-                    <!--                      group="category"-->
-                    <!--                      :rules="rules.typeOfPractice"-->
-                    <!--                      @update="updateRepository($event.map((id) => `/api/type_of_practices/${id}`), 'typeOfPractices')"-->
-                    <!--                      class="flex"-->
-                    <!--                    />-->
-                    <UiHelp>
-                      {{ $t('type_of_practices_autocomplete') }}
-                    </UiHelp>
-                  </div>
-                </v-col>
-
-                <!-- TODO: essayer de faire une condition plus explicite pour le v-if -->
-                <!--                <v-col cols="12" sm="6" v-if="getIdsFromUris(organization.typeOfPractices).indexOf(37) >= 0">-->
-                <!--                  <UiInputTextArea field="otherPractice" v-model="organization.otherPractice" />-->
-                <!--                </v-col>-->
-              </v-row>
-            </v-container>
-          </UiExpansionPanel>
-
-          <!-- Adresses -->
-          <!--          <UiExpansionPanel id="address_postal" icon="fa-globe-europe">-->
-          <!--            <v-container fluid class="container">-->
-          <!--              <v-row>-->
-          <!--                <v-col cols="12" sm="12">-->
-          <!--                  <UiCollection-->
-          <!--                    :model="models().OrganizationAddressPostal"-->
-          <!--                    :parent="entity"-->
-          <!--                    loaderType="image"-->
-          <!--                    newLink="/organization/address/new"-->
-          <!--                  >-->
-          <!--                    <template #list.item="{items}">-->
-          <!--                      <v-container fluid>-->
-          <!--                        <v-row dense>-->
-          <!--                          <v-col-->
-          <!--                            v-for="item in items"-->
-          <!--                            :key="item.id"-->
-          <!--                            cols="4"-->
-          <!--                          >-->
-          <!--                            <UiCard-->
-          <!--                              :link="`/organization/address/${item.id}`"-->
-          <!--                              :model="models().OrganizationAddressPostal"-->
-          <!--                              :entity="item"-->
-          <!--                            >-->
-          <!--                              <template #card.title>-->
-          <!--                                {{ $t(item.type) }}-->
-          <!--                              </template>-->
-          <!--                              <template #card.text>-->
-          <!--                                {{ item.addressPostal.streetAddress }} <br>-->
-          <!--                                <span v-if="item.addressPostal.streetAddressSecond">{{ item.addressPostal.streetAddressSecond }} <br></span>-->
-          <!--                                <span v-if="item.addressPostal.streetAddressThird">{{ item.addressPostal.streetAddressThird }} <br></span>-->
-          <!--                                {{ item.addressPostal.postalCode }} {{ item.addressPostal.addressCity }}<br>-->
-          <!--                                <span v-if="item.addressPostal.addressCountry">-->
-          <!--                                  <UiItemFromUri-->
-          <!--                                    :model="models().Country"-->
-          <!--                                    :query="repositories().countryRepository.query()"-->
-          <!--                                    :uri="item.addressPostal.addressCountry"-->
-          <!--                                  >-->
-          <!--                                    <template #item.text="{item}">-->
-          <!--                                      {{item.name}}-->
-          <!--                                    </template>-->
-          <!--                                  </UiItemFromUri>-->
-          <!--                                </span>-->
-          <!--                              </template>-->
-          <!--                            </UiCard>-->
-          <!--                          </v-col>-->
-          <!--                        </v-row>-->
-          <!--                      </v-container>-->
-          <!--                    </template>-->
-          <!--                  </UiCollection>-->
-          <!--                </v-col>-->
-          <!--              </v-row>-->
-          <!--            </v-container>-->
-          <!--          </UiExpansionPanel>-->
-
-          <!--          &lt;!&ndash;  Point de Contact&ndash;&gt;-->
-          <!--          <UiExpansionPanel id="contact_point" icon="fa-phone">-->
-          <!--            <v-container class="container">-->
-          <!--              <v-row>-->
-          <!--                <v-col cols="12" sm="12">-->
-          <!--                  <UiCollection-->
-          <!--                    :model="models().ContactPoint"-->
-          <!--                    :parent="entity"-->
-          <!--                    loaderType="image"-->
-          <!--                    newLink="/organization/contact_points/new"-->
-          <!--                  >-->
-          <!--                    <template #list.item="{items}">-->
-          <!--                      <v-container fluid>-->
-          <!--                        <v-row :dense="true">-->
-          <!--                          <v-col-->
-          <!--                            v-for="item in items"-->
-          <!--                            :key="item.id"-->
-          <!--                            cols="4"-->
-          <!--                          >-->
-          <!--                            <UiCard-->
-          <!--                              :link="`/organization/contact_points/${item.id}`"-->
-          <!--                              :model="models().ContactPoint"-->
-          <!--                              :entity="item"-->
-          <!--                            >-->
-          <!--                              <template #card.title>-->
-          <!--                                {{ $t(item.contactType) }}-->
-          <!--                              </template>-->
-          <!--                              <template #card.text>-->
-          <!--                                <span v-if="item.email"><strong>{{ $t('email') }}</strong> : {{ item.email }} <br></span>-->
-          <!--                                <span v-if="item.emailInvalid" class="danger&#45;&#45;text"><v-icon class="danger&#45;&#45;text">mdi-alert</v-icon> <strong>{{ $t('emailInvalid') }}</strong> : {{ item.emailInvalid }} <br></span>-->
-
-          <!--                                <span v-if="item.telphone"><strong>{{ $t('telphone') }}</strong> : {{ formatPhoneNumber(item.telphone) }} <br></span>-->
-          <!--                                <span v-if="item.telphoneInvalid" class="danger&#45;&#45;text"><v-icon class="danger&#45;&#45;text">mdi-alert</v-icon> <strong>{{ $t('telphoneInvalid') }}</strong> : {{ formatPhoneNumber(item.telphoneInvalid) }} <br></span>-->
-
-          <!--                                <span v-if="item.mobilPhone"><strong>{{ $t('mobilPhone') }}</strong> : {{ formatPhoneNumber(item.mobilPhone) }} <br></span>-->
-          <!--                                <span v-if="item.mobilPhoneInvalid" class="danger&#45;&#45;text"><v-icon class="danger&#45;&#45;text">mdi-alert</v-icon> <strong>{{ $t('mobilPhoneInvalid') }}</strong> : {{ formatPhoneNumber(item.mobilPhoneInvalid) }} </span>-->
-          <!--                              </template>-->
-          <!--                            </UiCard>-->
-          <!--                          </v-col>-->
-          <!--                        </v-row>-->
-          <!--                      </v-container>-->
-          <!--                    </template>-->
-          <!--                  </UiCollection>-->
-          <!--                </v-col>-->
-          <!--              </v-row>-->
-          <!--            </v-container>-->
-          <!--          </UiExpansionPanel>-->
-
-          <!--          &lt;!&ndash; Informations légales &ndash;&gt;-->
-          <!--          <UiExpansionPanel id="legalInformation" icon="fa-gavel">-->
-          <!--            <v-container fluid class="container">-->
-          <!--              <v-row>-->
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText-->
-          <!--                    field="siretNumber"-->
-          <!--                    :data="entity['siretNumber']"-->
-          <!--                    :error="siretError"-->
-          <!--                    :error-message="siretErrorMessage"-->
-          <!--                    :rules="rules.siretRule"-->
-          <!--                    @update="checkSiretHook($event, 'siretNumber')"-->
-          <!--                  />-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="apeNumber" :data="entity['apeNumber']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col v-if="entity['legalStatus'] === 'ASSOCIATION_LAW_1901'" cols="12" sm="6">-->
-          <!--                  <UiInputText field="waldecNumber" :data="entity['waldecNumber']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputDatePicker field="creationDate" :data="entity['creationDate']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="prefectureName" :data="entity['prefectureName']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="prefectureNumber" :data="entity['prefectureNumber']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputDatePicker field="declarationDate" :data="entity['declarationDate']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="tvaNumber" :data="entity['tvaNumber']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputEnum field="legalStatus" :data="entity['legalStatus']" enum-type="organization_legal"/>-->
-          <!--                </v-col>-->
-
-          <!--              </v-row>-->
-          <!--            </v-container>-->
-          <!--          </UiExpansionPanel>-->
-
-          <!--          &lt;!&ndash;  Agréments &ndash;&gt;-->
-          <!--          <UiExpansionPanel id="agrements" icon="fa-certificate">-->
-          <!--            <v-container class="container">-->
-          <!--              <v-row>-->
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="youngApproval" :data="entity['youngApproval']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="trainingApproval" :data="entity['trainingApproval']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="otherApproval" :data="entity['otherApproval']"/>-->
-          <!--                </v-col>-->
-          <!--              </v-row>-->
-          <!--            </v-container>-->
-          <!--          </UiExpansionPanel>-->
-
-          <!--          &lt;!&ndash; Salariés &ndash;&gt;-->
-          <!--          <UiExpansionPanel id="salary" icon="fa-users">-->
-          <!--            <v-container class="container">-->
-          <!--              <v-row>-->
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="collectiveAgreement" :data="entity['collectiveAgreement']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputEnum field="opca" :data="entity['opca']" enum-type="organization_opca"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="icomNumber" :data="entity['icomNumber']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="urssafNumber" :data="entity['urssafNumber']"/>-->
-          <!--                </v-col>-->
-          <!--              </v-row>-->
-          <!--            </v-container>-->
-          <!--          </UiExpansionPanel>-->
-
-          <!--          &lt;!&ndash; Réseaux &ndash;&gt;-->
-          <!--          <UiExpansionPanel v-if="organizationProfile.isInsideNetwork()" id="network" icon="fa-share-alt">-->
-          <!--            <v-container class="container">-->
-          <!--              <v-row>-->
-          <!--                <v-col cols="12" sm="12">-->
-          <!--                  <UiCollection-->
-          <!--                    :model="models().NetworkOrganization"-->
-          <!--                    :parent="entity"-->
-          <!--                    loaderType="text"-->
-          <!--                  >-->
-          <!--                    <template #list.item="{items}">-->
-          <!--                      <div v-for="item in items" :key="item.id">-->
-          <!--                        <span>{{ item.network.name }}</span> - <span>{{$t('first_subscription')}} : <UiTemplateDate :data="item.startDate" /></span>-->
-          <!--                      </div>-->
-          <!--                    </template>-->
-          <!--                  </UiCollection>-->
-          <!--                </v-col>-->
-          <!--                <v-col v-if="organizationProfile.isFfec()" cols="12" sm="6">-->
-          <!--                  <UiInputText field="budget" :data="entity['budget']" type="number" />-->
-          <!--                </v-col>-->
-
-          <!--                <v-col v-if="organizationProfile.isFfec()" cols="12" sm="6">-->
-          <!--                  <UiInputCheckbox field="isPedagogicIsPrincipalActivity" :data="entity['isPedagogicIsPrincipalActivity']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col v-if="organizationProfile.isFfec()" cols="12" sm="6">-->
-          <!--                  <UiInputText field="pedagogicBudget" :data="entity['pedagogicBudget']" type="number"/>-->
-          <!--                </v-col>-->
-          <!--              </v-row>-->
-          <!--            </v-container>-->
-          <!--          </UiExpansionPanel>-->
-
-          <!--          &lt;!&ndash; Communication &ndash;&gt;-->
-          <!--          <UiExpansionPanel id="communication" icon="fa-rss">-->
-          <!--            <v-container class="container">-->
-          <!--              <v-row>-->
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="twitter" :data="entity['twitter']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="youtube" :data="entity['youtube']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="facebook" :data="entity['facebook']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputText field="instagram" :data="entity['instagram']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <UiInputCheckbox field="portailVisibility" :data="entity['portailVisibility']"/>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="6">-->
-          <!--                  <div class="d-flex flex-column">-->
-          <!--                    <UiHelp class="d-flex flex-row">-->
-          <!--                      <span>{{ $t('image') }}</span>-->
-          <!--                      <p v-html="$t('communication_image_upload')"/>-->
-          <!--                    </UiHelp>-->
-          <!--                    <UiImage-->
-          <!--                      :id="getIdFromUri(entity['image'])"-->
-          <!--                      :upload="true"-->
-          <!--                      :width="200"-->
-          <!--                      field="image"-->
-          <!--                      :ownerId="id"-->
-
-          <!--                    ></UiImage>-->
-          <!--                  </div>-->
-          <!--                </v-col>-->
-
-          <!--                <v-col cols="12" sm="12">-->
-          <!--                  <UiCollection-->
-          <!--                    :model="models().OrganizationArticle"-->
-          <!--                    :parent="entity"-->
-          <!--                    loaderType="text"-->
-          <!--                  >-->
-          <!--                    <template #list.item="{items}">-->
-          <!--                      <h4 class="neutral-strong&#45;&#45;text font-weight-regular">{{$t('organizationArticle')}}</h4>-->
-          <!--                      <UiTemplateDataTable-->
-          <!--                        :headers="[-->
-          <!--                          { text: $t('title'), value: 'title' },-->
-          <!--                          { text: $t('link'), value: 'link' },-->
-          <!--                          { text: $t('date'), value: 'date' },-->
-          <!--                        ]"-->
-          <!--                        :items="items"-->
-          <!--                      >-->
-          <!--                        <template #item.date="{item}">-->
-          <!--                          <UiTemplateDate :data="item.date" />-->
-          <!--                        </template>-->
-          <!--                      </UiTemplateDataTable>-->
-          <!--                    </template>-->
-          <!--                  </UiCollection>-->
-          <!--                </v-col>-->
-
-          <!--              </v-row>-->
-          <!--            </v-container>-->
-          <!--          </UiExpansionPanel>-->
-
-          <!--          &lt;!&ndash; IBAN &ndash;&gt;-->
-          <!--          <UiExpansionPanel id="bank_account" icon="fa-euro-sign">-->
-          <!--            <v-container class="container">-->
-          <!--              <v-row>-->
-          <!--                <v-col cols="12" sm="12">-->
-          <!--                  <UiCollection-->
-          <!--                    :model="models().BankAccount"-->
-          <!--                    :parent="entity"-->
-          <!--                    loaderType="image"-->
-          <!--                    newLink="/organization/bank_account/new"-->
-          <!--                  >-->
-          <!--                    <template #list.item="{items}">-->
-          <!--                      <v-container fluid>-->
-          <!--                        <v-row :dense="true">-->
-          <!--                          <v-col-->
-          <!--                            v-for="item in items"-->
-          <!--                            :key="item.id"-->
-          <!--                            cols="4"-->
-          <!--                          >-->
-          <!--                            <UiCard-->
-          <!--                              :id="item.id"-->
-          <!--                              :link="`/organization/bank_account/${item.id}`"-->
-          <!--                              :model="models().BankAccount"-->
-          <!--                            >-->
-          <!--                              <template #card.text>-->
-          <!--                                <span v-if="item.bankName"><strong>{{ $t('bankName') }}</strong> : {{ item.bankName }} <br></span>-->
-
-          <!--                                <span v-if="item.bic"><strong>{{ $t('bic') }}</strong> : {{ item.bic }} <br></span>-->
-          <!--                                <span v-if="item.bicInvalid" class="danger&#45;&#45;text"><v-icon class="danger&#45;&#45;text">mdi-alert</v-icon> <strong>{{ $t('bicInvalid') }}</strong> : {{ item.bicInvalid }} <br></span>-->
-
-          <!--                                <span v-if="item.iban"><strong>{{ $t('iban') }}</strong> : {{ item.iban }} <br></span>-->
-          <!--                                <span v-if="item.ibanInvalid" class="danger&#45;&#45;text"><v-icon class="danger&#45;&#45;text">mdi-alert</v-icon> <strong>{{ $t('ibanInvalid') }}</strong> : {{ item.ibanInvalid }} <br></span>-->
-
-          <!--                              </template>-->
-          <!--                            </UiCard>-->
-          <!--                          </v-col>-->
-          <!--                        </v-row>-->
-          <!--                      </v-container>-->
-          <!--                    </template>-->
-          <!--                  </UiCollection>-->
-          <!--                </v-col>-->
-          <!--              </v-row>-->
-          <!--            </v-container>-->
-          <!--          </UiExpansionPanel>-->
-        </v-expansion-panels>
-      </template>
-    </UiForm>
-  </LayoutContainer>
-</template>
-
-<script setup lang="ts">
-import { computed, reactive, ref } from 'vue'
-import type { ComputedRef } from 'vue'
-import { useRoute } from '#app'
-import { useI18n } from 'vue-i18n'
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
-import TypeOfPractice from '~/models/Organization/TypeOfPractice'
-import { useExtensionPanel } from '~/composables/layout/useExtensionPanel'
-import { useValidation } from '~/composables/form/useValidation'
-import { useEntityManager } from '~/composables/data/useEntityManager'
-import UrlUtils from '~/services/utils/urlUtils'
-import Organization from '~/models/Organization/Organization'
-import { useI18nUtils } from '~/composables/utils/useI18nUtils'
-import ContactPoint from '~/models/Core/ContactPoint'
-import BankAccount from '~/models/Core/BankAccount'
-import OrganizationAddressPostal from '~/models/Organization/OrganizationAddressPostal'
-import Country from '~/models/Core/Country'
-import NetworkOrganization from '~/models/Network/NetworkOrganization'
-import OrganizationArticle from '~/models/Organization/OrganizationArticle'
-
-const id: number | null = useOrganizationProfileStore().id
-if (id === null) {
-  throw new Error('Missing organization id')
-}
-
-const organizationProfile = reactive($organizationProfile())
-
-const { em } = useEntityManager()
-const { fetch, fetchCollection } = useEntityFetch()
-const { pending } = fetch(Organization, id)
-
-const organization: ComputedRef<Organization> = computed(() => {
-  return em.find(Organization, id)
-})
-
-const { data: typeOfPractices, pending: typeOfPracticesPending } =
-  fetchCollection(TypeOfPractice)
-
-const route = ref(useRoute())
-const { panel } = useExtensionPanel(route)
-
-const { siretError, siretErrorMessage, validateSiret } =
-  useValidation().useValidateSiret()
-
-const validateSiretHook = async (
-  siret: string,
-  field: string,
-  updateRepository: any,
-) => {
-  await validateSiret(siret)
-  if (!siretError.value) {
-    em.save(Organization, organization.value)
-  }
-}
-
-const formatPhoneNumber = (number: string): string => {
-  return useI18nUtils().formatPhoneNumber(number)
-}
-
-// TODO: voir si l'extraction de cette id ne pourrait pas être faite en amont, au niveau des post-processors
-const getIdsFromUris = (uris: Array<string>) => {
-  const ids: Array<any> = []
-  for (const uri of uris) {
-    ids.push(UrlUtils.extractIdFromUri(uri))
-  }
-  return ids
-}
-
-// TODO: voir si l'extraction de cette id ne pourrait pas être faite en amont, au niveau des post-processors
-const getIdFromUri = (uri: string) => UrlUtils.extractIdFromUri(uri)
-
-const models = () => {
-  return {
-    Organization,
-    ContactPoint,
-    BankAccount,
-    OrganizationAddressPostal,
-    Country,
-    NetworkOrganization,
-    OrganizationArticle,
-  }
-}
-
-const i18n = useI18n()
-
-const rules = {
-  name: [
-    (nameValue: string) => !!nameValue || i18n.t('required'),
-    (nameValue: string) =>
-      (nameValue || '').length <= 128 || i18n.t('name_length_rule'),
-  ],
-  siret: [
-    (siretValue: string) =>
-      /^([0-9]{9}|[0-9]{14})$/.test(siretValue) || i18n.t('siret_error'),
-  ],
-  typeOfPractice: [
-    (typeOfPracticeValue: Array<number>) => {
-      if (!$organizationProfile().isManagerProduct())
-        return typeOfPracticeValue.length > 0 || i18n.t('required')
-      return true
-    },
-  ],
-}
-</script>
-
-<style scoped>
-.v-icon.v-icon {
-  font-size: 14px;
-}
-</style>

+ 1 - 12
pages/parameters/attendance_booking_reasons/[id].vue

@@ -6,28 +6,17 @@
         go-back-route="/parameters/attendances"
       >
         <template #default="{ entity }">
-          <UiInputText
-            v-model="entity.reason"
-            field="reason"
-            :rules="rules()"
-          />
+          <FormParameterAttendanceBookingReason v-if="entity !== null" :entity="entity" />
         </template>
       </UiFormEdition>
     </LayoutCommonSection>
   </div>
 </template>
 <script setup lang="ts">
-import { useI18n } from 'vue-i18n'
 import AttendanceBookingReason from '~/models/Booking/AttendanceBookingReason'
 
 definePageMeta({
   name: 'attendanceBookingReason',
 })
 
-const i18n = useI18n()
-
-const rules = () => [
-  (reason: string | null) =>
-    (reason !== null && reason.length > 0) || i18n.t('please_enter_a_value'),
-]
 </script>

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

@@ -6,20 +6,7 @@
         go-back-route="/parameters/attendances"
       >
         <template #default="{ entity }">
-          <v-container :fluid="true" class="container">
-            <v-row>
-              <v-col cols="12" sm="6" />
-            </v-row>
-            <v-row>
-              <v-col cols="12" sm="6">
-                <UiInputText
-                  v-model="entity.reason"
-                  field="reason"
-                  :rules="rules()"
-                />
-              </v-col>
-            </v-row>
-          </v-container>
+          <FormParameterAttendanceBookingReason :entity="entity" />
         </template>
       </UiFormCreation>
     </LayoutCommonSection>
@@ -27,17 +14,10 @@
 </template>
 
 <script setup lang="ts">
-import { useI18n } from 'vue-i18n'
 import AttendanceBookingReason from '~/models/Booking/AttendanceBookingReason'
 
 definePageMeta({
   name: 'new_attendance_booking_reason',
 })
 
-const i18n = useI18n()
-
-const rules = () => [
-  (reason: string | null) =>
-    (reason !== null && reason.length > 0) || i18n.t('please_enter_a_value'),
-]
 </script>

+ 31 - 43
pages/parameters/attendances.vue

@@ -2,35 +2,38 @@
   <div>
     <LayoutCommonSection v-if="organizationProfile.isSchool">
       <h4>{{ $t('alert_configuration') }}</h4>
-      <UiLoadingPanel v-if="pending" />
-      <UiForm v-else-if="parameters !== null" v-model="parameters">
-        <v-row>
-          <v-col cols="12">
-            <UiInputCheckbox
-              v-model="parameters.sendAttendanceEmail"
-              field="sendAttendanceEmail"
-              label="sendAttendanceEmail"
-            />
+      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
+        <template #default="{ entity : parameters }">
+          <div v-if="parameters">
+            <v-row>
+              <v-col cols="12">
+                <UiInputCheckbox
+                  v-model="parameters.sendAttendanceEmail"
+                  field="sendAttendanceEmail"
+                  label="sendAttendanceEmail"
+                />
 
-            <UiInputCheckbox
-              v-model="parameters.sendAttendanceSms"
-              field="sendAttendanceSms"
-            />
+                <UiInputCheckbox
+                  v-model="parameters.sendAttendanceSms"
+                  field="sendAttendanceSms"
+                />
 
-            <UiInputCheckbox
-              v-model="parameters.notifyAdministrationAbsence"
-              field="notifyAdministrationAbsence"
-            />
+                <UiInputCheckbox
+                  v-model="parameters.notifyAdministrationAbsence"
+                  field="notifyAdministrationAbsence"
+                />
 
-            <UiInputNumber
-              v-if="parameters.notifyAdministrationAbsence"
-              v-model="parameters.numberConsecutiveAbsences"
-              field="numberConsecutiveAbsences"
-              :rules="rules()"
-            />
-          </v-col>
-        </v-row>
-      </UiForm>
+                <UiInputNumber
+                  v-if="parameters.notifyAdministrationAbsence"
+                  v-model="parameters.numberConsecutiveAbsences"
+                  field="numberConsecutiveAbsences"
+                  :rules="getAsserts('numberConsecutiveAbsences')"
+                />
+              </v-col>
+            </v-row>
+          </div>
+        </template>
+      </UiFormEdition>
     </LayoutCommonSection>
 
     <LayoutCommonSection>
@@ -43,36 +46,21 @@
   </div>
 </template>
 <script setup lang="ts">
-import type { AsyncData } from '#app'
 import Parameters from '~/models/Organization/Parameters'
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
-import UrlUtils from '~/services/utils/urlUtils'
 import AttendanceBookingReason from '~/models/Booking/AttendanceBookingReason'
+import {getAssertUtils} from "~/services/asserts/getAssertUtils";
 
 definePageMeta({
   name: 'parameters_attendances_page',
 })
 
-const { fetch } = useEntityFetch()
-
-const i18n = useI18n()
-
 const organizationProfile = useOrganizationProfileStore()
 
 if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
 
-const { data: parameters, pending } = fetch(
-  Parameters,
-  organizationProfile.parametersId,
-) as AsyncData<Parameters | null, Error | null>
+const getAsserts = (key) => getAssertUtils(Parameters.getAsserts(), key)
 
-const rules = () => [
-  (numberConsecutiveAbsences: string | null) =>
-    (numberConsecutiveAbsences !== null &&
-      parseInt(numberConsecutiveAbsences) > 0) ||
-    i18n.t('please_enter_a_value'),
-]
 </script>

+ 70 - 75
pages/parameters/bulletin.vue

@@ -1,99 +1,94 @@
 <template>
   <LayoutContainer>
     <LayoutCommonSection>
-      <UiLoadingPanel v-if="pending" />
-      <UiForm v-else v-model="parameters">
-        <v-row>
-          <v-col cols="12">
-            <h4 class="mb-8">{{ $t('itemsToDisplayOnBulletins') }}</h4>
-
-            <UiInputCheckbox
-              v-model="parameters.bulletinWithTeacher"
-              field="bulletinWithTeacher"
-            />
-
-            <UiInputCheckbox
-              v-model="parameters.bulletinSignatureDirector"
-              field="bulletinSignatureDirector"
-            />
-
-            <UiInputCheckbox
-              v-model="parameters.bulletinShowEducationWithoutEvaluation"
-              field="bulletinShowEducationWithoutEvaluation"
-            />
-
-            <UiInputCheckbox
-              v-model="parameters.bulletinShowAbsences"
-              field="bulletinShowAbsences"
-            />
-
-            <UiInputCheckbox
-              v-model="parameters.bulletinPrintAddress"
-              field="bulletinPrintAddress"
-            />
-
-            <UiInputCheckbox
-              v-model="parameters.bulletinDisplayLevelAcquired"
-              field="bulletinDisplayLevelAcquired"
-            />
-
-            <UiInputCheckbox
-              v-model="parameters.bulletinViewTestResults"
-              field="bulletinViewTestResults"
-            />
-
-            <UiInputCheckbox
-              v-model="parameters.bulletinShowAverages"
-              field="bulletinShowAverages"
-            />
-
-            <h4 class="my-8">{{ $t('bulletinSettings') }}</h4>
-
-            <UiInputAutocompleteEnum
-              v-model="parameters.bulletinCriteriaSort"
-              field="bulletinCriteriaSort"
-              enum-name="organization_bulletin_criteria_sort"
-            />
-
-            <UiInputAutocompleteEnum
-              v-model="parameters.bulletinReceiver"
-              field="bulletinReceiver"
-              enum-name="organization_bulletin_send_to"
-            />
-
-            <UiInputCheckbox
-              v-model="parameters.bulletinEditWithoutEvaluation"
-              field="bulletinEditWithoutEvaluation"
-            />
-          </v-col>
-        </v-row>
-      </UiForm>
+      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
+        <template #default="{ entity : parameters }">
+          <div v-if="parameters">
+            <v-row>
+              <v-col cols="12">
+                <h4 class="mb-8">{{ $t('itemsToDisplayOnBulletins') }}</h4>
+
+                <UiInputCheckbox
+                  v-model="parameters.bulletinWithTeacher"
+                  field="bulletinWithTeacher"
+                />
+
+                <UiInputCheckbox
+                  v-model="parameters.bulletinSignatureDirector"
+                  field="bulletinSignatureDirector"
+                />
+
+                <UiInputCheckbox
+                  v-model="parameters.bulletinShowEducationWithoutEvaluation"
+                  field="bulletinShowEducationWithoutEvaluation"
+                />
+
+                <UiInputCheckbox
+                  v-model="parameters.bulletinShowAbsences"
+                  field="bulletinShowAbsences"
+                />
+
+                <UiInputCheckbox
+                  v-model="parameters.bulletinPrintAddress"
+                  field="bulletinPrintAddress"
+                />
+
+                <UiInputCheckbox
+                  v-model="parameters.bulletinDisplayLevelAcquired"
+                  field="bulletinDisplayLevelAcquired"
+                />
+
+                <UiInputCheckbox
+                  v-model="parameters.bulletinViewTestResults"
+                  field="bulletinViewTestResults"
+                />
+
+                <UiInputCheckbox
+                  v-model="parameters.bulletinShowAverages"
+                  field="bulletinShowAverages"
+                />
+
+                <h4 class="my-8">{{ $t('bulletinSettings') }}</h4>
+
+                <UiInputAutocompleteEnum
+                  v-model="parameters.bulletinCriteriaSort"
+                  field="bulletinCriteriaSort"
+                  enum-name="organization_bulletin_criteria_sort"
+                />
+
+                <UiInputAutocompleteEnum
+                  v-model="parameters.bulletinReceiver"
+                  field="bulletinReceiver"
+                  enum-name="organization_bulletin_send_to"
+                />
+
+                <UiInputCheckbox
+                  v-model="parameters.bulletinEditWithoutEvaluation"
+                  field="bulletinEditWithoutEvaluation"
+                />
+              </v-col>
+            </v-row>
+          </div>
+        </template>
+      </UiFormEdition>
     </LayoutCommonSection>
   </LayoutContainer>
 </template>
 
 <script setup lang="ts">
-import type { AsyncData } from '#app'
 import Parameters from '~/models/Organization/Parameters'
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 
 definePageMeta({
   name: 'parameters_bulletin_page',
 })
 
-const { fetch } = useEntityFetch()
 
 const organizationProfile = useOrganizationProfileStore()
 
 if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
-
-const { data: parameters, pending } = fetch(
-  Parameters,
-  organizationProfile.parametersId,
-) as AsyncData<Parameters, Parameters | true>
 </script>
 
 <style scoped lang="scss"></style>

+ 8 - 7
pages/parameters/cycles/[id].vue

@@ -3,24 +3,25 @@
     <LayoutCommonSection>
       <UiFormEdition :model="Cycle" go-back-route="/parameters/teaching">
         <template #default="{ entity }">
-          <UiInputText v-model="entity.label" field="label" :rules="rules()" />
+          <UiInputText
+            v-if="entity !== null"
+            v-model="entity.label"
+            field="label"
+            :rules="getAsserts('label')"
+          />
         </template>
       </UiFormEdition>
     </LayoutCommonSection>
   </div>
 </template>
 <script setup lang="ts">
-import { useI18n } from 'vue-i18n'
 import Cycle from '~/models/Education/Cycle'
+import {getAssertUtils} from "~/services/asserts/getAssertUtils";
 
 definePageMeta({
   name: 'cycle',
 })
 
-const i18n = useI18n()
+const getAsserts = (key) => getAssertUtils(Cycle.getAsserts(), key)
 
-const rules = () => [
-  (label: string | null) =>
-    (label !== null && label.length > 0) || i18n.t('please_enter_a_value'),
-]
 </script>

+ 46 - 50
pages/parameters/education_notation.vue

@@ -1,79 +1,75 @@
 <template>
   <LayoutContainer>
     <LayoutCommonSection>
-      <UiLoadingPanel v-if="pending" />
-      <UiForm v-else v-model="parameters">
-        <v-row>
-          <v-col cols="12">
-            <UiInputCheckbox
-              v-model="parameters.periodValidation"
-              field="periodValidation"
-              label="define_validation_periods_for_teachers"
-            />
+      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
+        <template #default="{ entity : parameters }">
+          <div v-if="parameters">
+            <v-row>
+              <v-col cols="12">
+                <UiInputCheckbox
+                  v-model="parameters.periodValidation"
+                  field="periodValidation"
+                  label="define_validation_periods_for_teachers"
+                />
 
-            <UiInputCheckbox
-              v-model="parameters.editCriteriaNotationByAdminOnly"
-              field="editCriteriaNotationByAdminOnly"
-              label="evaluation_criterium_edition_is_admin_only"
-            />
+                <UiInputCheckbox
+                  v-model="parameters.editCriteriaNotationByAdminOnly"
+                  field="editCriteriaNotationByAdminOnly"
+                  label="evaluation_criterium_edition_is_admin_only"
+                />
 
-            <UiInputCheckbox
-              v-model="parameters.requiredValidation"
-              field="requiredValidation"
-              label="mandatory_validation_for_evaluations"
-            />
+                <UiInputCheckbox
+                  v-model="parameters.requiredValidation"
+                  field="requiredValidation"
+                  label="mandatory_validation_for_evaluations"
+                />
 
-            <UiInputAutocompleteEnum
-              v-if="organizationProfile.hasModule('AdvancedEducationNotation')"
-              v-model="parameters.advancedEducationNotationType"
-              enum-name="advanced_education_notation"
-              field="advancedEducationNotationType"
-            />
+                <UiInputAutocompleteEnum
+                  v-if="organizationProfile.hasModule('AdvancedEducationNotation')"
+                  v-model="parameters.advancedEducationNotationType"
+                  enum-name="advanced_education_notation"
+                  field="advancedEducationNotationType"
+                />
 
-            <UiInputAutocompleteEnum
-              v-model="parameters.educationPeriodicity"
-              enum-name="education_periodicity"
-              field="educationPeriodicity"
-            />
+                <UiInputAutocompleteEnum
+                  v-model="parameters.educationPeriodicity"
+                  enum-name="education_periodicity"
+                  field="educationPeriodicity"
+                />
 
-            <UiInputNumber
-              v-model="parameters.average"
-              field="average"
-              label="max_note_for_pedagogical_followup"
-              :default="20"
-              :min="1"
-              :max="100"
-              class="mt-2"
-            />
-          </v-col>
-        </v-row>
-      </UiForm>
+                <UiInputNumber
+                  v-model="parameters.average"
+                  field="average"
+                  label="max_note_for_pedagogical_followup"
+                  :default="20"
+                  :min="1"
+                  :max="100"
+                  class="mt-2"
+                />
+              </v-col>
+            </v-row>
+          </div>
+        </template>
+      </UiFormEdition>
     </LayoutCommonSection>
   </LayoutContainer>
+
 </template>
 
 <script setup lang="ts">
-import type { AsyncData } from '#app'
 import Parameters from '~/models/Organization/Parameters'
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 
 definePageMeta({
   name: 'parameters_education_notation_page',
 })
 
-const { fetch } = useEntityFetch()
-
 const organizationProfile = useOrganizationProfileStore()
 
 if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
 
-const { data: parameters, pending } = fetch(
-  Parameters,
-  organizationProfile.parametersId,
-) as AsyncData<Parameters | null, Error | null>
 </script>
 
 <style scoped lang="scss"></style>

+ 1 - 12
pages/parameters/education_timings/[id].vue

@@ -6,28 +6,17 @@
         go-back-route="/parameters/education_timings"
       >
         <template #default="{ entity }">
-          <UiInputNumber
-            v-model="entity.timing"
-            field="educationTiming"
-            :rules="rules()"
-          />
+          <FormParameterEducationTiming v-if="entity !== null" :entity="entity" />
         </template>
       </UiFormEdition>
     </LayoutCommonSection>
   </div>
 </template>
 <script setup lang="ts">
-import { useI18n } from 'vue-i18n'
 import EducationTiming from '~/models/Education/EducationTiming'
 
 definePageMeta({
   name: 'educationTiming',
 })
 
-const i18n = useI18n()
-
-const rules = () => [
-  (timing: string | null) =>
-    (timing !== null && parseInt(timing) > 0) || i18n.t('please_enter_a_value'),
-]
 </script>

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

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

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

@@ -6,20 +6,7 @@
         go-back-route="/parameters/education_timings"
       >
         <template #default="{ entity }">
-          <v-container :fluid="true" class="container">
-            <v-row>
-              <v-col cols="12" sm="6" />
-            </v-row>
-            <v-row>
-              <v-col cols="12" sm="6">
-                <UiInputNumber
-                  v-model="entity.timing"
-                  field="new_education_timings"
-                  :rules="rules()"
-                />
-              </v-col>
-            </v-row>
-          </v-container>
+          <FormParameterEducationTiming :entity="entity" />
         </template>
       </UiFormCreation>
     </LayoutCommonSection>
@@ -27,17 +14,10 @@
 </template>
 
 <script setup lang="ts">
-import { useI18n } from 'vue-i18n'
 import EducationTiming from '~/models/Education/EducationTiming'
 
 definePageMeta({
   name: 'new_education_timing',
 })
 
-const i18n = useI18n()
-
-const rules = () => [
-  (timing: number | null) =>
-    (timing !== null && timing > 0) || i18n.t('please_enter_a_value'),
-]
 </script>

+ 70 - 74
pages/parameters/general_parameters.vue

@@ -1,113 +1,109 @@
 <template>
   <LayoutContainer>
     <LayoutCommonSection>
-      <UiLoadingPanel v-if="pending" />
-      <UiForm v-else-if="parameters !== null" v-model="parameters">
-        <v-row>
-          <v-col cols="12">
-            <UiInputDatePicker
-              v-if="
+      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
+        <template #default="{ entity : parameters }">
+          <div v-if="parameters">
+            <v-row>
+              <v-col cols="12">
+                <UiInputDatePicker
+                  v-if="
                 organizationProfile.isSchool ||
                 organizationProfile.isManagerProduct
               "
-              v-model="parameters.financialDate"
-              field="financialDate"
-              label="start_date_of_financial_season"
-              position="left"
-              class="my-2"
-            />
+                  v-model="parameters.financialDate"
+                  field="financialDate"
+                  label="start_date_of_financial_season"
+                  position="left"
+                  class="my-2"
+                />
 
-            <UiInputDatePicker
-              v-if="organizationProfile.isSchool"
-              v-model="parameters.musicalDate"
-              field="musicalDate"
-              label="start_date_of_activity_season"
-              position="left"
-              class="my-2"
-            />
+                <UiInputDatePicker
+                  v-if="organizationProfile.isSchool"
+                  v-model="parameters.musicalDate"
+                  field="musicalDate"
+                  label="start_date_of_activity_season"
+                  position="left"
+                  class="my-2"
+                />
 
-            <UiInputDatePicker
-              v-if="organizationProfile.isSchool"
-              v-model="parameters.startCourseDate"
-              field="startCourseDate"
-              label="start_date_of_courses"
-              position="left"
-              class="my-2"
-            />
+                <UiInputDatePicker
+                  v-if="organizationProfile.isSchool"
+                  v-model="parameters.startCourseDate"
+                  field="startCourseDate"
+                  label="start_date_of_courses"
+                  position="left"
+                  class="my-2"
+                />
 
-            <UiInputDatePicker
-              v-if="organizationProfile.isSchool"
-              v-model="parameters.endCourseDate"
-              field="endCourseDate"
-              label="end_date_of_courses"
-              position="left"
-              class="my-2"
-            />
+                <UiInputDatePicker
+                  v-if="organizationProfile.isSchool"
+                  v-model="parameters.endCourseDate"
+                  field="endCourseDate"
+                  label="end_date_of_courses"
+                  position="left"
+                  class="my-2"
+                />
 
-            <UiInputAutocompleteEnum
-              v-model="parameters.timezone"
-              enum-name="timezone"
-              field="timezone"
-            />
+                <UiInputAutocompleteEnum
+                  v-model="parameters.timezone"
+                  enum-name="timezone"
+                  field="timezone"
+                />
 
-            <UiInputCheckbox
-              v-model="parameters.showAdherentList"
-              field="showAdherentList"
-              label="show_adherents_list_and_their_coordinates"
-            />
+                <UiInputCheckbox
+                  v-model="parameters.showAdherentList"
+                  field="showAdherentList"
+                  label="show_adherents_list_and_their_coordinates"
+                />
 
-            <UiInputCheckbox
-              v-if="
+                <UiInputCheckbox
+                  v-if="
                 organizationProfile.isSchool &&
                 organizationProfile.isAssociation
               "
-              v-model="parameters.studentsAreAdherents"
-              field="studentsAreAdherents"
-              label="students_are_also_association_members"
-            />
+                  v-model="parameters.studentsAreAdherents"
+                  field="studentsAreAdherents"
+                  label="students_are_also_association_members"
+                />
 
-            <div
-              v-if="organizationProfile.isCMFCentralService"
-              class="d-flex flex-column"
-            >
+                <div
+                  v-if="organizationProfile.isCMFCentralService"
+                  class="d-flex flex-column"
+                >
               <span class="mb-1 v-label" style="font-size: 12px"
-                >{{ $t('licenceQrCode') }}
+              >{{ $t('licenceQrCode') }}
               </span>
-              <UiInputImage
-                v-model="parameters.qrCode"
-                field="qrCode"
-                :width="120"
-                :cropping-enabled="true"
-              />
-            </div>
-          </v-col>
-        </v-row>
-      </UiForm>
+                  <UiInputImage
+                    v-model="parameters.qrCode"
+                    field="qrCode"
+                    :width="120"
+                    :cropping-enabled="true"
+                  />
+                </div>
+              </v-col>
+            </v-row>
+          </div>
+        </template>
+      </UiFormEdition>
     </LayoutCommonSection>
   </LayoutContainer>
 </template>
 
 <script setup lang="ts">
 import Parameters from '~/models/Organization/Parameters'
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 
 definePageMeta({
   name: 'parameters_general_page',
 })
 
-const { fetch } = useEntityFetch()
-
 const organizationProfile = useOrganizationProfileStore()
 
 if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
 
-const { data: parameters, pending } = fetch(
-  Parameters,
-  organizationProfile.parametersId,
-)
 </script>
 
 <style scoped lang="scss"></style>

+ 42 - 47
pages/parameters/intranet.vue

@@ -1,77 +1,72 @@
 <template>
   <LayoutContainer>
     <LayoutCommonSection>
-      <UiLoadingPanel v-if="pending" />
-      <UiForm v-else v-model="parameters">
-        <v-row>
-          <v-col cols="12">
-            <h4 class="mb-4">{{ $t('teachers') }}</h4>
+      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
+        <template #default="{ entity : parameters }">
+          <div v-if="parameters">
+            <v-row>
+              <v-col cols="12">
+                <h4 class="mb-4">{{ $t('teachers') }}</h4>
 
-            <UiInputCheckbox
-              v-model="parameters.createCourse"
-              field="createCourse"
-              label="allow_teachers_to_create_courses"
-            />
+                <UiInputCheckbox
+                  v-model="parameters.createCourse"
+                  field="createCourse"
+                  label="allow_teachers_to_create_courses"
+                />
 
-            <UiInputCheckbox
-              v-model="parameters.consultTeacherListing"
-              field="consultTeacherListing"
-              label="allow_teachers_to_consult_colleagues_informations"
-            />
+                <UiInputCheckbox
+                  v-model="parameters.consultTeacherListing"
+                  field="consultTeacherListing"
+                  label="allow_teachers_to_consult_colleagues_informations"
+                />
 
-            <UiInputCheckbox
-              v-model="parameters.consultPedagogicResult"
-              field="showAdherentList"
-              label="allow_students_to_consult_their_pedagogical_followup"
-            />
+                <UiInputCheckbox
+                  v-model="parameters.consultPedagogicResult"
+                  field="showAdherentList"
+                  label="allow_students_to_consult_their_pedagogical_followup"
+                />
 
-            <UiInputCheckbox
-              v-model="parameters.generateAttendanceReport"
-              field="generateAttendanceReport"
-              label="allow_teachers_to_generate_attendance_reports"
-            />
+                <UiInputCheckbox
+                  v-model="parameters.generateAttendanceReport"
+                  field="generateAttendanceReport"
+                  label="allow_teachers_to_generate_attendance_reports"
+                />
 
-            <h4 class="mt-3 mb-4">{{ $t('pupils-members') }}</h4>
-            <UiInputCheckbox
-              v-model="parameters.administrationCc"
-              field="administrationCc"
-              label="send_teachers_mail_reports_copy_to_administration"
-            />
+                <h4 class="mt-3 mb-4">{{ $t('pupils-members') }}</h4>
+                <UiInputCheckbox
+                  v-model="parameters.administrationCc"
+                  field="administrationCc"
+                  label="send_teachers_mail_reports_copy_to_administration"
+                />
 
-            <UiInputCheckbox
-              v-model="parameters.allowMembersToChangeGivenNameAndName"
-              field="allowMembersToChangeGivenNameAndName"
-              label="allow_members_to_change_their_names_and_firstnames"
-            />
-          </v-col>
-        </v-row>
-      </UiForm>
+                <UiInputCheckbox
+                  v-model="parameters.allowMembersToChangeGivenNameAndName"
+                  field="allowMembersToChangeGivenNameAndName"
+                  label="allow_members_to_change_their_names_and_firstnames"
+                />
+              </v-col>
+            </v-row>
+          </div>
+        </template>
+      </UiFormEdition>
     </LayoutCommonSection>
   </LayoutContainer>
 </template>
 
 <script setup lang="ts">
-import type { AsyncData } from '#app'
 import Parameters from '~/models/Organization/Parameters'
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 
 definePageMeta({
   name: 'parameters_intranet_page',
 })
 
-const { fetch } = useEntityFetch()
-
 const organizationProfile = useOrganizationProfileStore()
 
 if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
 
-const { data: parameters, pending } = fetch(
-  Parameters,
-  organizationProfile.parametersId,
-) as AsyncData<Parameters, Parameters | true>
 </script>
 
 <style scoped lang="scss"></style>

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

@@ -6,12 +6,7 @@
         go-back-route="/parameters/residence_areas"
       >
         <template #default="{ entity }">
-          <UiInputText
-            v-if="entity !== null"
-            v-model="entity.label"
-            field="label"
-            :rules="rules()"
-          />
+          <FormParameterResidenceArea v-if="entity !== null" :entity="entity" />
         </template>
       </UiFormEdition>
     </LayoutCommonSection>
@@ -19,17 +14,10 @@
 </template>
 
 <script setup lang="ts">
-import { useI18n } from 'vue-i18n'
 import ResidenceArea from '~/models/Billing/ResidenceArea'
 
 definePageMeta({
   name: 'edit_resident_area',
 })
 
-const i18n = useI18n()
-
-const rules = () => [
-  (label: string | null) =>
-    (label !== null && label.length > 0) || i18n.t('please_enter_a_value'),
-]
 </script>

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

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

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

@@ -6,18 +6,7 @@
         go-back-route="/parameters/residence_areas"
       >
         <template #default="{ entity }">
-          <v-container :fluid="true" class="container">
-            <v-row>
-              <v-col cols="12" sm="6">
-                <UiInputText
-                  v-model="entity.label"
-                  field="label"
-                  type="string"
-                  :rules="rules()"
-                />
-              </v-col>
-            </v-row>
-          </v-container>
+          <FormParameterResidenceArea :entity="entity" />
         </template>
       </UiFormCreation>
     </LayoutCommonSection>
@@ -25,17 +14,10 @@
 </template>
 
 <script setup lang="ts">
-import { useI18n } from 'vue-i18n'
 import ResidenceArea from '~/models/Billing/ResidenceArea'
 
 definePageMeta({
   name: 'create_a_new_residence_area',
 })
 
-const i18n = useI18n()
-
-const rules = () => [
-  (label: string | null) =>
-    (label !== null && label.length > 0) || i18n.t('please_enter_a_value'),
-]
 </script>

+ 24 - 38
pages/parameters/sms.vue

@@ -1,61 +1,47 @@
 <template>
   <div>
     <LayoutCommonSection>
-      <UiForm v-if="parameters" v-model="parameters">
-        <v-row>
-          <v-col cols="12">
-            <UiInputText v-model="parameters.usernameSMS" field="usernameSMS" />
-          </v-col>
-          <v-col cols="12">
-            <UiInputText
-              v-model="parameters.passwordSMS"
-              field="passwordSMS"
-              class="password"
-            />
-          </v-col>
-          <v-col cols="12">
-            <div class="mb-3">
-              {{ $t('smsSenderName') }} : <b>{{ parameters.smsSenderName }}</b>
-            </div>
-          </v-col>
-        </v-row>
-      </UiForm>
+      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
+        <template #default="{ entity : parameters }">
+          <div v-if="parameters">
+            <v-row>
+              <v-col cols="12">
+                <UiInputText v-model="parameters.usernameSMS" field="usernameSMS" />
+              </v-col>
+              <v-col cols="12">
+                <UiInputText
+                  v-model="parameters.passwordSMS"
+                  field="passwordSMS"
+                  class="password"
+                />
+              </v-col>
+              <v-col cols="12">
+                <div class="mb-3">
+                  {{ $t('smsSenderName') }} : <b>{{ parameters.smsSenderName }}</b>
+                </div>
+              </v-col>
+            </v-row>
+          </div>
+        </template>
+      </UiFormEdition>
     </LayoutCommonSection>
   </div>
 </template>
 <script setup lang="ts">
-import type { AsyncData } from '#app'
+
 import Parameters from '~/models/Organization/Parameters'
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 
 definePageMeta({
   name: 'parameters_sms_page',
 })
 
-const i18n = useI18n()
-
-const { fetch } = useEntityFetch()
-
 const organizationProfile = useOrganizationProfileStore()
 
 if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
 
-const { data: parameters } = fetch(
-  Parameters,
-  organizationProfile.parametersId,
-) as AsyncData<Parameters | null, Error | null>
-
-/**
- * Règles de validation
- */
-const rules = () => [
-  (smsSenderName: string | null) =>
-    (smsSenderName !== null && /^\w{3,11}$/.test(smsSenderName)) ||
-    i18n.t('please_enter_a_value_for_the_sms_sender_name'),
-]
 </script>
 
 <style scoped lang="scss">

+ 6 - 2
pages/parameters/subdomains/[id].vue

@@ -2,7 +2,7 @@
 <template>
   <div>
     <LayoutCommonSection>
-      <UiLoadingPanel v-if="pending" />
+      <UiLoadingPanel v-if="status == 'pending'" />
       <div v-else-if="subdomain !== null">
         <div>{{ $t('youRegisteredTheFollowingSubdomain') }} :</div>
 
@@ -61,7 +61,7 @@ const { getIdFromRoute } = useRouteUtils()
 
 const id = getIdFromRoute()
 
-const { data: subdomain, pending } = fetch(Subdomain, id)
+const { data: subdomain, status } = fetch(Subdomain, id)
 
 const activationPending: Ref<boolean> = ref(false)
 
@@ -83,4 +83,8 @@ const quit = () => {
   activationPending.value = false
   pageStore.loading = false
 }
+
+onUnmounted(() => {
+  useRepo(Subdomain).flush()
+})
 </script>

+ 4 - 0
pages/parameters/subdomains/new.vue

@@ -148,6 +148,10 @@ const rules = () => [
     subdomainAvailable.value !== false ||
     i18n.t('this_subdomain_is_already_in_use'),
 ]
+
+onUnmounted(() => {
+  useRepo(Subdomain).flush()
+})
 </script>
 
 <style scoped lang="scss">

+ 23 - 29
pages/parameters/super_admin.vue

@@ -10,38 +10,35 @@
         </div>
       </div>
 
-      <UiLoadingPanel v-if="pending" />
-      <UiForm
-        v-else-if="adminAccess"
-        ref="form"
-        v-model="adminAccess"
-        class="w-100"
-      >
-        <v-table class="mb-4">
-          <tbody>
-            <tr>
-              <td>{{ $t('username') }} :</td>
-              <td>
-                <b>{{ adminAccess.username }}</b>
-              </td>
-            </tr>
-          </tbody>
-        </v-table>
+      <UiFormEdition :model="AdminAccess" :id="accessProfile.id" 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>
+              </tbody>
+            </v-table>
+
+            <UiInputEmail
+              v-model="adminAccess.email"
+              field="email"
+              label="associated_email"
+              class="mx-4"
+            />
+          </div>
+        </template>
+      </UiFormEdition>
 
-        <UiInputEmail
-          v-model="adminAccess.email"
-          field="email"
-          label="associated_email"
-          class="mx-4"
-        />
-      </UiForm>
-      <span v-else>{{ $t('no_admin_access_recorded') }}</span>
     </LayoutCommonSection>
   </div>
 </template>
 
 <script setup lang="ts">
-import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import { useAccessProfileStore } from '~/stores/accessProfile'
 import AdminAccess from '~/models/Access/AdminAccess'
 
@@ -49,14 +46,11 @@ definePageMeta({
   name: 'parameters_super_admin_page',
 })
 
-const { fetch } = useEntityFetch()
-
 const accessProfile = useAccessProfileStore()
 if (accessProfile.id === null) {
   throw new Error('Missing access profile id')
 }
 
-const { data: adminAccess, pending } = fetch(AdminAccess, accessProfile.id)
 </script>
 
 <style scoped lang="scss">

+ 28 - 19
pages/parameters/teaching.vue

@@ -3,14 +3,21 @@
     <LayoutCommonSection>
       <h4>{{ $t('configuration') }}</h4>
 
-      <UiLoadingPanel v-if="pending" />
-      <UiForm v-else-if="parameters !== null" v-model="parameters">
-        <UiInputCheckbox
-          v-model="parameters.showEducationIsACollectivePractice"
-          field="showEducationIsACollectivePractice"
-          label="allow_to_configure_teachings_with_played_instrument_choice"
-        />
-      </UiForm>
+      <UiFormEdition :model="Parameters" :id="organizationProfile.parametersId">
+        <template #default="{ entity : parameters }">
+          <div v-if="parameters">
+            <v-row>
+              <v-col cols="12">
+                <UiInputCheckbox
+                  v-model="parameters.showEducationIsACollectivePractice"
+                  field="showEducationIsACollectivePractice"
+                  label="allow_to_configure_teachings_with_played_instrument_choice"
+                />
+              </v-col>
+            </v-row>
+          </div>
+        </template>
+      </UiFormEdition>
     </LayoutCommonSection>
 
     <LayoutCommonSection>
@@ -30,7 +37,6 @@
 </template>
 
 <script setup lang="ts">
-import type { AsyncData } from '#app'
 import type { ComputedRef } from 'vue'
 import Parameters from '~/models/Organization/Parameters'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
@@ -38,7 +44,6 @@ import Cycle from '~/models/Education/Cycle'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 import type { AnyJson } from '~/types/data'
 import { useEnumFetch } from '~/composables/data/useEnumFetch'
-import type ApiResource from '~/models/ApiResource'
 import { TABLE_ACTION } from '~/types/enum/enums'
 
 definePageMeta({
@@ -51,20 +56,15 @@ if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
 
-const { fetch, fetchCollection } = useEntityFetch()
+const { fetchCollection } = useEntityFetch()
 const { fetch: fetchEnum } = useEnumFetch()
 
-const { data: cycleEnum, pending: enumPending } = fetchEnum('education_cycle')
+const { data: cycleEnum, status: enumStatus } = fetchEnum('education_cycle')
 
-const { data: parameters, pending: parametersPending } = fetch(
-  Parameters,
-  organizationProfile.parametersId,
-) as AsyncData<ApiResource | null, Error | null>
-
-const { data: cycles, pending: cyclesPending } = fetchCollection(Cycle)
+const { data: cycles, status: cyclesStatus } = fetchCollection(Cycle)
 
 const pending: ComputedRef<boolean> = computed(
-  () => enumPending.value || parametersPending.value || cyclesPending.value,
+  () => enumStatus.value == 'pending' || cyclesStatus.value == 'pending',
 )
 
 const orderedCycles: ComputedRef<AnyJson> = computed(() => {
@@ -106,6 +106,15 @@ const goToCycleEditPage = (item: object) => {
   const cycle = orderedCycles.value[item.value]
   navigateTo(`/parameters/cycles/${cycle.id}`)
 }
+
+// Nettoyer les données lors du démontage du composant
+onBeforeUnmount(() => {
+  // Nettoyer les références du store si nécessaire
+  if (process.client) {
+    clearNuxtData('/^' + Cycle.entity + '_many_/')
+    useRepo(Cycle).flush()
+  }
+})
 </script>
 
 <style scoped lang="scss"></style>

+ 9 - 4
pages/parameters/website.vue

@@ -5,7 +5,7 @@
         {{ $t('your_website') }}
       </h4>
 
-      <UiLoadingPanel v-if="pending" />
+      <UiLoadingPanel v-if="status == 'pending'" />
       <UiForm
         v-else-if="parameters !== null"
         v-model="parameters"
@@ -58,7 +58,7 @@
         </h4>
       </div>
 
-      <UiLoadingPanel v-if="subdomainsPending" />
+      <UiLoadingPanel v-if="subdomainsStatus == 'pending'" />
       <div v-else>
         <v-table v-if="subdomains!.items" class="subdomains-table my-2">
           <tbody>
@@ -145,14 +145,14 @@ if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
 
-const { data: parameters, pending } = fetch(
+const { data: parameters, status } = fetch(
   Parameters,
   organizationProfile.parametersId,
 ) as AsyncData<ApiResource | null, Error | null>
 
 const query = new Query(new EqualFilter('organization', organizationProfile.id))
 
-const { data: subdomains, pending: subdomainsPending } = fetchCollection(
+const { data: subdomains, status: subdomainsStatus } = fetchCollection(
   Subdomain,
   null,
   query,
@@ -172,6 +172,11 @@ const onAddSubdomainClick = () => {
   }
   navigateTo('/parameters/subdomains/new')
 }
+
+onUnmounted(() => {
+  useRepo(Parameters).flush()
+  useRepo(Subdomain).flush()
+})
 </script>
 
 <style scoped lang="scss">

+ 10 - 9
pages/subscription.vue

@@ -45,7 +45,7 @@ Page 'Mon abonnement'
                 <strong>{{ $t('remaining_sms_credit') }}</strong> -
                 <span
                   v-if="
-                    !mobytPending && mobytStatus !== null && mobytStatus.active
+                    mobytPendingStatus == 'success' && mobytStatus !== null && mobytStatus.active
                   "
                 >
                   {{
@@ -494,6 +494,7 @@ import { useDownloadFromRoute } from '~/composables/utils/useDownloadFromRoute'
 import { useApiLegacyRequestService } from '~/composables/data/useApiLegacyRequestService'
 import { usePageStore } from '~/stores/page'
 import { DOLIBARR_BILLING_DOC_TYPE } from '~/types/enum/enums'
+import type {AsyncDataRequestStatus} from "#app/composables/asyncData";
 
 // meta
 definePageMeta({
@@ -514,16 +515,16 @@ const showDialogTrialStopConfirmation: Ref<boolean> = ref(false)
 const openedPanels: Ref<Array<string>> = initPanel()
 const organizationProfile = getOrganizationProfile()
 const accessProfileStore = useAccessProfileStore()
-const { mobytStatus, mobytPending } = getMobytInformations()
+const { mobytStatus, mobytPendingStatus } = getMobytInformations()
 
-const { data: dolibarrAccount, pending: dolibarrPending } = fetch(
+const { data: dolibarrAccount, status: dolibarrStatus } = fetch(
   DolibarrAccount,
   organizationProfile.id,
 )
 
 const showDolibarrBillsPanel = computed(
   () =>
-    !dolibarrPending.value &&
+    dolibarrStatus.value == 'success' &&
     dolibarrAccount.value &&
     dolibarrAccount.value.bills.length > 0,
 )
@@ -606,21 +607,21 @@ function getOrganizationProfile() {
  */
 function getMobytInformations(): {
   mobytStatus: Ref<MobytUserStatus | null>
-  mobytPending: Ref<boolean>
+  mobytPendingStatus: Ref<boolean>
 } {
   const mobytStatus: Ref<MobytUserStatus | null> = ref(null)
-  const mobytPending: Ref<boolean> = ref(false)
+  const mobytPendingStatus: Ref<AsyncDataRequestStatus> = ref('pending')
 
   if (ability.can('manage', 'texto')) {
-    const { data, pending } = fetch(
+    const { data, status } = fetch(
       MobytUserStatus,
       organizationProfile!.id!,
     ) as AsyncData<MobytUserStatus | null, Error | null>
     mobytStatus.value = data
-    mobytPending.value = pending
+    mobytPendingStatus.value = status
   }
 
-  return { mobytStatus, mobytPending }
+  return { mobytStatus, mobytPendingStatus }
 }
 
 /**

+ 1 - 1
services/asserts/MaxAssert.ts

@@ -7,6 +7,6 @@ export class MaxAssert implements AssertRule {
 
   createRule(criteria: number): (value: string) => true | string {
     return (value: string) =>
-      value.length <= criteria || `Maximum ${criteria} caractères`;
+      value === null || value.length <= criteria || `Maximum ${criteria} caractères`;
   }
 }

+ 2 - 1
services/asserts/NullableAssert.ts

@@ -7,8 +7,9 @@ export class NullableAssert implements AssertRule {
   }
 
   createRule(criteria: boolean): (value: any) => true | string {
+
     const { t } = useI18n();
     return (value: any) =>
-      !criteria ? !!value || t('required') : true;
+      !criteria ? value !== null && !!value || t('please_enter_a_value') : true
   }
 }

+ 5 - 0
services/asserts/TypeAssert.ts

@@ -16,6 +16,11 @@ export class TypeAssert implements AssertRule {
         validationUtils.validEmail(email) || t('email_error');
     }
 
+    if (criteria === 'integer') {
+      return (value: any) =>
+        parseInt(value) > 0 || t('need_to_be_integer');
+    }
+
     return () => true;
   }
 }

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

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

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

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

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

@@ -20,9 +20,9 @@ export default class OrderBy implements ApiFilter {
     this.mode = mode
   }
 
-  public applyToPiniaOrmQuery(
-    query: PiniaOrmQuery<ApiResource>,
-  ): PiniaOrmQuery<ApiResource> {
+  public applyToPiniaOrmQuery <T extends ApiResource>(
+    query: PiniaOrmQuery<T>,
+  ): PiniaOrmQuery<T> {
     return query.orderBy(
       (instance) => StringUtils.normalize(instance[this.field] ?? ''),
       this.mode,

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

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

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

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

+ 44 - 0
services/data/Filters/TimeFilter.ts

@@ -0,0 +1,44 @@
+import AbstractFilter from '~/services/data/Filters/AbstractFilter'
+import RefUtils from '~/services/utils/refUtils'
+import type {ApiFilter} from "~/types/data";
+import type ApiResource from "~/models/ApiResource";
+import type {Query as PiniaOrmQuery} from "pinia-orm";
+
+export default class TimeFilter extends AbstractFilter implements ApiFilter{
+  field: string
+  filterValue: string
+
+  /**
+   * @param field
+   * @param value
+   * @param reactiveFilter
+   */
+  constructor(
+    field: string,
+    value: string,
+    reactiveFilter: boolean = false,
+  ) {
+    super(reactiveFilter)
+    this.field = field
+    this.filterValue = value
+  }
+
+  public applyToPiniaOrmQuery <T extends ApiResource>(
+    query: PiniaOrmQuery<T>,
+  ): PiniaOrmQuery<T> {
+    return query
+  }
+
+  public getApiQueryPart(): string {
+    const filterValue = RefUtils.castToRef(
+      this.filterValue,
+      this.reactiveFilter,
+    )
+
+    if (filterValue.value === null) {
+      return ''
+    }
+
+    return `${this.field}[time]=${filterValue.value}`
+  }
+}

+ 3 - 3
services/data/Query.ts

@@ -58,9 +58,9 @@ export default class Query {
    * @see https://pinia-orm.codedredd.de/guide/repository/retrieving-data
    * @param query
    */
-  public applyToPiniaOrmQuery (
-    query: PiniaOrmQuery<ApiResource>,
-  ): PiniaOrmQuery<ApiResource> {
+  public applyToPiniaOrmQuery <T extends ApiResource>(
+    query: PiniaOrmQuery<T>,
+  ): PiniaOrmQuery<T> {
     // 'Where' filters shall be applied first, then orderBy filters, and finally pagination
     const where: ApiFilter[] = []
     const orderBy: ApiFilter[] = []

+ 5 - 5
services/data/entityManager.ts

@@ -194,11 +194,11 @@ class EntityManager {
    * @param parent
    * @param query
    */
-  public async fetchCollection(
-    model: typeof ApiResource,
-    parent: ApiResource | null,
+  public async fetchCollection <T extends typeof ApiResource>(
+    model: T,
+    parent: T | null,
     query: Query | null = null,
-  ): Promise<ComputedRef<Collection>> {
+  ): Promise<ComputedRef<Collection<InstanceType<T>>>> {
     let url
 
     if (parent !== null) {
@@ -226,7 +226,7 @@ class EntityManager {
     }
 
     const res = computed(() => {
-      const items: PiniaOrmCollection<ApiResource> = piniaOrmQuery.get()
+      const items: PiniaOrmCollection<InstanceType<T>> = piniaOrmQuery.get()
 
       return {
         items,

+ 12 - 1
stores/page.ts

@@ -7,13 +7,23 @@ import type { Alert } from '~/types/interfaces'
 export const usePageStore = defineStore('page', () => {
   const alerts: Ref<Array<Alert>> = ref([])
   const loading: Ref<boolean> = ref(false)
+  let timeoutId: ReturnType<typeof setTimeout> | null = null
 
   const removeSlowlyAlert = () => {
-    setTimeout(() => {
+    cancelRemoveAlert()
+    timeoutId = setTimeout(() => {
       alerts.value.shift()
+      timeoutId = null
     }, 300)
   }
 
+  const cancelRemoveAlert = () => {
+    if (timeoutId !== null) {
+      clearTimeout(timeoutId)
+      timeoutId = null
+    }
+  }
+
   /**
    * Ajout des alerts dans le store
    * @param type
@@ -32,5 +42,6 @@ export const usePageStore = defineStore('page', () => {
     loading,
     removeSlowlyAlert,
     addAlert,
+    cancelRemoveAlert
   }
 })

+ 5 - 5
types/data.d.ts

@@ -47,16 +47,16 @@ interface Pagination {
   previous?: number
 }
 
-interface Collection {
-  items: PiniaOrmCollection<ApiResource>
+interface Collection <T extends ApiResource>{
+  items: PiniaOrmCollection<T>
   pagination: Pagination
   totalItems: number | undefined
 }
 
 interface ApiFilter {
-  applyToPiniaOrmQuery: (
-    query: PiniaOrmQuery<ApiResource>,
-  ) => PiniaOrmQuery<ApiResource>
+  applyToPiniaOrmQuery: <T extends ApiResource>(
+    query: PiniaOrmQuery<T>,
+  ) => PiniaOrmQuery<T>
   getApiQueryPart: () => string
 }