codevdesign 0.0.91 → 0.0.92

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,124 +1,120 @@
1
- <template>
2
- <div>
3
- <v-row dense>
4
- <v-col
5
- cols="9"
6
- md="8"
7
- lg="6"
8
- class="d-flex align-end"
9
- >
10
- <div class="labelSwitchSiSwitchApres">{{ texte }}</div>
11
- </v-col>
12
- <v-col
13
- cols="2"
14
- class="d-flex align-end justify-end"
15
- >
16
- <v-switch
17
- v-model="maValeur"
18
- :disabled="desactiver"
19
- :density="densite"
20
- :inset="inset"
21
- hide-details
22
- @update:model-value="sauvegarder"
23
- />
24
- </v-col>
25
- </v-row>
26
- <v-row dense>
27
- <v-col
28
- cols="9"
29
- md="8"
30
- >
31
- <span v-html="texteDetaille"></span>
32
- </v-col>
33
- </v-row>
34
- </div>
35
- </template>
36
- <script setup lang="ts">
37
- import { ref, watch, onMounted, type PropType } from 'vue'
38
-
39
- // Définition des props
40
- const props = defineProps({
41
- valeurInverse: {
42
- type: Boolean,
43
- default: false,
44
- },
45
- desactiver: {
46
- type: Boolean,
47
- default: false,
48
- },
49
- inset: {
50
- type: Boolean,
51
- default: true,
52
- },
53
- modelValue: {
54
- type: Boolean,
55
- required: true,
56
- },
57
- texte: {
58
- type: String,
59
- required: true,
60
- },
61
- texteDetaille: {
62
- type: String,
63
- required: true,
64
- },
65
- densite: { type: String as PropType<'default' | 'comfortable' | 'compact'>, default: 'compact' },
66
- })
67
-
68
- // Définition de l'événement
69
- const emit = defineEmits(['update:modelValue'])
70
-
71
- // Référence pour la valeur interne
72
- const maValeur = ref<boolean>(false)
73
-
74
- // Montée du composant
75
- onMounted(() => {
76
- if (props.valeurInverse) {
77
- maValeur.value = !props.modelValue
78
- } else {
79
- maValeur.value = props.modelValue
80
- }
81
- })
82
-
83
- // Watcher pour suivre les changements de la prop 'value'
84
- watch(
85
- () => props.modelValue,
86
- newValue => {
87
- if (props.valeurInverse) {
88
- maValeur.value = !newValue
89
- } else {
90
- maValeur.value = newValue
91
- }
92
- },
93
- )
94
-
95
- watch(
96
- () => props.valeurInverse,
97
- newValue => {
98
- if (props.valeurInverse) {
99
- maValeur.value = newValue
100
- } else {
101
- maValeur.value = !newValue
102
- }
103
- },
104
- )
105
-
106
- // Méthode pour sauvegarder la valeur
107
- const sauvegarder = () => {
108
- if (props.valeurInverse) {
109
- emit('update:modelValue', !maValeur.value)
110
- } else {
111
- emit('update:modelValue', maValeur.value)
112
- }
113
- }
114
- </script>
115
-
116
- <style lang="css" scoped>
117
- .labelSwitchSiSwitchApres {
118
- font-weight: bold;
119
- margin-top: 25px;
120
- }
121
- .v-switch {
122
- margin-bottom: -10px; /* aligner le switch avec son texte*/
123
- }
124
- </style>
1
+ <template>
2
+ <div>
3
+ <v-row dense>
4
+ <v-col
5
+ cols="9"
6
+ md="8"
7
+ lg="6"
8
+ class="d-flex align-end"
9
+ >
10
+ <div class="labelSwitchSiSwitchApres">{{ texte }}</div>
11
+ </v-col>
12
+ <v-col
13
+ cols="2"
14
+ class="d-flex align-end justify-end"
15
+ >
16
+ <v-switch
17
+ v-model="maValeur"
18
+ :disabled="desactiver"
19
+ density="compact"
20
+ inset
21
+ v-bind="$attrs"
22
+ hide-details
23
+ @update:model-value="sauvegarder"
24
+ />
25
+ </v-col>
26
+ </v-row>
27
+ <v-row dense>
28
+ <v-col
29
+ cols="9"
30
+ md="8"
31
+ >
32
+ <span v-html="texteDetaille"></span>
33
+ </v-col>
34
+ </v-row>
35
+ </div>
36
+ </template>
37
+ <script setup lang="ts">
38
+ import { ref, watch, onMounted, type PropType } from 'vue'
39
+
40
+ // Définition des props
41
+ const props = defineProps({
42
+ valeurInverse: {
43
+ type: Boolean,
44
+ default: false,
45
+ },
46
+ desactiver: {
47
+ type: Boolean,
48
+ default: false,
49
+ },
50
+ modelValue: {
51
+ type: Boolean,
52
+ required: true,
53
+ },
54
+ texte: {
55
+ type: String,
56
+ required: true,
57
+ },
58
+ texteDetaille: {
59
+ type: String,
60
+ required: true,
61
+ },
62
+ })
63
+
64
+ // Définition de l'événement
65
+ const emit = defineEmits(['update:modelValue'])
66
+
67
+ // Référence pour la valeur interne
68
+ const maValeur = ref<boolean>(false)
69
+
70
+ // Montée du composant
71
+ onMounted(() => {
72
+ if (props.valeurInverse) {
73
+ maValeur.value = !props.modelValue
74
+ } else {
75
+ maValeur.value = props.modelValue
76
+ }
77
+ })
78
+
79
+ // Watcher pour suivre les changements de la prop 'value'
80
+ watch(
81
+ () => props.modelValue,
82
+ newValue => {
83
+ if (props.valeurInverse) {
84
+ maValeur.value = !newValue
85
+ } else {
86
+ maValeur.value = newValue
87
+ }
88
+ },
89
+ )
90
+
91
+ watch(
92
+ () => props.valeurInverse,
93
+ newValue => {
94
+ if (props.valeurInverse) {
95
+ maValeur.value = newValue
96
+ } else {
97
+ maValeur.value = !newValue
98
+ }
99
+ },
100
+ )
101
+
102
+ // Méthode pour sauvegarder la valeur
103
+ const sauvegarder = () => {
104
+ if (props.valeurInverse) {
105
+ emit('update:modelValue', !maValeur.value)
106
+ } else {
107
+ emit('update:modelValue', maValeur.value)
108
+ }
109
+ }
110
+ </script>
111
+
112
+ <style lang="css" scoped>
113
+ .labelSwitchSiSwitchApres {
114
+ font-weight: bold;
115
+ margin-top: 25px;
116
+ }
117
+ .v-switch {
118
+ margin-bottom: -10px; /* aligner le switch avec son texte*/
119
+ }
120
+ </style>
@@ -1,197 +1,197 @@
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
+ :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>