codevdesign 1.0.39 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,197 +1,198 @@
1
- <template>
2
- <div>
3
- <v-autocomplete
4
- v-model="selection"
5
- v-model:search="search"
6
- :items="items"
7
- :loading="chargementInterne"
8
- density="compact"
9
- :hide-no-data="!doitAfficherAucunResultat"
10
- :no-data-text="$t('csqc.csqcRechercheUtilisateur.aucunResultat')"
11
- :item-title="formatterUtilisateur"
12
- item-value="id"
13
- :placeholder="$t('csqc.csqcRechercheUtilisateur.placeholderRechercheUtilisateur')"
14
- return-object
15
- autofocus
16
- auto-select-first
17
- variant="outlined"
18
- />
19
- </div>
20
- </template>
21
-
22
- <script setup lang="ts">
23
- import { ref, watch, computed } from 'vue'
24
- import { useI18n } from 'vue-i18n'
25
- import appAxios from '../outils/appAxios'
26
- import type { EmployeMinsLsCodev } from '../modeles/employeMinsLsCodev'
27
-
28
- const props = defineProps<{
29
- matriculeDefaut?: string | null
30
- chargement?: boolean
31
- urlBase: string
32
- }>()
33
-
34
- const emit = defineEmits<{
35
- (e: 'selection', value: string | null): void
36
- (e: 'selectionPlus', utilisateur: EmployeMinsLsCodev | null): void
37
- (e: 'erreur', message: string): void
38
- }>()
39
-
40
- const { t } = useI18n({ useScope: 'global' })
41
-
42
- /** État interne */
43
- const selection = ref<EmployeMinsLsCodev | null>(null)
44
- const utilisateurs = ref<EmployeMinsLsCodev[]>([])
45
- const search = ref<string>('')
46
- const erreur = ref<string | null>(null)
47
-
48
- /** Charge interne : on combine le chargement externe (prop) et l’interne */
49
- const chargementInterne = ref(false)
50
- const chargementEffectif = computed(() => props.chargement === true || chargementInterne.value)
51
-
52
- /** Items sécurisés pour l’autocomplete */
53
- const items = computed<EmployeMinsLsCodev[]>(() => (Array.isArray(utilisateurs.value) ? utilisateurs.value : []))
54
-
55
- /** Cache clé→liste (toujours des arrays valides) */
56
- const cache = ref<Record<string, EmployeMinsLsCodev[]>>({})
57
-
58
- /** Détection d’un terme valide (limite les calls) */
59
- const estTermeValide = (terme?: string): boolean => !!terme && terme.trim().length >= 4 && !terme.includes('(')
60
-
61
- /** Affichage “aucun résultat” contrôlé */
62
- const doitAfficherAucunResultat = computed(
63
- () => estTermeValide(search.value) && !chargementEffectif.value && items.value.length === 0,
64
- )
65
-
66
- /** Formattage d’un employé en ligne lisible */
67
- const formatterUtilisateur = (item: EmployeMinsLsCodev): string => {
68
- const lieu = [item.numeroLieuPrincipal, item.nomLieuPrincipal].filter(Boolean).join('-')
69
- const corps = [item.corpsEmploiPrincipal, item.corpsEmploiPrincipalDescription].filter(Boolean).join('-')
70
- return `${item.prenom} ${item.nom} (${item.matricule}), ${item.courrielProfessionnel}${lieu ? `, ${lieu}` : ''}${corps ? `, ${corps}` : ''}`
71
- }
72
-
73
- /** Type guard: on s’assure qu’on a bien un tableau d’employés */
74
- function isEmployeArray(x: unknown): x is EmployeMinsLsCodev[] {
75
- return (
76
- Array.isArray(x) && x.every(u => u && typeof u === 'object' && 'prenom' in u && 'nom' in u && 'matricule' in u)
77
- )
78
- }
79
-
80
- /** Réinitialise proprement la liste + émet une erreur optionnelle */
81
- function safeClearUsers(msg?: string) {
82
- utilisateurs.value = []
83
- if (msg) {
84
- erreur.value = msg
85
- emit('erreur', msg)
86
- }
87
- }
88
-
89
- /** Anti-race condition: on ne retient le résultat que si l’ID correspond */
90
- let lastQueryId = 0
91
-
92
- async function rechercherUtilisateurs(terme?: string) {
93
- if (!estTermeValide(terme)) {
94
- utilisateurs.value = []
95
- return
96
- }
97
-
98
- const queryId = ++lastQueryId
99
- const termeLower = terme!.toLowerCase()
100
-
101
- // Cache prefix-match: on prend la plus longue clé qui matche le début
102
- const cachedKey = Object.keys(cache.value)
103
- .filter(k => termeLower.startsWith(k))
104
- .sort((a, b) => b.length - a.length)[0]
105
-
106
- if (cachedKey) {
107
- const cached = cache.value[cachedKey]
108
- if (isEmployeArray(cached)) {
109
- // Filtre côté client pour les affiner
110
- const tl = termeLower
111
- utilisateurs.value = cached.filter(user => formatterUtilisateur(user).toLowerCase().includes(tl))
112
- return
113
- } else {
114
- // Cache corrompu (théorique)
115
- delete cache.value[cachedKey]
116
- }
117
- }
118
-
119
- try {
120
- chargementInterne.value = true
121
- const url = `${props.urlBase}/api/ComposantUI/Utilisateurs/${encodeURIComponent(terme as string)}`
122
- const reponse = await appAxios.getAxios().get<unknown>(url)
123
- const data = (reponse as any)?.data ?? reponse
124
-
125
- if (queryId !== lastQueryId) {
126
- // Une requête plus récente est arrivée, on ignore
127
- return
128
- }
129
-
130
- if (!isEmployeArray(data)) {
131
- // Probable HTML/redirect login/erreur
132
- safeClearUsers(t('csqc.csqcRechercheUtilisateur.erreur'))
133
- return
134
- }
135
-
136
- utilisateurs.value = data
137
- cache.value[termeLower] = data
138
- } catch (e: unknown) {
139
- let message = t('csqc.csqcRechercheUtilisateur.erreur')
140
- if (e instanceof Error) message = e.message
141
- else if (typeof e === 'string') message = e
142
- safeClearUsers(message)
143
- } finally {
144
- chargementInterne.value = false
145
- }
146
- }
147
-
148
- /** Debounce recherche */
149
- let timer: number | undefined
150
- watch(
151
- search,
152
- terme => {
153
- if (chargementEffectif.value) return
154
- window.clearTimeout(timer)
155
- timer = window.setTimeout(() => rechercherUtilisateurs(terme), 250)
156
- },
157
- { flush: 'post' },
158
- )
159
-
160
- /** Émettre la sélection (email & objet complet) */
161
- watch(selection, utilisateur => {
162
- emit('selection', utilisateur?.courrielProfessionnel ?? null)
163
- emit('selectionPlus', utilisateur ?? null)
164
- })
165
-
166
- /** Préchargement par matricule par défaut */
167
- watch(
168
- () => props.matriculeDefaut,
169
- async (nv, ov) => {
170
- // Si vide / nul → on nettoie
171
- if (!nv || nv.trim() === '') {
172
- selection.value = null
173
- search.value = ''
174
- utilisateurs.value = []
175
- return
176
- }
177
-
178
- // Si la même valeur est déjà sélectionnée → rien à faire
179
- if (selection.value?.matricule === nv) return
180
-
181
- // Recherche par matricule par défaut
182
- await rechercherUtilisateurs(nv)
183
- if (utilisateurs.value.length > 0) {
184
- const utilisateur = utilisateurs.value.find(u => u.matricule === nv) ?? null
185
- if (utilisateur) {
186
- search.value = formatterUtilisateur(utilisateur)
187
- selection.value = utilisateur
188
- } else {
189
- // Pas trouvé → reset “propre”
190
- selection.value = null
191
- search.value = nv // ou ''
192
- }
193
- }
194
- },
195
- { immediate: true },
196
- )
197
- </script>
1
+ <template>
2
+ <div>
3
+ <v-autocomplete
4
+ v-model="selection"
5
+ v-model:search="search"
6
+ :items="items"
7
+ v-bind="$attrs"
8
+ :loading="chargementInterne"
9
+ density="compact"
10
+ :hide-no-data="!doitAfficherAucunResultat"
11
+ :no-data-text="$t('csqc.csqcRechercheUtilisateur.aucunResultat')"
12
+ :item-title="formatterUtilisateur"
13
+ item-value="id"
14
+ :placeholder="$t('csqc.csqcRechercheUtilisateur.placeholderRechercheUtilisateur')"
15
+ return-object
16
+ autofocus
17
+ auto-select-first
18
+ variant="outlined"
19
+ />
20
+ </div>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import { ref, watch, computed } from 'vue'
25
+ import { useI18n } from 'vue-i18n'
26
+ import appAxios from '../outils/appAxios'
27
+ import type { EmployeMinsLsCodev } from '../modeles/employeMinsLsCodev'
28
+
29
+ const props = defineProps<{
30
+ matriculeDefaut?: string | null
31
+ chargement?: boolean
32
+ urlBase: string
33
+ }>()
34
+
35
+ const emit = defineEmits<{
36
+ (e: 'selection', value: string | null): void
37
+ (e: 'selectionPlus', utilisateur: EmployeMinsLsCodev | null): void
38
+ (e: 'erreur', message: string): void
39
+ }>()
40
+
41
+ const { t } = useI18n({ useScope: 'global' })
42
+
43
+ /** État interne */
44
+ const selection = ref<EmployeMinsLsCodev | null>(null)
45
+ const utilisateurs = ref<EmployeMinsLsCodev[]>([])
46
+ const search = ref<string>('')
47
+ const erreur = ref<string | null>(null)
48
+
49
+ /** Charge interne : on combine le chargement externe (prop) et l’interne */
50
+ const chargementInterne = ref(false)
51
+ const chargementEffectif = computed(() => props.chargement === true || chargementInterne.value)
52
+
53
+ /** Items sécurisés pour l’autocomplete */
54
+ const items = computed<EmployeMinsLsCodev[]>(() => (Array.isArray(utilisateurs.value) ? utilisateurs.value : []))
55
+
56
+ /** Cache clé→liste (toujours des arrays valides) */
57
+ const cache = ref<Record<string, EmployeMinsLsCodev[]>>({})
58
+
59
+ /** Détection d’un terme valide (limite les calls) */
60
+ const estTermeValide = (terme?: string): boolean => !!terme && terme.trim().length >= 4 && !terme.includes('(')
61
+
62
+ /** Affichage “aucun résultat” contrôlé */
63
+ const doitAfficherAucunResultat = computed(
64
+ () => estTermeValide(search.value) && !chargementEffectif.value && items.value.length === 0,
65
+ )
66
+
67
+ /** Formattage d’un employé en ligne lisible */
68
+ const formatterUtilisateur = (item: EmployeMinsLsCodev): string => {
69
+ const lieu = [item.numeroLieuPrincipal, item.nomLieuPrincipal].filter(Boolean).join('-')
70
+ const corps = [item.corpsEmploiPrincipal, item.corpsEmploiPrincipalDescription].filter(Boolean).join('-')
71
+ return `${item.prenom} ${item.nom} (${item.matricule}), ${item.courrielProfessionnel}${lieu ? `, ${lieu}` : ''}${corps ? `, ${corps}` : ''}`
72
+ }
73
+
74
+ /** Type guard: on s’assure qu’on a bien un tableau d’employés */
75
+ function isEmployeArray(x: unknown): x is EmployeMinsLsCodev[] {
76
+ return (
77
+ Array.isArray(x) && x.every(u => u && typeof u === 'object' && 'prenom' in u && 'nom' in u && 'matricule' in u)
78
+ )
79
+ }
80
+
81
+ /** Réinitialise proprement la liste + émet une erreur optionnelle */
82
+ function safeClearUsers(msg?: string) {
83
+ utilisateurs.value = []
84
+ if (msg) {
85
+ erreur.value = msg
86
+ emit('erreur', msg)
87
+ }
88
+ }
89
+
90
+ /** Anti-race condition: on ne retient le résultat que si l’ID correspond */
91
+ let lastQueryId = 0
92
+
93
+ async function rechercherUtilisateurs(terme?: string) {
94
+ if (!estTermeValide(terme)) {
95
+ utilisateurs.value = []
96
+ return
97
+ }
98
+
99
+ const queryId = ++lastQueryId
100
+ const termeLower = terme!.toLowerCase()
101
+
102
+ // Cache prefix-match: on prend la plus longue clé qui matche le début
103
+ const cachedKey = Object.keys(cache.value)
104
+ .filter(k => termeLower.startsWith(k))
105
+ .sort((a, b) => b.length - a.length)[0]
106
+
107
+ if (cachedKey) {
108
+ const cached = cache.value[cachedKey]
109
+ if (isEmployeArray(cached)) {
110
+ // Filtre côté client pour les affiner
111
+ const tl = termeLower
112
+ utilisateurs.value = cached.filter(user => formatterUtilisateur(user).toLowerCase().includes(tl))
113
+ return
114
+ } else {
115
+ // Cache corrompu (théorique)
116
+ delete cache.value[cachedKey]
117
+ }
118
+ }
119
+
120
+ try {
121
+ chargementInterne.value = true
122
+ const url = `${props.urlBase}/api/ComposantUI/Utilisateurs/${encodeURIComponent(terme as string)}`
123
+ const reponse = await appAxios.getAxios().get<unknown>(url)
124
+ const data = (reponse as any)?.data ?? reponse
125
+
126
+ if (queryId !== lastQueryId) {
127
+ // Une requête plus récente est arrivée, on ignore
128
+ return
129
+ }
130
+
131
+ if (!isEmployeArray(data)) {
132
+ // Probable HTML/redirect login/erreur
133
+ safeClearUsers(t('csqc.csqcRechercheUtilisateur.erreur'))
134
+ return
135
+ }
136
+
137
+ utilisateurs.value = data
138
+ cache.value[termeLower] = data
139
+ } catch (e: unknown) {
140
+ let message = t('csqc.csqcRechercheUtilisateur.erreur')
141
+ if (e instanceof Error) message = e.message
142
+ else if (typeof e === 'string') message = e
143
+ safeClearUsers(message)
144
+ } finally {
145
+ chargementInterne.value = false
146
+ }
147
+ }
148
+
149
+ /** Debounce recherche */
150
+ let timer: number | undefined
151
+ watch(
152
+ search,
153
+ terme => {
154
+ if (chargementEffectif.value) return
155
+ window.clearTimeout(timer)
156
+ timer = window.setTimeout(() => rechercherUtilisateurs(terme), 250)
157
+ },
158
+ { flush: 'post' },
159
+ )
160
+
161
+ /** Émettre la sélection (email & objet complet) */
162
+ watch(selection, utilisateur => {
163
+ emit('selection', utilisateur?.courrielProfessionnel ?? null)
164
+ emit('selectionPlus', utilisateur ?? null)
165
+ })
166
+
167
+ /** Préchargement par matricule par défaut */
168
+ watch(
169
+ () => props.matriculeDefaut,
170
+ async (nv, ov) => {
171
+ // Si vide / nul on nettoie
172
+ if (!nv || nv.trim() === '') {
173
+ selection.value = null
174
+ search.value = ''
175
+ utilisateurs.value = []
176
+ return
177
+ }
178
+
179
+ // Si la même valeur est déjà sélectionnée → rien à faire
180
+ if (selection.value?.matricule === nv) return
181
+
182
+ // Recherche par matricule par défaut
183
+ await rechercherUtilisateurs(nv)
184
+ if (utilisateurs.value.length > 0) {
185
+ const utilisateur = utilisateurs.value.find(u => u.matricule === nv) ?? null
186
+ if (utilisateur) {
187
+ search.value = formatterUtilisateur(utilisateur)
188
+ selection.value = utilisateur
189
+ } else {
190
+ // Pas trouvé → reset “propre”
191
+ selection.value = null
192
+ search.value = nv // ou ''
193
+ }
194
+ }
195
+ },
196
+ { immediate: true },
197
+ )
198
+ </script>
@@ -1,88 +1,88 @@
1
- <template>
2
- <v-snackbar
3
- v-model="snackbar"
4
- v-bind="$attrs"
5
- :timeout="tempsFinal"
6
- :style="styleCss"
7
- color="success"
8
- multi-line
9
- @update:model-value="fermer"
10
- >
11
- <template v-if="props.titre || props.message">
12
- <b style="font-size: 12pt">{{ props.titre }}</b>
13
- <br v-if="props.titre" />
14
- <b>{{ props.message }}</b>
15
- </template>
16
-
17
- <template #actions>
18
- <!-- on affiche tjrs si c'est -1 car on ne pourra pas fermer le snack -->
19
- <v-icon
20
- v-if="props.btnFermer || props.temps === -1"
21
- color="white"
22
- style="cursor: pointer"
23
- @click.stop="fermer"
24
- >
25
- mdi-close
26
- </v-icon>
27
- </template>
28
- </v-snackbar>
29
- </template>
30
- <script lang="ts" setup>
31
- import { computed, ref, watch, type PropType } from 'vue'
32
-
33
- // Définir les props avec les types
34
- const props = defineProps({
35
- styleCss: {
36
- type: String,
37
- required: false,
38
- },
39
- temps: {
40
- type: Number,
41
- required: false,
42
- default: 4000,
43
- validator(value: number) {
44
- // Si la valeur est inférieure à -1, on la remplace par -1
45
- if (value < -1) {
46
- return false // Laisser échouer la validation pour gérer ça dans une logique personnalisée
47
- }
48
- // Validation que la valeur soit comprise entre -1 et Number.MAX_VALUE
49
- return value <= Number.MAX_VALUE
50
- },
51
- },
52
- message: {
53
- type: String,
54
- required: true,
55
- },
56
- titre: {
57
- type: String,
58
- required: false,
59
- },
60
- btnFermer: {
61
- type: Boolean,
62
- required: false,
63
- default: true,
64
- },
65
- })
66
- const emit = defineEmits(['fermer:snackbar'])
67
- const fermer = (): void => {
68
- snackbar.value = false
69
- emit('fermer:snackbar')
70
- }
71
- // Déclarer l'état réactif pour snackbar
72
- const snackbar = ref(false)
73
- const message = computed(() => props.message)
74
- const tempsFinal = computed(() => {
75
- if (props.temps < -1) {
76
- return -1 // Si la valeur est inférieure à -1, on la met à -1
77
- }
78
- if (props.temps >= 0 && props.temps < 1000) return 1000 // on met 1 seconde minimum sinon one ne la voit pas
79
-
80
- return props.temps // Sinon, on retourne la valeur de la prop
81
- })
82
-
83
- watch(message, nouveau => {
84
- if (nouveau != null && nouveau !== '') {
85
- snackbar.value = true
86
- }
87
- })
88
- </script>
1
+ <template>
2
+ <v-snackbar
3
+ v-model="snackbar"
4
+ v-bind="$attrs"
5
+ :timeout="tempsFinal"
6
+ :style="styleCss"
7
+ color="success"
8
+ multi-line
9
+ @update:model-value="fermer"
10
+ >
11
+ <template v-if="props.titre || props.message">
12
+ <b style="font-size: 12pt">{{ props.titre }}</b>
13
+ <br v-if="props.titre" />
14
+ <b>{{ props.message }}</b>
15
+ </template>
16
+
17
+ <template #actions>
18
+ <!-- on affiche tjrs si c'est -1 car on ne pourra pas fermer le snack -->
19
+ <v-icon
20
+ v-if="props.btnFermer || props.temps === -1"
21
+ color="white"
22
+ style="cursor: pointer"
23
+ @click.stop="fermer"
24
+ >
25
+ mdi-close
26
+ </v-icon>
27
+ </template>
28
+ </v-snackbar>
29
+ </template>
30
+ <script lang="ts" setup>
31
+ import { computed, ref, watch, type PropType } from 'vue'
32
+
33
+ // Définir les props avec les types
34
+ const props = defineProps({
35
+ styleCss: {
36
+ type: String,
37
+ required: false,
38
+ },
39
+ temps: {
40
+ type: Number,
41
+ required: false,
42
+ default: 4000,
43
+ validator(value: number) {
44
+ // Si la valeur est inférieure à -1, on la remplace par -1
45
+ if (value < -1) {
46
+ return false // Laisser échouer la validation pour gérer ça dans une logique personnalisée
47
+ }
48
+ // Validation que la valeur soit comprise entre -1 et Number.MAX_VALUE
49
+ return value <= Number.MAX_VALUE
50
+ },
51
+ },
52
+ message: {
53
+ type: String,
54
+ required: true,
55
+ },
56
+ titre: {
57
+ type: String,
58
+ required: false,
59
+ },
60
+ btnFermer: {
61
+ type: Boolean,
62
+ required: false,
63
+ default: true,
64
+ },
65
+ })
66
+ const emit = defineEmits(['fermer:snackbar'])
67
+ const fermer = (): void => {
68
+ snackbar.value = false
69
+ emit('fermer:snackbar')
70
+ }
71
+ // Déclarer l'état réactif pour snackbar
72
+ const snackbar = ref(false)
73
+ const message = computed(() => props.message)
74
+ const tempsFinal = computed(() => {
75
+ if (props.temps < -1) {
76
+ return -1 // Si la valeur est inférieure à -1, on la met à -1
77
+ }
78
+ if (props.temps >= 0 && props.temps < 1000) return 1000 // on met 1 seconde minimum sinon one ne la voit pas
79
+
80
+ return props.temps // Sinon, on retourne la valeur de la prop
81
+ })
82
+
83
+ watch(message, nouveau => {
84
+ if (nouveau != null && nouveau !== '') {
85
+ snackbar.value = true
86
+ }
87
+ })
88
+ </script>