Olivier Massot 4 ヶ月 前
コミット
546913a3d0

+ 91 - 47
components/Ui/Input/TreeSelect.vue

@@ -41,7 +41,7 @@ et sélectionner des éléments organisés en catégories et sous-catégories.
         @input="onSearchInput"
         @click:clear.stop="onSearchClear"
       />
-      <v-divider class="mt-2"/>
+      <v-divider class="mt-2" />
     </template>
 
     <template #selection="{ item, index }">
@@ -52,10 +52,18 @@ et sélectionner des éléments organisés en catégories et sous-catégories.
         @click:close="removeItem(item.raw.value!)"
       >
         <!-- Use the label from the item if available, otherwise use the mapping -->
-        {{ item.raw.label && item.raw.label !== item.raw.value ? item.raw.label : selectedItemsMap[item.raw.value] || item.raw.value }}
+        {{
+          item.raw.label && item.raw.label !== item.raw.value
+            ? item.raw.label
+            : selectedItemsMap[item.raw.value] || item.raw.value
+        }}
       </v-chip>
       <span
-        v-if="maxVisibleChips && index === maxVisibleChips && modelValue.length > maxVisibleChips"
+        v-if="
+          maxVisibleChips &&
+          index === maxVisibleChips &&
+          modelValue.length > maxVisibleChips
+        "
         class="text-grey text-caption align-self-center"
       >
         (+{{ modelValue.length - maxVisibleChips }} {{ $t('others') }})
@@ -65,13 +73,20 @@ et sélectionner des éléments organisés en catégories et sous-catégories.
     <template #item="{ item }">
       <template v-if="item.raw.type === 'category'">
         <v-list-item
-          @click.stop="toggleCategory(item.raw.id)"
           :ripple="false"
-          :class="{ 'v-list-item--active': expandedCategories.has(item.raw.id) }"
+          :class="{
+            'v-list-item--active': expandedCategories.has(item.raw.id),
+          }"
+          @click.stop="toggleCategory(item.raw.id)"
         >
           <template #prepend>
             <v-icon
-              :icon="'fas ' + (expandedCategories.has(item.raw.id) ? 'fa-angle-down' : 'fa-angle-right')"
+              :icon="
+                'fas ' +
+                (expandedCategories.has(item.raw.id)
+                  ? 'fa-angle-down'
+                  : 'fa-angle-right')
+              "
               size="small"
             />
           </template>
@@ -83,16 +98,21 @@ et sélectionner des éléments organisés en catégories et sous-catégories.
 
       <template v-else-if="item.raw.type === 'subcategory'">
         <v-list-item
-          @click.stop="toggleSubcategory(item.raw.id)"
           :ripple="false"
           :class="{
             'v-list-item--active': expandedSubcategories.has(item.raw.id),
-            'pl-8': true
+            'pl-8': true,
           }"
+          @click.stop="toggleSubcategory(item.raw.id)"
         >
           <template #prepend>
             <v-icon
-              :icon="'fas ' + (expandedSubcategories.has(item.raw.id) ? 'fa-angle-down' : 'fa-angle-right')"
+              :icon="
+                'fas ' +
+                (expandedSubcategories.has(item.raw.id)
+                  ? 'fa-angle-down'
+                  : 'fa-angle-right')
+              "
               size="small"
             />
           </template>
@@ -104,16 +124,19 @@ et sélectionner des éléments organisés en catégories et sous-catégories.
 
       <template v-else>
         <v-list-item
-          @click="toggleItem(item.raw.value!)"
           :active="modelValue.includes(item.raw.value!)"
-          :class="{ 'pl-12': item.raw.level === 2, 'pl-8': item.raw.level === 1 }"
+          :class="{
+            'pl-12': item.raw.level === 2,
+            'pl-8': item.raw.level === 1,
+          }"
+          @click="toggleItem(item.raw.value!)"
         >
           <template #prepend>
             <v-checkbox
               :model-value="modelValue.includes(item.raw.value!)"
