codevdesign 1.0.34 → 1.0.36

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.
Files changed (105) hide show
  1. package/assets/csqc.css +259 -0
  2. package/composants/csqcAide.vue +55 -0
  3. package/composants/csqcAlerteErreur.vue +87 -0
  4. package/composants/csqcChaise/chaiseConteneur.vue +367 -0
  5. package/composants/csqcChaise/chaiseItem.vue +54 -0
  6. package/composants/csqcCodeBudgetaireGenerique.vue +336 -0
  7. package/composants/csqcConfirmation.vue +75 -0
  8. package/composants/csqcDate.vue +57 -0
  9. package/composants/csqcDialogue.vue +118 -0
  10. package/composants/csqcEditeurTexteRiche.vue +380 -0
  11. package/composants/csqcEntete.vue +163 -0
  12. package/composants/csqcImportCSV.vue +125 -0
  13. package/composants/csqcModaleSaisie.vue +95 -0
  14. package/composants/csqcOptionSwitch.vue +120 -0
  15. package/composants/csqcRecherche.vue +213 -0
  16. package/composants/csqcRechercheUtilisateur.vue +197 -0
  17. package/composants/csqcSnackbar.vue +88 -0
  18. package/composants/csqcTable/csqcTable.vue +383 -0
  19. package/composants/csqcTable/csqcTableExportExcel.vue +58 -0
  20. package/composants/csqcTable/csqcTableModaleChoixColonnes.vue +586 -0
  21. package/composants/csqcTexteBilingue.vue +175 -0
  22. package/composants/csqcTiroir.vue +156 -0
  23. package/composants/gabarit/csqcMenu.vue +281 -0
  24. package/composants/gabarit/pivEntete.vue +205 -0
  25. package/composants/gabarit/pivPiedPage.vue +70 -0
  26. package/composants/gabarit/vueDefault.vue +5 -0
  27. package/composants/validateurs.ts +183 -0
  28. package/enums/choixLangue.ts +10 -0
  29. package/index.ts +75 -0
  30. package/locales/en.json +100 -0
  31. package/locales/fr.json +101 -0
  32. package/modeles/apiReponse.ts +12 -0
  33. package/modeles/assurancesAssuranceGeneraleGrics.ts +10 -0
  34. package/modeles/assurancesAssurancePersonnelleGrics.ts +13 -0
  35. package/modeles/assurancesContratGrics.ts +14 -0
  36. package/modeles/assurancesDetailsPrimeReguliereGrics.ts +12 -0
  37. package/modeles/assurancesDonneesAssureurGrics.ts +13 -0
  38. package/modeles/assurancesEmployeGrics.ts +11 -0
  39. package/modeles/assurancesGrics.ts +14 -0
  40. package/modeles/assurancesRegimeAssuranceGrics.ts +9 -0
  41. package/modeles/assurancesRegimeBaseEmployeurGrics.ts +9 -0
  42. package/modeles/assurancesRegimeBaseGrics.ts +9 -0
  43. package/modeles/composants/csqcMenuModele.ts +18 -0
  44. package/modeles/composants/datatableColonne.ts +31 -0
  45. package/modeles/composants/snackbar.ts +18 -0
  46. package/modeles/data.ts +24 -0
  47. package/modeles/droitIntervention.ts +14 -0
  48. package/modeles/employeAdresseGrics.ts +13 -0
  49. package/modeles/employeAdressesPersonnellesGrics.ts +12 -0
  50. package/modeles/employeAffectationCorpsEmploiGrics.ts +9 -0
  51. package/modeles/employeBanquesCongeBanqueGrics.ts +9 -0
  52. package/modeles/employeBanquesCongeGrics.ts +14 -0
  53. package/modeles/employeBanquesCongeRegimeAbsenceGrics.ts +9 -0
  54. package/modeles/employeCourrielsPersonnels.ts +9 -0
  55. package/modeles/employeCourrielsProfessionnels.ts +9 -0
  56. package/modeles/employeEmploisCategorieGrics.ts +9 -0
  57. package/modeles/employeEmploisClasseGrics.ts +9 -0
  58. package/modeles/employeEmploisCorpsEmploiGrics.ts +9 -0
  59. package/modeles/employeEmploisEtatEmploiGrics.ts +9 -0
  60. package/modeles/employeEmploisGrics.ts +37 -0
  61. package/modeles/employeEmploisGroupePaieGrics.ts +9 -0
  62. package/modeles/employeEmploisLieuTravailPrincipalGrics.ts +10 -0
  63. package/modeles/employeEmploisLieuxTravailSecondairesGrics.ts +10 -0
  64. package/modeles/employeEmploisRegimeAbsenceGrics.ts +9 -0
  65. package/modeles/employeEmploisSecteurGrics.ts +9 -0
  66. package/modeles/employeEmploisStatutEngagementGrics.ts +9 -0
  67. package/modeles/employeExperienceEmploiGrics.ts +9 -0
  68. package/modeles/employeExperienceEmployeGrics.ts +12 -0
  69. package/modeles/employeExperienceExperiencesGrics.ts +11 -0
  70. package/modeles/employeExperienceExperiencesTotalesGrics.ts +15 -0
  71. package/modeles/employeExperienceGrics.ts +17 -0
  72. package/modeles/employeGrics.ts +31 -0
  73. package/modeles/employeMinsLsCodev.ts +10 -0
  74. package/modeles/employeTelephoneGrics.ts +12 -0
  75. package/modeles/employeTelephonesPersonnelsGrics.ts +11 -0
  76. package/modeles/employeTelephonesProfessionnelsGrics.ts +11 -0
  77. package/modeles/groupeCE.ts +14 -0
  78. package/modeles/groupeCEIntervalle.ts +13 -0
  79. package/modeles/historiquesAbsenceBanqueGrics.ts +9 -0
  80. package/modeles/historiquesAbsenceGrics.ts +21 -0
  81. package/modeles/historiquesAbsenceLieuTravailGrics.ts +9 -0
  82. package/modeles/historiquesAbsenceSousBanqueGrics.ts +9 -0
  83. package/modeles/intervention.ts +35 -0
  84. package/modeles/motifsAbsenceBanque.ts +9 -0
  85. package/modeles/motifsAbsenceGrics.ts +17 -0
  86. package/modeles/motifsAbsenceRegimeAbsence.ts +9 -0
  87. package/modeles/motifsAbsenceSousMotifs.ts +9 -0
  88. package/modeles/motifsAbsenceTraitementBanques.ts +11 -0
  89. package/modeles/notificationGabaritDefaut.ts +10 -0
  90. package/modeles/response.ts +12 -0
  91. package/modeles/role.ts +31 -0
  92. package/modeles/roleMin.ts +12 -0
  93. package/modeles/syndicat.ts +26 -0
  94. package/modeles/syndicatGroupeCe.ts +10 -0
  95. package/modeles/syndicatResponsable.ts +15 -0
  96. package/modeles/syndicatUnite.ts +10 -0
  97. package/modeles/typeEnseignement.ts +14 -0
  98. package/modeles/typeTelephone.ts +12 -0
  99. package/modeles/unite.ts +23 -0
  100. package/modeles/uniteTypeEnseignement.ts +12 -0
  101. package/modeles/utilisateur.ts +15 -0
  102. package/outils/appAxios.ts +116 -0
  103. package/outils/csqcOutils.ts +366 -0
  104. package/outils/rafraichisseurToken.ts +187 -0
  105. package/package.json +6 -12
