Vincent 3 mesiacov pred
rodič
commit
86808fe183

+ 71 - 52
components/Form/Freemium/Event.vue

@@ -36,6 +36,18 @@
         <span class="label">{{ $t('description') }}</span>
         <UiInputTextArea v-model="proxyEntity.description" class="mt-3" />
 
+        <UiInputAutocompleteApiResources
+          v-model="proxyEntity.gender"
+          field="gender"
+          label="gender_event"
+          :model="EventGender"
+          list-value="id"
+          list-label="name"
+          :rules="getAsserts('gender')"
+          :pagination="false"
+          :apiFilters="queryConditions"
+        />
+
         <UiInputTreeSelectEventCategories
           v-model="proxyEntity.categories"
           label="event_categories_choices"
@@ -108,65 +120,68 @@
           </v-col>
         </v-row>
 
-        <UiInputText
-          v-model="proxyEntity.placeName"
-          :readonly="!newPlace && !editPlace"
-          field="placeName"
-        />
-
-        <UiInputText
-          v-model="proxyEntity.streetAddress"
-          :readonly="!newPlace && !editPlace"
-          field="streetAddress"
-        />
+        <div  v-if="newPlace || editPlace">
+          <UiInputText
+            v-model="proxyEntity.placeName"
+            :readonly="!newPlace && !editPlace"
+            field="placeName"
+          />
 
-        <UiInputText
-          v-model="proxyEntity.streetAddressSecond"
-          :readonly="!newPlace && !editPlace"
-          field="streetAddressSecond"
-        />
+          <UiInputText
+            v-model="proxyEntity.streetAddress"
+            :readonly="!newPlace && !editPlace"
+            field="streetAddress"
+          />
 
-        <UiInputText
-          v-model="proxyEntity.streetAddressThird"
-          :readonly="!newPlace && !editPlace"
-          field="streetAddressThird"
-        />
+          <UiInputText
+            v-model="proxyEntity.streetAddressSecond"
+            :readonly="!newPlace && !editPlace"
+            field="streetAddressSecond"
+          />
 
-        <UiInputText
-          v-model="proxyEntity.postalCode"
-          :readonly="!newPlace && !editPlace"
-          field="postalCode"
-        />
+          <UiInputText
+            v-model="proxyEntity.streetAddressThird"
+            :readonly="!newPlace && !editPlace"
+            field="streetAddressThird"
+          />
 
-        <UiInputText
-          v-model="proxyEntity.addressCity"
-          :readonly="!newPlace && !editPlace"
-          field="addressCity"
-        />
+          <UiInputText
+            v-model="proxyEntity.postalCode"
+            :readonly="!newPlace && !editPlace"
+            field="postalCode"
+          />
 
-        <UiInputAutocompleteApiResources
-          v-model="proxyEntity.addressCountry"
-          :readonly="!newPlace && !editPlace"
-          field="addressCountry"
-          :model="Country"
-          list-value="id"
-          list-label="name"
-        />
+          <UiInputText
+            v-model="proxyEntity.addressCity"
+            :readonly="!newPlace && !editPlace"
+            field="addressCity"
+          />
 
-        <client-only>
-          <UiMapLeaflet
-            v-model:latitude="proxyEntity.latitude"
-            v-model:longitude="proxyEntity.longitude"
+          <UiInputAutocompleteApiResources
+            v-model="proxyEntity.addressCountry"
             :readonly="!newPlace && !editPlace"
-            :street-address="proxyEntity.streetAddress"
-            :street-address-second="proxyEntity.streetAddressSecond"
-            :street-address-third="proxyEntity.streetAddressThird"
-            :postal-code="proxyEntity.postalCode"
-            :address-city="proxyEntity.addressCity"
-            :address-country-id="proxyEntity.addressCountry"
-            :search-button="true"
-          ></UiMapLeaflet>
-        </client-only>
+            field="addressCountry"
+            :model="Country"
+            list-value="id"
+            list-label="name"
+          />
+
+          <client-only>
+            <UiMapLeaflet
+              v-model:latitude="proxyEntity.latitude"
+              v-model:longitude="proxyEntity.longitude"
+              :readonly="!newPlace && !editPlace"
+              :street-address="proxyEntity.streetAddress"
+              :street-address-second="proxyEntity.streetAddressSecond"
+              :street-address-third="proxyEntity.streetAddressThird"
+              :postal-code="proxyEntity.postalCode"
+              :address-city="proxyEntity.addressCity"
+              :address-country-id="proxyEntity.addressCountry"
+              :search-button="true"
+            ></UiMapLeaflet>
+          </client-only>
+        </div>
+
       </v-col>
     </v-row>
   </LayoutCommonSection>
