codevdesign 1.0.76 → 1.0.78

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,97 +1,97 @@
1
- <template>
2
- <csqcDialogue
3
- ref="modale"
4
- :titre="titre"
5
- activator="supprimer"
6
- v-bind="$attrs"
7
- :largeur="largeur"
8
- @ok="confirmer"
9
- @annuler="annuler"
10
- >
11
- <v-form
12
- ref="form"
13
- @submit.prevent
14
- >
15
- <v-row>
16
- <v-col
17
- cols="12"
18
- class="pa-0 ma-0"
19
- >
20
- <span v-html="texte"></span>
21
- </v-col>
22
- </v-row>
23
-
24
- <v-row>
25
- <v-col
26
- cols="12"
27
- class="pa-0 ma-0"
28
- >
29
- <v-text-field
30
- v-if="!multiligne"
31
- v-model="reponse"
32
- autofocus
33
- @keyup.enter="confirmer"
34
- />
35
- <v-textarea
36
- v-if="multiligne"
37
- v-model="reponse"
38
- :rows="3"
39
- autofocus
40
- />
41
- </v-col>
42
- </v-row>
43
- </v-form>
44
- </csqcDialogue>
45
- </template>
46
-
47
- <script lang="ts" setup>
48
- import { ref } from 'vue'
49
- import csqcDialogue from './csqcDialogue.vue'
50
-
51
- const modale = ref<InstanceType<typeof csqcDialogue> | null>(null)
52
- const utilisateurATermine = ref(false)
53
- const reponse = ref<string | null>(null)
54
- const texte = ref('')
55
- const titre = ref('')
56
- const largeur = ref('525px')
57
- const multiligne = ref(false)
58
-
59
- const ouvrir = async (
60
- titreParam: string,
61
- message: string,
62
- valeurDefaut: string | null = null,
63
- multiligneParam: boolean = false,
64
- largeurParam = '525px',
65
- ) => {
66
- texte.value = message
67
- titre.value = titreParam
68
- reponse.value = valeurDefaut
69
- multiligne.value = multiligneParam
70
- largeur.value = largeurParam
71
- utilisateurATermine.value = false
72
- modale.value?.ouvrir()
73
-
74
- while (!utilisateurATermine.value) {
75
- await new Promise(resolve => setTimeout(resolve, 100)) // Attendre 100ms
76
- }
77
-
78
- return reponse.value
79
- }
80
-
81
- const confirmer = (): void => {
82
- utilisateurATermine.value = true
83
- fermer()
84
- }
85
-
86
- const annuler = (): void => {
87
- reponse.value = null
88
- utilisateurATermine.value = true
89
- fermer()
90
- }
91
-
92
- const fermer = (): void => {
93
- modale.value?.fermer()
94
- }
95
-
96
- defineExpose({ ouvrir, fermer })
97
- </script>
1
+ <template>
2
+ <csqcDialogue
3
+ ref="modale"
4
+ :titre="titre"
5
+ activator="supprimer"
6
+ :largeur="largeur"
7
+ v-bind="$attrs"
8
+ @ok="confirmer"
9
+ @annuler="annuler"
10
+ >
11
+ <v-form
12
+ ref="form"
13
+ @submit.prevent
14
+ >
15
+ <v-row>
16
+ <v-col
17
+ cols="12"
18
+ class="pa-0 ma-0"
19
+ >
20
+ <span v-html="texte"></span>
21
+ </v-col>
22
+ </v-row>
23
+
24
+ <v-row>
25
+ <v-col
26
+ cols="12"
27
+ class="pa-0 ma-0"
28
+ >
29
+ <v-text-field
30
+ v-if="!multiligne"
31
+ v-model="reponse"
32
+ autofocus
33
+ @keyup.enter="confirmer"
34
+ />
35
+ <v-textarea
36
+ v-if="multiligne"
37
+ v-model="reponse"
38
+ :rows="3"
39
+ autofocus
40
+ />
41
+ </v-col>
42
+ </v-row>
43
+ </v-form>
44
+ </csqcDialogue>
45
+ </template>
46
+
47
+ <script lang="ts" setup>
48
+ import { ref } from 'vue'
49
+ import csqcDialogue from './csqcDialogue.vue'
50
+
51
+ const modale = ref<InstanceType<typeof csqcDialogue> | null>(null)
52
+ const utilisateurATermine = ref(false)
53
+ const reponse = ref<string | null>(null)
54
+ const texte = ref('')
55
+ const titre = ref('')
56
+ const largeur = ref('525px')
57
+ const multiligne = ref(false)
58
+
59
+ const ouvrir = async (
60
+ titreParam: string,
61
+ message: string,
62
+ valeurDefaut: string | null = null,
63
+ multiligneParam: boolean = false,
64
+ largeurParam = '525px',
65
+ ) => {
66
+ texte.value = message
67
+ titre.value = titreParam
68
+ reponse.value = valeurDefaut
69
+ multiligne.value = multiligneParam
70
+ largeur.value = largeurParam
71
+ utilisateurATermine.value = false
72
+ modale.value?.ouvrir()
73
+
74
+ while (!utilisateurATermine.value) {
75
+ await new Promise(resolve => setTimeout(resolve, 100)) // Attendre 100ms
76
+ }
77
+
78
+ return reponse.value
79
+ }
80
+
81
+ const confirmer = (): void => {
82
+ utilisateurATermine.value = true
83
+ fermer()
84
+ }
85
+
86
+ const annuler = (): void => {
87
+ reponse.value = null
88
+ utilisateurATermine.value = true
89
+ fermer()
90
+ }
91
+
92
+ const fermer = (): void => {
93
+ modale.value?.fermer()
94
+ }
95
+
96
+ defineExpose({ ouvrir, fermer })
97
+ </script>
@@ -1,198 +1,198 @@
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="($attrs.density as any) ?? '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="($attrs.autofocus as any) ?? true"
17
- auto-select-first
18
- :variant="($attrs.variant as any) ?? '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
+ <template>
2
+ <div>
3
+ <v-autocomplete
4
+ v-model="selection"
5
+ v-model:search="search"
6
+ :items="items"
7
+ :loading="chargementInterne"
8
+ :density="($attrs.density as any) ?? '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="($attrs.autofocus as any) ?? true"
16
+ auto-select-first
17
+ :variant="($attrs.variant as any) ?? 'outlined'"
18
+ v-bind="$attrs"
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>