@@ -0,0 +1,95 @@
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
+ multiligneParam: boolean = false,
63
+ largeurParam = '525px',
64
+ ) => {
65
+ texte.value = message
66
+ titre.value = titreParam
67
+ multiligne.value = multiligneParam
68
+ largeur.value = largeurParam
69
+ utilisateurATermine.value = false
70
+ modale.value?.ouvrir()
71
+
72
+ while (!utilisateurATermine.value) {
73
+ await new Promise(resolve => setTimeout(resolve, 100)) // Attendre 100ms
74
+ }
75
+
76
+ return reponse.value
77
+ }
78
+
79
+ const confirmer = (): void => {
80
+ utilisateurATermine.value = true
81
+ fermer()
82
+ }
83
+
84
+ const annuler = (): void => {
85
+ reponse.value = null
86
+ utilisateurATermine.value = true
87
+ fermer()
88
+ }
89
+
90
+ const fermer = (): void => {
91
+ modale.value?.fermer()
92
+ }
93
+
94
+ defineExpose({ ouvrir, fermer })
95
+ </script>
@@ -0,0 +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="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>
@@ -0,0 +1,213 @@
1
+ <template>
2
+ <div>
3
+ <v-row
4
+ dense
5
+ class="pt-0 mt-0"
6
+ >
7
+ <v-col
8
+ cols="12"
9
+ sm="auto"
10
+ >
11
+ <slot name="recherche"></slot>
12
+ <v-text-field
13
+ v-if="afficher"
14
+ v-model="rechercheLocale"
15
+ :label="rechercheTexte ? rechercheTexte : $t('csqc.label.recherche')"
16
+ single-line
17
+ density="compact"
18
+ hide-details
19
+ v-bind="$attrs"
20
+ clearable
21
+ :rounded="0"
22
+ variant="outlined"
23
+ max-width="640"
24
+ min-width="500"
25
+ class="BarreRecherche"
26
+ :loading="chargement"
27
+ :disabled="chargement || desactiver"
28
+ >
29
+ <template #append>
30
+ <v-btn
31
+ class="BarreRechercheBackIcone"
32
+ :loading="chargement"
33
+ :disabled="chargement || desactiver"
34
+ >
35
+ <v-icon
36
+ class="BarreRechercheIcone"
37
+ icon="mdi-magnify"
38
+ />
39
+ </v-btn>
40
+ </template>
41
+ </v-text-field>
42
+ </v-col>
43
+ <!-- Entre recherche et ajouté-->
44
+ <v-col
45
+ cols="12"
46
+ sm="auto"
47
+ class="pt-1 mt-0 pb-0 mb-0"
48
+ >
49
+ <slot name="milieu" />
50
+ </v-col>
51
+
52
+ <!-- Affichage des boutons et icônes-->
53
+ <v-col
54
+ cols="12"
55
+ sm="auto"
56
+ class="flex-grow-1 d-flex justify-end"
57
+ >
58
+ <slot name="droite" />
59
+ </v-col>
60
+ </v-row>
61
+ <v-row
62
+ dense
63
+ class="pt-0 mt-0"
64
+ >
65
+ <v-col
66
+ cols="12"
67
+ :lg="rechercheAvanceeLargeur"
68
+ >
69
+ <v-expansion-panels
70
+ v-if="rechercheAvancee"
71
+ static
72
+ :readonly="chargement || desactiver"
73
+ expand-icon=""
74
+ @update:model-value="onPanelChange"
75
+ >
76
+ <v-expansion-panel
77
+ flat
78
+ elevation="0"
79
+ >
80
+ <v-expansion-panel-title
81
+ style="color: #095797"
82
+ class="pl-0 ml-0"
83
+ ><slot name="rechercheAvanceeTitre">
84
+ <span
85
+ style="text-decoration: underline"
86
+ v-html="rechercheAvanceeTexte ? rechercheAvanceeTexte : $t('csqc.label.rechercheAvanceeDefaut')"
87
+ ></span>
88
+ <slot name="rechercheAvanceeApresTitre"></slot
89
+ ></slot>
90
+ </v-expansion-panel-title>
91
+ <v-expansion-panel-text :style="rechercheAvanceeStyle">
92
+ <slot name="rechercheAvancee"></slot>
93
+ </v-expansion-panel-text>
94
+ </v-expansion-panel>
95
+ </v-expansion-panels>
96
+ </v-col>
97
+ </v-row>
98
+ </div>
99
+ </template>
100
+
101
+ <script lang="ts" setup>
102
+ import { ref, watch } from 'vue'
103
+
104
+ // Props
105
+ const props = defineProps({
106
+ afficher: {
107
+ type: Boolean,
108
+ required: false,
109
+ default: true,
110
+ },
111
+ rechercheTexte: {
112
+ type: String,
113
+ required: false,
114
+ default: '',
115
+ },
116
+ rechercheAvanceeTexte: {
117
+ type: String,
118
+ required: false,
119
+ default: '',
120
+ },
121
+ rechercheAvanceeLargeur: {
122
+ type: Number,
123
+ required: false,
124
+ default: 12,
125
+ },
126
+ chargement: {
127
+ type: Boolean,
128
+ required: false,
129
+ default: false,
130
+ },
131
+ desactiver: {
132
+ type: Boolean,
133
+ required: false,
134
+ default: false,
135
+ },
136
+ recherche: {
137
+ type: String,
138
+ required: false,
139
+ default: '',
140
+ },
141
+ rechercheAvancee: {
142
+ type: Boolean,
143
+ required: false,
144
+ default: false,
145
+ },
146
+ rechercheAvanceeStyle: {
147
+ type: String,
148
+ required: false,
149
+ default: '',
150
+ },
151
+ })
152
+
153
+ const emit = defineEmits<{
154
+ (e: 'update:recherche', recherche: string): void
155
+ (e: 'panneau:etat', isOpen: boolean): void // événement pour l'ouverture/fermeture du panneau
156
+ }>()
157
+
158
+ // Création d'une variable locale pour gérer la recherche
159
+ const rechercheLocale = ref(props.recherche)
160
+
161
+ // Surveiller les changements de la prop `recherche` et mettre à jour `rechercheLocale`
162
+ watch(
163
+ () => props.recherche,
164
+ nouvelleValeur => {
165
+ rechercheLocale.value = nouvelleValeur
166
+ },
167
+ )
168
+
169
+ // Émettre l'événement `update:recherche` chaque fois que l'utilisateur tape
170
+ watch(rechercheLocale, nouvelleValeur => {
171
+ emit('update:recherche', nouvelleValeur!)
172
+ })
173
+
174
+ // Fonction pour émettre l'événement d'ouverture/fermeture du panneau
175
+ const onPanelChange = (val: unknown) => {
176
+ if (typeof val === 'string' || typeof val === 'number' || val === null) {
177
+ const isOpen = val !== null && val !== ''
178
+ emit('panneau:etat', isOpen)
179
+ } else {
180
+ emit('panneau:etat', false)
181
+ }
182
+ }
183
+ </script>
184
+
185
+ <style lang="css">
186
+ /* barre de recherche design QC*/
187
+ .BarreRecherche {
188
+ min-height: 40px;
189
+ }
190
+
191
+ .BarreRechercheBackIcone {
192
+ border-right: 1px solid #808a9d;
193
+ border-top: 1px solid #808a9d;
194
+ border-bottom: 1px solid #808a9d;
195
+ background-color: #095797 !important;
196
+ height: 40px !important;
197
+ width: 40px !important;
198
+ max-width: 40px !important;
199
+ min-width: 40px !important;
200
+ padding-right: 0 !important;
201
+ padding-left: 0 !important;
202
+ border-radius: 0;
203
+ margin-left: -16px;
204
+ }
205
+
206
+ /* icone loupe */
207
+ .BarreRechercheIcone {
208
+ font-size: 34px !important;
209
+ margin-left: 1px !important;
210
+ margin-top: 2px !important;
211
+ color: white !important;
212
+ }
213
+ </style>
@@ -0,0 +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>
@@ -0,0 +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>