@@ -241,6 +256,8 @@ import DateUtils from '~/services/utils/dateUtils'
 import Country from '~/models/Core/Country'
 import PlaceSearchItem from '~/models/Custom/Search/PlaceSearchItem'
 import { useEntityManager } from '~/composables/data/useEntityManager'
+import EventGender from "~/models/Booking/EventGender";
+import EqualFilter from "~/services/data/Filters/EqualFilter";
 
 const props = defineProps<{
   entity: Event
@@ -257,6 +274,8 @@ const proxyEntity = computed({
   set: (value) => emit('update:entity', value),
 })
 
+const queryConditions = [new EqualFilter('type', 'CULTURAL_EVENT')]
+
 /**
  * Si la date de début est mise à jour, on s'assure que la date de fin est
  * après elle, sinon elle devient égale

+ 3 - 1
components/Layout/Header.vue

@@ -14,7 +14,9 @@ Contient entre autres le nom de l'organisation, l'accès à l'aide et aux préf
     </template>
 
     <v-toolbar-title v-if="smAndUp">
-      <LayoutHeaderTitle>
+      <LayoutHeaderTitle
+        :hasLateralMenu
+      >
         {{ title }}
       </LayoutHeaderTitle>
     </v-toolbar-title>

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

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

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

@@ -12,16 +12,20 @@ import UrlUtils from '~/services/utils/urlUtils'
 const runtimeConfig = useRuntimeConfig()
 const i18n = useI18n()
 const router = useRouter()
+const organizationProfile = useOrganizationProfileStore()
 
 const items: ComputedRef<Array<AnyJson>> = computed(() => {
   const crumbs: Array<AnyJson> = []
   const baseUrl =
     runtimeConfig.baseUrlAdminLegacy ?? runtimeConfig.public.baseUrlAdminLegacy
 
-  crumbs.push({
-    title: i18n.t('welcome'),
-    href: UrlUtils.join(baseUrl, '#', 'dashboard'),
-  })
+  if(!organizationProfile.isFreemiumProduct){
+    crumbs.push({
+      title: i18n.t('welcome'),
+      href: UrlUtils.join(baseUrl, '#', 'dashboard'),
+    })
+  }
+
   const pathPart: Array<string> = UrlUtils.split(router.currentRoute.value.path)
 
   let path: string = ''

+ 7 - 3
components/Ui/EventList.vue

@@ -19,11 +19,11 @@
         </template>
 
         <template #append>
-          <v-avatar class="edit-btn" @click="$emit('edit', event.id)">
+          <v-avatar class="edit-btn action-btn" @click="$emit('edit', event.id)">
             <v-icon>fas fa-pencil</v-icon>
           </v-avatar>
 
-          <UiButtonDelete :entity="event" class="delete-btn">
+          <UiButtonDelete :entity="event" class="delete-btn action-btn">
             <v-avatar>
               <v-icon>fas fa-trash</v-icon>
             </v-avatar>
@@ -111,8 +111,12 @@ const date = useDate()
 .edit-btn {
   cursor: pointer;
 }
+.action-btn:hover{
+  background-color: rgb(var(--v-theme-on-neutral-soft--sub));
+  border-radius: 20px;
+}
 :deep(.v-list-item):hover {
   cursor: pointer;
-  background-color: rgb(var(--v-theme-neutral));
+  background-color: rgb(var(--v-theme-neutral-soft));
 }
 </style>

+ 1 - 1
components/Ui/Form.vue

@@ -60,7 +60,7 @@ de quitter si des données ont été modifiées.
     </v-form>
 
     <!-- Confirmation dialog -->
-    <LazyLayoutDialog :show="isConfirmationDialogShowing" :max-width="1000">
+    <LazyLayoutDialog :show="isConfirmationDialogShowing" :max-width="1000" theme="danger">
       <template #dialogText>
         <v-card-title class="text-h5 theme-neutral">
           {{ $t('caution') }}

+ 47 - 7
components/Ui/Input/Autocomplete/ApiResources.vue

@@ -31,6 +31,7 @@ Exemple : on souhaite lister les lieux d'une structure
       :variant="variant"
       :readonly="readonly"
       :clearable="true"
+      :rules="rules"
       @update:model-value="onUpdateModelValue"
       @update:search="onUpdateSearch"
     />
@@ -54,6 +55,8 @@ import InArrayFilter from '~/services/data/Filters/InArrayFilter'
 import SearchFilter from '~/services/data/Filters/SearchFilter'
 import type ApiModel from '~/models/ApiModel'
 import PlaceSearchItem from '~/models/Custom/Search/PlaceSearchItem'
+import type {ApiFilter} from "~/types/data";
+import EqualFilter from "~/services/data/Filters/EqualFilter";
 
 const props = defineProps({
   /**
@@ -74,10 +77,26 @@ const props = defineProps({
   /**
    * Filtres à transmettre à la source de données
    */
-  query: {
-    type: Object as PropType<typeof Query | null>,
+  apiFilters: {
+    type: Array<ApiFilter>,
     required: false,
-    default: null,
+    default: [],
+  },
+  /**
+   * Nombre de résultat
+   */
+  resultNumber: {
+    type: Number,
+    required: false,
+    default: 20,
+  }
+  , /**
+   * Autorise la pagination
+   * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-multiple
+   */
+  pagination: {
+    type: Boolean,
+    default: true,
   },
   /**
    * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
@@ -162,6 +181,15 @@ const props = defineProps({
     required: false,
     default: null,
   },
+  /**
+   * Règles de validation
+   * @see https://vuetify.cn/en/components/forms/#validation-with-submit-clear
+   */
+  rules: {
+    type: Array,
+    required: false,
+    default: () => [],
+  },
 })
 
 /**
@@ -189,11 +217,17 @@ const activeIds = computed(() => {
  * Query transmise à l'API lors de l'initialisation afin de récupérer les items actifs
  */
 const queryActive = computed(() => {
-  return new Query(
+  const query = new Query(
     new OrderBy(props.listLabel, ORDER_BY_DIRECTION.ASC),
-    new PageFilter(ref(1), ref(20)),
     new InArrayFilter(props.listValue, activeIds),
   )
+  if(props.pagination){
+    query.add( new PageFilter(ref(1), ref(props.resultNumber)))
+  }
+  for(const index in props.apiFilters){
+    query.add(props.apiFilters[index])
+  }
+  return query
 })
 
 /**
@@ -219,9 +253,15 @@ const searchFilter: Ref<string | null> = ref(null)
  */
 const querySearch = new Query(
   new OrderBy(props.listLabel, ORDER_BY_DIRECTION.ASC),
-  new PageFilter(ref(1), ref(20)),
   new SearchFilter(props.listLabel, searchFilter, SEARCH_STRATEGY.IPARTIAL),
 )
+if(props.pagination){
+  querySearch.add(   new PageFilter(ref(1), ref(props.resultNumber)))
+}
+for(const index in props.apiFilters){
+  querySearch.add(props.apiFilters[index])
+}
+
 /**
  * On fetch les résultats correspondants à la recherche faite par l'utilisateur
  */
@@ -246,7 +286,7 @@ const item = (searchItem): ListItem => {
   return {
     id: searchItem[props.listValue],
     title: searchItem[props.listLabel]
-      ? searchItem[props.listLabel]
+      ? i18n.t(searchItem[props.listLabel])
       : `(${i18n.t('missing_value')})`,
   }
 }

+ 90 - 59
components/Ui/Input/TreeSelect.vue

@@ -176,6 +176,11 @@ const props = defineProps({
     required: false,
     default: null,
   },
+  nbLevel: {
+    type: Number,
+    required: false,
+    default: 3,
+  },
   /**
    * Label du champ
    * Si non défini, c'est le nom de propriété qui est utilisé
@@ -273,21 +278,27 @@ const expandParentsOfSelectedItems = () => {
     const item = normalizedItems.value.find((i) => i.value === selectedId)
     if (!item) continue
 
-    // Trouver la sous-catégorie
-    const subcategory = normalizedItems.value.find(
-      (i) => i.id === item.parentId,
-    )
-    if (subcategory) {
-      expandedSubcategories.value.add(subcategory.id)
+    let parentId = null
+    if(props.nbLevel === 3){
+      // Trouver la sous-catégorie
+      const subcategory = normalizedItems.value.find(
+        (i) => i.id === item.parentId,
+      )
+      if (subcategory) {
+        expandedSubcategories.value.add(subcategory.id)
+        parentId = subcategory.parentId
+      }
+    }else{
+      parentId = item.parentId
+    }
 
+    if (parentId) {
       // Trouver la catégorie
-      if (subcategory.parentId) {
-        const category = normalizedItems.value.find(
-          (i) => i.id === subcategory.parentId,
-        )
-        if (category) {
-          expandedCategories.value.add(category.id)
-        }
+      const category = normalizedItems.value.find(
+        (i) => i.id === parentId,
+      )
+      if (category) {
+        expandedCategories.value.add(category.id)
       }
     }
   }
@@ -387,25 +398,33 @@ const onSearchInput = () => {
     // Trouver tous les éléments qui correspondent à la recherche
     const matchingItems = normalizedItems.value.filter(
       (item) =>
-        item.type === 'item' && item.level === 2 && itemMatchesSearch(item),
+        item.type === 'item' && item.level === (props.nbLevel - 1) && itemMatchesSearch(item),
     )
-
     // Pour chaque élément correspondant, ajouter ses parents aux ensembles d'expansion
     for (const item of matchingItems) {
-      // Trouver et ajouter la sous-catégorie parente
-      const subcategory = normalizedItems.value.find(
-        (i) => i.id === item.parentId,
-      )
-      if (subcategory) {
-        expandedSubcategories.value.add(subcategory.id)
+      let category
+      if(props.nbLevel === 3){
+        // Trouver et ajouter la sous-catégorie parente
+        const subcategory = normalizedItems.value.find(
+          (i) => i.id === item.parentId,
+        )
+        if (subcategory) {
+          expandedSubcategories.value.add(subcategory.id)
 
+          // Trouver et ajouter la catégorie parente
+          category = normalizedItems.value.find(
+            (i) => i.id === subcategory.parentId,
+          )
+        }
+      }else{
         // Trouver et ajouter la catégorie parente
-        const category = normalizedItems.value.find(
-          (i) => i.id === subcategory.parentId,
+        category = normalizedItems.value.find(
+          (i) => i.id === item.parentId,
         )
-        if (category) {
-          expandedCategories.value.add(category.id)
-        }
+      }
+
+      if (category) {
+        expandedCategories.value.add(category.id)
       }
     }
   }
@@ -469,8 +488,8 @@ const itemMatchesSearch = (item: SelectItem): boolean => {
   )
   if (!itemWithNormalizedLabel) return false
 
-  // Si c'est un élément de niveau 2, vérifier son label et les labels de ses parents
-  if (item.type === 'item' && item.level === 2) {
+  // Si c'est un élément de niveau nbLevel - 1, vérifier son label et les labels de ses parents
+  if (item.type === 'item' && item.level === (props.nbLevel - 1)) {
     // Vérifier le label de l'élément
     if (
       anyWordStartsWith(
@@ -480,28 +499,35 @@ const itemMatchesSearch = (item: SelectItem): boolean => {
     )
       return true
 
-    // Trouver et vérifier le label de la sous-catégorie parente
-    const subcategory = normalizedItems.value.find(
-      (i) => i.id === item.parentId,
-    )
-    if (
-      subcategory &&
-      anyWordStartsWith(subcategory.normalizedLabel!, normalizedSearch)
-    )
-      return true
-
-    // Trouver et vérifier le label de la catégorie parente
-    if (subcategory && subcategory.parentId) {
-      const category = normalizedItems.value.find(
-        (i) => i.id === subcategory.parentId,
+    let parentId = item.parentId
+    if(props.nbLevel === 3){
+      // Trouver et vérifier le label de la sous-catégorie parente
+      const subcategory = normalizedItems.value.find(
+        (i) => i.id === parentId,
       )
       if (
-        category &&
-        anyWordStartsWith(category.normalizedLabel!, normalizedSearch)
+        subcategory &&
+        anyWordStartsWith(subcategory.normalizedLabel!, normalizedSearch)
       )
         return true
+
+      // Trouver et vérifier le label de la catégorie parente
+      if (subcategory && subcategory.parentId) {
+        parentId =  subcategory.parentId
+      }
     }
 
+    // Trouver et vérifier le label de la catégorie parente
+   const category = normalizedItems.value.find(
+     (i) => i.id === parentId,
+   )
+   if (
+     category &&
+     anyWordStartsWith(category.normalizedLabel!, normalizedSearch)
+   )
+     return true
+
+
     return false
   }
 
@@ -520,7 +546,7 @@ const itemMatchesSearch = (item: SelectItem): boolean => {
 const findMatchingLevel2Items = (): SelectItem[] => {
   return normalizedItems.value.filter(
     (item) =>
-      item.type === 'item' && item.level === 2 && itemMatchesSearch(item),
+      item.type === 'item' && item.level === (props.nbLevel - 1) && itemMatchesSearch(item),
   )
 }
 
@@ -537,15 +563,26 @@ const buildSearchResultsList = (matchingItems: SelectItem[]): SelectItem[] => {
   const addedSubcategoryIds = new Set<string>()
 
   for (const item of matchingItems) {
-    // Trouver la sous-catégorie parente
-    const subcategory = normalizedItems.value.find(
-      (i) => i.id === item.parentId,
-    )
-    if (!subcategory) continue
+    let parentId = item.parentId
+    if(props.nbLevel === 3){
+      // Trouver la sous-catégorie parente
+      const subcategory = normalizedItems.value.find(
+        (i) => i.id === parentId,
+      )
+      if (!subcategory) continue
+
+      // Ajouter la sous-catégorie si elle n'est pas déjà présente
+      if (!addedSubcategoryIds.has(subcategory.id)) {
+        result.push(subcategory)
+        addedSubcategoryIds.add(subcategory.id)
+        expandedSubcategories.value.add(subcategory.id)
+      }
+      parentId = subcategory.parentId
+    }
 
     // Trouver la catégorie parente
     const category = normalizedItems.value.find(
-      (i) => i.id === subcategory.parentId,
+      (i) => i.id === parentId,
     )
     if (!category) continue
 
@@ -556,13 +593,6 @@ const buildSearchResultsList = (matchingItems: SelectItem[]): SelectItem[] => {
       expandedCategories.value.add(category.id)
     }
 
-    // Ajouter la sous-catégorie si elle n'est pas déjà présente
-    if (!addedSubcategoryIds.has(subcategory.id)) {
-      result.push(subcategory)
-      addedSubcategoryIds.add(subcategory.id)
-      expandedSubcategories.value.add(subcategory.id)
-    }
-
     // Ajouter l'élément
     result.push(item)
   }
@@ -588,7 +618,8 @@ const processItemsRecursively = (
       result.push(item)
       if (expandedCategories.value.has(item.id)) {
         const subcategories = normalizedItems.value.filter(
-          (i) => i.parentId === item.id && i.type === 'subcategory',
+          (i) => i.parentId === item.id
+            && ((props.nbLevel == 2 && i.type === 'item')  || (props.nbLevel == 3 && i.type === 'subcategory')),
         )
         processItemsRecursively(subcategories, result, true)
       }

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

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

+ 1 - 0
config/theme.ts

@@ -74,6 +74,7 @@ export const lightTheme: Theme = {
     'on-neutral--clickable': '#00997d',
 
     'neutral-soft': '#f2f2f2',
+    'on-neutral-soft--sub': '#c9c9c9',
     'on-neutral-soft': '#333333',
 
     'neutral-very-soft': '#ffffff',

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

@@ -1,6 +1,6 @@
 {
   "freemium_event_create_page_breadcrumbs":  "Création d'un événement",
-  "freemium_dashboard_page_breadcrumbs": "Freemium",
+  "freemium_dashboard_page_breadcrumbs": "Accueil",
   "freemium_event_edit_page_breadcrumbs": "Édition d'un événement",
   "freemium_organization_page_breadcrumbs": "Fiche de ma structure",
   "new_education_timing_breadcrumbs":  "Création",

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

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

+ 20 - 0
i18n/lang/fr/general.json

@@ -1,4 +1,22 @@
 {
+  "VARIOUS_ORCHESTRA": "Orchestre divers",
+  "MUSIC_TEACHING": "Enseignement musique",
+  "DRAMATIC_ARTS": "Enseignement arts dramatiques",
+  "DANCE_LESSONS": "Enseignement danse",
+  "CIRCUS_TRAINING": "Enseignement de cirque",
+  "ART_TEACHING": "Enseignement d'art",
+  "MAJORETTE_AND_TWIRLING": "Majorettes et Twirling",
+  "TAP_DANCE": "Claquettes",
+  "LATIN_DANCE": "Danse latines",
+  "FOLK_DANCE": "Danse folklorique",
+  "CLASSICAL_DANCE": "Danse classique",
+  "BREAK_DANCING": "Break dance",
+  "BALLROOM_DANCE": "Danse de salon",
+  "COMPANIES": "Compagnies",
+  "OTHER_TYPE": "Autres",
+  "type_of_practices": "Type de pratiques",
+  "welcome_freemium_title": "Mon compte opentalent",
+  "gender_event": "Genre",
   "url_error": "Le lien n'est pas correct",
   "organization_logo": "Logo de votre structure",
   "event_image": "Affiche de l'événement",
@@ -148,6 +166,8 @@
   "CATEGORY_OTHER": "Autres activités",
   "CATEGORY_CHORUS": "Chorale / Groupe vocal",
   "CATEGORY_BAND": "Ensemble",
+  "CATEGORY_DANCES": "Danses",
+  "CATEGORY_TEACHING": "Enseignements",
   "BRASS_BAND": "Brass band",
   "HUNTING_HORNS": "Trompes de chasse",
   "PHILHARMONIC_ORCHESTRA": "Orchestre philharmonique",

+ 19 - 0
models/Booking/EventGender.ts

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

+ 6 - 0
models/Freemium/Event.ts

@@ -5,6 +5,7 @@ import File from '~/models/Core/File'
 import Place from '~/models/Place/Place'
 import Country from '~/models/Core/Country'
 import Category from '~/models/Core/Category'
+import EventGender from "~/models/Booking/EventGender";
 
 /**
  * AP2i Model : Freemium / Event
@@ -89,4 +90,9 @@ export default class Event extends ApiModel {
   @Attr(() => [])
   @IriEncoded(Category)
   declare categories: Array<string> | null
+
+  @Attr(null)
+  @IriEncoded(EventGender)
+  @Assert({ nullable: false })
+  declare gender: number | null
 }

+ 13 - 0
models/Freemium/Organization.ts

@@ -3,6 +3,7 @@ import ApiModel from '~/models/ApiModel'
 import { Assert, IdLess, IriEncoded } from '~/models/decorators'
 import Country from '~/models/Core/Country'
 import File from '~/models/Core/File'
+import TypeOfPractice from "~/models/Organization/TypeOfPractice";
 
 /**
  * AP2i Model : Freemium / Organization
@@ -19,6 +20,14 @@ export default class Organization extends ApiModel {
   @Assert({ nullable: false, max: 128 })
   declare name: string | null
 
+  @Str('ARTISTIC_PRACTICE_ONLY')
+  @Assert({ nullable: false })
+  declare principalType: string | null
+
+  @Str('ASSOCIATION_LAW_1901')
+  @Assert({ nullable: false })
+  declare legalStatus: string | null
+
   @Str(null)
   declare description: string | null
 
@@ -76,4 +85,8 @@ export default class Organization extends ApiModel {
   @Attr(null)
   @IriEncoded(File)
   declare logo: number | null
+
+  @Attr(() => [])
+  @IriEncoded(TypeOfPractice)
+  declare typeOfPractices: Array<string> | null
 }

+ 16 - 14
pages/freemium/index.vue

@@ -2,20 +2,22 @@
   <div>
     <v-container fluid class="inner-container">
       <v-row>
+        <v-col cols="12" class="text-h6 text-uppercase font-weight-bold">{{$t('welcome_freemium_title')}}</v-col>
         <!-- Bloc événements -->
         <v-col cols="12" md="7">
           <v-card>
             <v-tabs v-model="tab" class="tabs-title">
-              <v-tab value="future">{{ $t('futur_event') }}</v-tab>
-              <v-tab value="past">{{ $t('past_event') }}</v-tab>
+              <v-tab value="future" class="text-none">{{ $t('futur_event') }}</v-tab>
+              <v-tab value="past" class="text-none">{{ $t('past_event') }}</v-tab>
             </v-tabs>
 
-            <v-btn color="primary" to="freemium/events/new" class="ml-5 mt-5">{{
-              $t('add_event')
-            }}</v-btn>
 
             <v-tabs-window v-model="tab" >
               <v-tabs-window-item value="future">
+                <v-btn color="primary" to="freemium/events/new" class="ml-5 mt-5">{{
+                    $t('add_event')
+                  }}</v-btn>
+
                 <div v-if="statusUpcomingEvents == FETCHING_STATUS.PENDING">
                   <v-col cols="12" class="loader">
                     <v-skeleton-loader
@@ -90,8 +92,13 @@
             </v-card-text>
           </v-card>
 
-          <v-btn block class="mb-2 btn btn_edit_orga" to="freemium/organization">
-            <i class="fa fa-pen mr-2" />{{ $t('edit_organization') }}
+          <v-btn
+            block
+            prepend-icon="fa-solid fa-pen"
+            class="my-5 text-black"
+            to="freemium/organization"
+          >
+            {{ $t('edit_organization') }}
           </v-btn>
 
           <v-btn block class="text-black btn btn_trial" @click="startTrial">
@@ -233,11 +240,10 @@ onUnmounted(() => {
 
 <style scoped lang="scss">
 .tabs-title {
-  margin-top: 20px;
   padding-left: 20px;
-  background-color: rgb(var(--v-theme-neutral));
+  background-color: rgb(var(--v-theme-neutral-soft));
   .v-tab--selected {
-    color: rgb(var(--v-theme-on-neutral--clickable));
+    color: rgb(var(--v-theme-neutral-soft--sub));
   }
 }
 
@@ -286,10 +292,6 @@ onUnmounted(() => {
   }
 }
 
-.btn_edit_orga {
-  color: rgb(var(--v-theme-on-primary-alt)) !important;
-}
-
 .no_event {
   padding: 25px;
   font-size: 16px;

+ 17 - 0
pages/freemium/organization.vue

@@ -13,6 +13,23 @@
                 :rules="getAsserts('name')"
               />
 
+              <UiInputAutocompleteEnum
+                v-model="organization.legalStatus"
+                enum-name="organization_legal"
+                field="legalStatus"
+              />
+
+              <UiInputAutocompleteEnum
+                v-model="organization.principalType"
+                enum-name="organization_principal_type_short_list"
+                field="principalType"
+              />
+
+              <UiInputTreeSelectTypeOfPractices
+                v-model="organization.typeOfPractices"
+                label="type_of_practices"
+              />
+
               <UiInputTextArea
                 v-model="organization.description"
                 field="description"