-              @click.stop="toggleItem(item.raw.value!)"
               color="primary"
               :hide-details="true"
+              @click.stop="toggleItem(item.raw.value!)"
             />
           </template>
           <v-list-item-title>
@@ -140,17 +163,17 @@ interface SelectItem {
 const props = defineProps({
   modelValue: {
     type: Array as PropType<string[]>,
-    required: true
+    required: true,
   },
   items: {
     type: Array as PropType<SelectItem[]>,
-    required: true
+    required: true,
   },
   maxVisibleChips: {
     type: Number,
     required: false,
-    default: null
-  }
+    default: null,
+  },
 })
 
 const emit = defineEmits(['update:modelValue'])
@@ -181,10 +204,10 @@ const toggleCategory = (categoryId: string) => {
   if (expandedCategories.value.has(categoryId)) {
     expandedCategories.value.delete(categoryId)
     // Fermer aussi les sous-catégories
-    const subcategories = props.items.filter(i =>
-      i.parentId === categoryId && i.type === 'subcategory'
+    const subcategories = props.items.filter(
+      (i) => i.parentId === categoryId && i.type === 'subcategory',
     )
-    subcategories.forEach(sub => {
+    subcategories.forEach((sub) => {
       expandedSubcategories.value.delete(sub.id)
     })
   } else {
@@ -232,7 +255,10 @@ const toggleItem = (value: string) => {
  * after the specified item has been removed.
  */
 const removeItem = (value: string) => {
-  emit('update:modelValue', props.modelValue.filter(item => item !== value))
+  emit(
+    'update:modelValue',
+    props.modelValue.filter((item) => item !== value),
+  )
 }
 
 /**
@@ -245,21 +271,20 @@ const onSearchInput = () => {
 
   if (searchText.value.trim()) {
     // Trouver tous les éléments qui correspondent à la recherche
-    const matchingItems = props.items.filter(item =>
-      item.type === 'item' &&
-      item.level === 2 &&
-      itemMatchesSearch(item)
+    const matchingItems = props.items.filter(
+      (item) =>
+        item.type === 'item' && item.level === 2 && 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 = props.items.find(i => i.id === item.parentId)
+      const subcategory = props.items.find((i) => i.id === item.parentId)
       if (subcategory) {
         expandedSubcategories.value.add(subcategory.id)
 
         // Trouver et ajouter la catégorie parente
-        const category = props.items.find(i => i.id === subcategory.parentId)
+        const category = props.items.find((i) => i.id === subcategory.parentId)
         if (category) {
           expandedCategories.value.add(category.id)
         }
@@ -280,12 +305,15 @@ const onSearchClear = () => {
  * @param {string} normalizedSearch - The normalized search term.
  * @returns {boolean} `true` if any word in the text starts with the search term; otherwise, `false`.
  */
-const anyWordStartsWith = (normalizedText: string, normalizedSearch: string): boolean => {
+const anyWordStartsWith = (
+  normalizedText: string,
+  normalizedSearch: string,
+): boolean => {
   // Split the text into words
   const words = normalizedText.split(' ')
 
   // Check if any word starts with the search term
-  return words.some(word => word.startsWith(normalizedSearch))
+  return words.some((word) => word.startsWith(normalizedSearch))
 }
 
 /**
@@ -315,16 +343,31 @@ const itemMatchesSearch = (item: SelectItem): boolean => {
   // 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) {
     // Vérifier le label de l'élément
-    if (anyWordStartsWith(StringUtils.normalize(item.label), normalizedSearch)) return true
+    if (anyWordStartsWith(StringUtils.normalize(item.label), normalizedSearch))
+      return true
 
     // Trouver et vérifier le label de la sous-catégorie parente
-    const subcategory = props.items.find(i => i.id === item.parentId)
-    if (subcategory && anyWordStartsWith(StringUtils.normalize(subcategory.label), normalizedSearch)) return true
+    const subcategory = props.items.find((i) => i.id === item.parentId)
+    if (
+      subcategory &&
+      anyWordStartsWith(
+        StringUtils.normalize(subcategory.label),
+        normalizedSearch,
+      )
+    )
+      return true
 
     // Trouver et vérifier le label de la catégorie parente
     if (subcategory && subcategory.parentId) {
-      const category = props.items.find(i => i.id === subcategory.parentId)
-      if (category && anyWordStartsWith(StringUtils.normalize(category.label), normalizedSearch)) return true
+      const category = props.items.find((i) => i.id === subcategory.parentId)
+      if (
+        category &&
+        anyWordStartsWith(
+          StringUtils.normalize(category.label),
+          normalizedSearch,
+        )
+      )
+        return true
     }
 
     return false
@@ -340,10 +383,9 @@ const itemMatchesSearch = (item: SelectItem): boolean => {
  * @returns {SelectItem[]} Les éléments de niveau 2 qui correspondent à la recherche.
  */
 const findMatchingLevel2Items = (): SelectItem[] => {
-  return props.items.filter(item =>
-    item.type === 'item' &&
-    item.level === 2 &&
-    itemMatchesSearch(item)
+  return props.items.filter(
+    (item) =>
+      item.type === 'item' && item.level === 2 && itemMatchesSearch(item),
   )
 }
 
@@ -361,11 +403,11 @@ const buildSearchResultsList = (matchingItems: SelectItem[]): SelectItem[] => {
 
   for (const item of matchingItems) {
     // Trouver la sous-catégorie parente
-    const subcategory = props.items.find(i => i.id === item.parentId)
+    const subcategory = props.items.find((i) => i.id === item.parentId)
     if (!subcategory) continue
 
     // Trouver la catégorie parente
-    const category = props.items.find(i => i.id === subcategory.parentId)
+    const category = props.items.find((i) => i.id === subcategory.parentId)
     if (!category) continue
 
     // Ajouter la catégorie si elle n'est pas déjà présente
@@ -400,14 +442,14 @@ const buildSearchResultsList = (matchingItems: SelectItem[]): SelectItem[] => {
 const processItemsRecursively = (
   items: SelectItem[],
   result: SelectItem[],
-  parentExpanded = true
+  parentExpanded = true,
 ): void => {
   for (const item of items) {
     if (item.type === 'category') {
       result.push(item)
       if (expandedCategories.value.has(item.id)) {
-        const subcategories = props.items.filter(i =>
-          i.parentId === item.id && i.type === 'subcategory'
+        const subcategories = props.items.filter(
+          (i) => i.parentId === item.id && i.type === 'subcategory',
         )
         processItemsRecursively(subcategories, result, true)
       }
@@ -415,8 +457,8 @@ const processItemsRecursively = (
       if (parentExpanded) {
         result.push(item)
         if (expandedSubcategories.value.has(item.id)) {
-          const subItems = props.items.filter(i =>
-            i.parentId === item.id && i.type === 'item'
+          const subItems = props.items.filter(
+            (i) => i.parentId === item.id && i.type === 'item',
           )
           processItemsRecursively(subItems, result, true)
         }
@@ -434,7 +476,7 @@ const processItemsRecursively = (
  */
 const buildNormalModeList = (): SelectItem[] => {
   const result: SelectItem[] = []
-  const topLevelItems = props.items.filter(item => !item.parentId)
+  const topLevelItems = props.items.filter((item) => !item.parentId)
   processItemsRecursively(topLevelItems, result)
   return result
 }
@@ -467,10 +509,12 @@ const selectedItemsMap = computed(() => {
   const map: Record<string, string> = {}
 
   // Find all selectable items (type 'item') in the original items array
-  const selectableItems = props.items.filter(item => item.type === 'item' && item.value)
+  const selectableItems = props.items.filter(
+    (item) => item.type === 'item' && item.value,
+  )
 
   // Create a map of values to labels
-  selectableItems.forEach(item => {
+  selectableItems.forEach((item) => {
     if (item.value) {
       map[item.value] = item.label
     }

+ 30 - 16
components/Ui/Input/TreeSelect/EventCategories.vue

@@ -15,12 +15,14 @@ import EventCategory from '~/models/Core/EventCategory'
 const props = defineProps({
   modelValue: {
     type: Array as PropType<string[]>,
-    required: true
-  }
+    required: true,
+  },
 })
 
 const i18n = useI18n()
 
+const emit = defineEmits(['update:modelValue'])
+
 const { fetchCollection } = useEntityFetch()
 
 const { data: categories, pending } = fetchCollection(EventCategory)
@@ -36,8 +38,13 @@ const hierarchicalItems = computed(() => {
   const subFamiliesMap = new Map()
 
   // First pass: collect all unique families and subfamilies
-  categories.value.items.forEach(category => {
-    if (!category.famillyLabel || !category.subfamillyLabel || !category.genderLabel) return
+  categories.value.items.forEach((category) => {
+    if (
+      !category.famillyLabel ||
+      !category.subfamillyLabel ||
+      !category.genderLabel
+    )
+      return
 
     // Create unique keys for families and subfamilies
     const familyKey = category.famillyLabel
@@ -49,7 +56,7 @@ const hierarchicalItems = computed(() => {
         id: `family-${familyKey}`,
         label: i18n.t(category.famillyLabel),
         type: 'category',
-        level: 0
+        level: 0,
       })
     }
 
@@ -60,35 +67,40 @@ const hierarchicalItems = computed(() => {
         label: i18n.t(category.subfamillyLabel),
         type: 'subcategory',
         parentId: `family-${familyKey}`,
-        level: 1
+        level: 1,
       })
     }
   })
 
   // Convert families map to array and sort alphabetically by label
   const sortedFamilies = Array.from(familiesMap.values()).sort((a, b) =>
-    a.label.localeCompare(b.label)
+    a.label.localeCompare(b.label),
   )
 
   // Add sorted families to result
-  sortedFamilies.forEach(family => {
+  sortedFamilies.forEach((family) => {
     result.push(family)
   })
 
   // Convert subfamilies map to array and sort alphabetically by label
   const sortedSubfamilies = Array.from(subFamiliesMap.values()).sort((a, b) =>
-    a.label.localeCompare(b.label)
+    a.label.localeCompare(b.label),
   )
 
   // Add sorted subfamilies to result
-  sortedSubfamilies.forEach(subfamily => {
+  sortedSubfamilies.forEach((subfamily) => {
     result.push(subfamily)
   })
 
   // Collect all genders first, then sort and add to result
   const genders = []
-  categories.value.items.forEach(category => {
-    if (!category.famillyLabel || !category.subfamillyLabel || !category.genderLabel) return
+  categories.value.items.forEach((category) => {
+    if (
+      !category.famillyLabel ||
+      !category.subfamillyLabel ||
+      !category.genderLabel
+    )
+      return
 
     const familyKey = category.famillyLabel
     const subfamilyKey = `${category.famillyLabel}-${category.subfamillyLabel}`
@@ -99,14 +111,16 @@ const hierarchicalItems = computed(() => {
       value: category.id.toString(),
       type: 'item',
       parentId: `subfamily-${subfamilyKey}`,
-      level: 2
+      level: 2,
     })
   })
 
   // Sort genders alphabetically by label and add to result
-  genders.sort((a, b) => a.label.localeCompare(b.label)).forEach(gender => {
-    result.push(gender)
-  })
+  genders
+    .sort((a, b) => a.label.localeCompare(b.label))
+    .forEach((gender) => {
+      result.push(gender)
+    })
 
   return result
 })

+ 1 - 1
models/Core/EventCategory.ts

@@ -1,4 +1,4 @@
-import { Num, Str, Uid } from 'pinia-orm/dist/decorators'
+import { Str, Uid } from 'pinia-orm/dist/decorators'
 import ApiResource from '~/models/ApiResource'
 
 /**

+ 102 - 15
pages/dev/poc_tree_select_input.vue

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