codevdesign 1.0.70 → 1.0.72

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,671 +1,672 @@
1
- <template>
2
- <div>
3
- <v-row v-if="barreHautAfficher">
4
- <slot name="ligne" />
5
- <!-- Affichage de la boite de recherche-->
6
- <v-col>
7
- <slot name="recherche"></slot>
8
- <Recherche
9
- v-bind="$attrs"
10
- :afficher="rechercheAfficher"
11
- :recherche-texte="rechercheTexte"
12
- :chargement="chargementListe"
13
- :recherche="recherche"
14
- :recherche-avancee="rechercheAvancee"
15
- :recherche-avancee-texte="rechercheAvanceeTexte"
16
- :recherche-avancee-largeur="rechercheAvanceeLargeur"
17
- :recherche-avancee-style="rechercheAvanceeStyle"
18
- class="flex-grow-1"
19
- @update:recherche="chargerRecherche"
20
- @panneau:etat="onPanelChange"
21
- >
22
- <template #milieu>
23
- <slot name="milieu" />
24
- </template>
25
-
26
- <template #droite>
27
- <slot name="droite" />
28
- <v-btn
29
- v-if="btnAjouter"
30
- color="primary"
31
- class="ml-1 float-right"
32
- @click.stop="ajouter"
33
- >
34
- {{ props.btnAjouterTexte ? props.btnAjouterTexte : $t('csqc.bouton.ajouter') }}
35
- </v-btn>
36
-
37
- <exportExcelComponent
38
- v-if="excel"
39
- :liste="filteredItems"
40
- :chargement-liste="chargementListe"
41
- :nom-fichier="excelNomFichier"
42
- class="mt-1 ml-1 float-right"
43
- />
44
- <v-icon
45
- v-if="permettreChoixColonnes"
46
- color="grisMoyen"
47
- class="mt-1 ml-1 float-right"
48
- @click.stop="ouvrirChoixColonnes"
49
- >
50
- mdi-table-edit
51
- </v-icon>
52
- </template>
53
- <template #rechercheAvanceeTitre>
54
- <slot name="rechercheAvanceeTitre" />
55
- </template>
56
- <template #rechercheAvanceeApresTitre>
57
- <slot name="rechercheAvanceeApresTitre" />
58
- </template>
59
- <template #rechercheAvancee>
60
- <slot name="rechercheAvancee" />
61
- </template>
62
- </Recherche>
63
- </v-col>
64
- </v-row>
65
-
66
- <!-- datatable-->
67
- <v-row>
68
- <v-col
69
- cols="12"
70
- class="d-flex ControlesDatatable flex-wrap"
71
- >
72
- <v-data-table
73
- ref="datatable"
74
- v-bind="$attrs"
75
- :headers="colonnesAffichees"
76
- :item-key="itemKey"
77
- :items="listeEffective"
78
- :search="recherche"
79
- :loading="chargementListe"
80
- :hover="hover"
81
- :density="densite"
82
- :items-per-page="itemsParPage"
83
- :item-per-page-options="itemsParPageOptions"
84
- :sort-by="triDepart"
85
- :multi-sort="estMultiTriActif"
86
- @click:row="cliqueLigne"
87
- >
88
- <!-- utilisation des slots via le component parent-->
89
- <!-- eslint-disable-next-line -->
90
- <template
91
- v-for="(_, slot) of $slots"
92
- :key="slot"
93
- #[slot]="scope"
94
- >
95
- <slot
96
- :name="slot"
97
- v-bind="scope"
98
- />
99
- </template>
100
-
101
- <!-- Icône ancre (drag & drop) -->
102
- <template
103
- v-if="estEnModeModificateurOrdre"
104
- #item.ancre
105
- >
106
- <v-icon class="ancre-drag">mdi-drag-vertical</v-icon>
107
- </template>
108
-
109
- <!-- boutons supprimer et modifier -->
110
- <!-- eslint-disable-next-line -->
111
- <template v-slot:item.action="{ item }">
112
- <slot name="actionsCustom"></slot>
113
- <v-icon
114
- v-if="props.btnSupprimer"
115
- size="large"
116
- class="iconeSupprimer float-right"
117
- @click.stop.prevent="ouvrirModaleSupprimer(item)"
118
- >
119
- mdi-delete
120
- </v-icon>
121
-
122
- <v-icon
123
- v-if="props.btnModifier"
124
- size="large"
125
- class="iconeEditer float-right"
126
- @click.stop.prevent="modifier(item)"
127
- >
128
- mdi-pencil
129
- </v-icon>
130
- </template>
131
- </v-data-table>
132
-
133
- <!-- Fenêtre de suppression -->
134
- <confirmation
135
- v-if="props.btnSupprimer"
136
- ref="modaleSupprimer"
137
- :texte="supprimerTexte"
138
- :titre="supprimerTitreTexte"
139
- :largeur="modaleSupprimerLargeur"
140
- @confirmer="supprimer"
141
- />
142
- <modale-choix
143
- ref="modaleChoix"
144
- v-if="permettreChoixColonnes"
145
- :urlbase="urlbase"
146
- :formulaire-id="formulaireId"
147
- :identifiant="identifiant"
148
- :colonnes="colonnesPourChoix"
149
- :choix-origine="choixOrigineNormalise"
150
- @selection="selectionChoix"
151
- @sauvegarde="sauvegardeChoix"
152
- />
153
- </v-col>
154
- </v-row>
155
- </div>
156
- </template>
157
- <script setup lang="ts">
158
- /* eslint-disable @typescript-eslint/no-explicit-any */
159
- import { computed, onBeforeUnmount, nextTick, onMounted, ref, watch, type PropType, type Slots } from 'vue'
160
- import { useI18n } from 'vue-i18n'
161
- import type { SortItem } from 'vuetify/lib/components/VDataTable/composables/sort.mjs'
162
-
163
- import Recherche from '../csqcRecherche.vue'
164
- import confirmation from '../csqcConfirmation.vue'
165
- import exportExcelComponent from './csqcTableExportExcel.vue'
166
- import ModaleChoix from './csqcTableModaleChoixColonnes.vue'
167
- import axios from '../../outils/appAxios'
168
- import Colonne from '../../modeles/composants/datatableColonne'
169
- import Sortable from 'sortablejs'
170
-
171
- type VueColonnes = {
172
- nomVue: string
173
- colonnes: string[]
174
- defaut?: boolean
175
- }
176
-
177
- const slots = defineSlots<Slots>()
178
- const emit = defineEmits([
179
- 'ajouter',
180
- 'cliqueLigne',
181
- 'supprimer',
182
- 'modifier',
183
- 'donneesExportees',
184
- 'panneau:etat',
185
- 'ordreRangeeModifie',
186
- ])
187
-
188
- // ============================================================================
189
- // Props
190
- // ============================================================================
191
- const props = defineProps({
192
- barreHautAfficher: { type: Boolean, default: true },
193
-
194
- btnAjouter: { type: Boolean, default: true },
195
- btnAjouterTexte: { type: String, default: '' },
196
- btnModifier: { type: Boolean, default: true },
197
- btnSupprimer: { type: Boolean, default: true },
198
-
199
- chargementListe: { type: Boolean, default: false },
200
- operationEnCours: { type: Boolean, default: false },
201
-
202
- // Headers Vuetify
203
- colonnes: {
204
- type: Array as PropType<Colonne[]>,
205
- default: () => [],
206
- },
207
-
208
- densite: { type: String as PropType<'default' | 'comfortable' | 'compact'>, default: 'default' },
209
-
210
- excel: { type: Boolean, default: false },
211
- excelNomFichier: { type: String, default: 'csqc' },
212
-
213
- // Choix colonnes
214
- formulaireId: { type: Number, default: -1 },
215
- identifiant: { type: String, default: '' },
216
- urlbase: { type: String, default: '' },
217
- permettreChoixColonnes: { type: Boolean, default: false },
218
-
219
- // Table
220
- itemKey: { type: String, default: 'id' },
221
- itemsParPage: { type: Number, default: 10 },
222
- itemsParPageOptions: { type: Array, default: () => [10, 25, 30, -1] },
223
- liste: {
224
- type: Array as PropType<Record<string, any>[]>,
225
- default: () => [],
226
- },
227
- hover: { type: Boolean, default: false },
228
-
229
- // Recherche
230
- rechercheTexte: { type: String, default: '' },
231
- rechercheAfficher: { type: Boolean, default: true },
232
- rechercheAvancee: { type: Boolean, default: false },
233
- rechercheAvanceeTexte: { type: String, default: '' },
234
- rechercheAvanceeLargeur: { type: Number, default: 12 },
235
- rechercheAvanceeStyle: { type: String, default: '' },
236
-
237
- // Modale suppression
238
- modaleSupprimerChamp: { type: String, default: '' },
239
- modaleSupprimerTexte: { type: String, default: '' },
240
- modaleSupprimerTitre: { type: String, default: '' },
241
- modaleSupprimerLargeur: { type: String, default: '525px' },
242
-
243
- // Tri
244
- triDescDepart: { type: Boolean, default: false },
245
- triParDepart: {
246
- type: [Array, String] as PropType<string | string[] | SortItem[] | undefined>,
247
- default: undefined,
248
- },
249
-
250
- // Ordre manuel (drag & drop)
251
- modificateurOrdreChamp: { type: String, default: undefined },
252
- })
253
-
254
- const { t } = useI18n({ useScope: 'global' })
255
- const recherche = ref<string>('')
256
-
257
- // Modale suppression (on garde l’item sélectionné)
258
- const itemSelectionne = ref<any>(null)
259
-
260
- // ============================================================================
261
- // State - Choix de colonnes (vues sauvegardées) + sélection courante
262
- // ============================================================================
263
- const datatable = ref<any>(null)
264
- const listeInterne = ref<Record<string, any>[]>([])
265
- const modaleChoix = ref<InstanceType<typeof ModaleChoix> | null>(null)
266
- const openChoixColonnes = ref(false)
267
- const choix = ref<VueColonnes[]>([]) // la liste des vues disponibles
268
- const vueActive = ref<VueColonnes | null>(null) // optionnel: la vue "appliquée"
269
- const valeurSelectionChoixColonnes = ref<string | null>(null) // nomVue sélectionné dans la modale / UI
270
-
271
- // ============================================================================
272
- // Charger le choix de colonnes sauvegardé s'il y a lieu
273
- // ============================================================================
274
- onMounted(async () => {
275
- if (!props.permettreChoixColonnes) return
276
-
277
- try {
278
- const res: any = await axios
279
- .getAxios()
280
- .get(`${props.urlbase}/api/ComposantUI/Colonnes/${props.formulaireId}/Identifiant/${props.identifiant}`)
281
-
282
- const payload = res?.data ?? res
283
-
284
- if (!payload) {
285
- choix.value = []
286
- return
287
- }
288
-
289
- choix.value = typeof payload === 'string' ? JSON.parse(payload) : payload
290
- } catch (e) {
291
- choix.value = []
292
- // console.debug(e)
293
- }
294
- })
295
-
296
- // ============================================================================
297
- // Computed - Normaliser pour la modale Choix colonne
298
- // ============================================================================
299
- const choixOrigineNormalise = computed(() =>
300
- (choix.value ?? []).map(c => ({
301
- _id: '',
302
- nomVue: c.nomVue,
303
- colonnes: c.colonnes ?? [],
304
- defaut: c.defaut ?? false,
305
- })),
306
- )
307
-
308
- // ============================================================================
309
- // Actions - Modale choix colonnes
310
- // ============================================================================
311
- function ouvrirChoixColonnes() {
312
- modaleChoix.value?.ouvrir()
313
- }
314
-
315
- function appliquerVue(vue: VueColonnes) {
316
- vueActive.value = vue
317
- openChoixColonnes.value = false
318
- }
319
-
320
- function selectionChoix(vue: VueColonnes) {
321
- // on garde une trace du nom sélectionné (pour retrouver la vue plus tard)
322
- valeurSelectionChoixColonnes.value = vue.nomVue
323
- appliquerVue(vue)
324
- }
325
-
326
- function sauvegardeChoix(vues: VueColonnes[]) {
327
- if (!props.permettreChoixColonnes) return
328
-
329
- // si la sélection courante n’existe plus, on reset
330
- if (
331
- valeurSelectionChoixColonnes.value != null &&
332
- vues.find(x => x.nomVue === valeurSelectionChoixColonnes.value) == null
333
- ) {
334
- valeurSelectionChoixColonnes.value = null
335
- }
336
-
337
- choix.value = vues
338
-
339
- // optionnel: appliquer automatiquement la vue par défaut
340
- const defaut = vues.find(v => v.defaut)
341
- if (defaut) appliquerVue(defaut)
342
- }
343
-
344
- // ============================================================================
345
- // Computed - Mode ordre manuel
346
- // ============================================================================
347
- const estEnModeModificateurOrdre = computed(() => {
348
- if (!props.modificateurOrdreChamp) return false
349
-
350
- if (props.liste == null) return false
351
-
352
- // Vérifie que le champ existe dans les données (pas nécessairement une colonne visible)
353
- if (!props.liste.length) return false // liste vide, on attend sans avertissement
354
-
355
- const premierItem = props.liste[0]
356
-
357
- if (!(props.modificateurOrdreChamp in premierItem)) {
358
- console.log(
359
- `[csqcTable] modificateurOrdreChamp="${props.modificateurOrdreChamp}" n'existe pas dans les données. Le mode d'ordonnancement est désactivé.`,
360
- )
361
- return false
362
- }
363
-
364
- return true
365
- })
366
-
367
- // ============================================================================
368
- // Computed - Colonnes à envoyer à la modale (liste ordonnée + libellé)
369
- // ============================================================================
370
- const colonnesPourChoix = computed(() =>
371
- props.colonnes
372
- // on exclut l'action (souvent toujours visible) + colonnes "cachées"
373
- .filter(c => c.key && c.key !== 'action' && (c as any).align !== 'd-none')
374
- .map((c, idx) => ({
375
- title: (c as any).title ?? (c as any).text ?? String((c as any).key ?? (c as any).value),
376
- value: String((c as any).key ?? (c as any).value), // ← on force string
377
- ordre: idx + 1,
378
- })),
379
- )
380
-
381
- // ============================================================================
382
- // Computed - Headers réellement affichés par la table (selon le choix actif)
383
- // ============================================================================
384
- const colonnesAffichees = computed(() => {
385
- // 1) colonnes "disponibles" (on enlève celles explicitement cachées)
386
- const colonnesFiltre = props.colonnes.filter(c => (c as any).align !== 'd-none')
387
-
388
- // 2) résoudre la liste selon les vues sauvegardées (choix colonnes)
389
- let colonnesBase: Colonne[]
390
- if (!props.permettreChoixColonnes || !choix.value?.length) {
391
- colonnesBase = colonnesFiltre
392
- } else {
393
- // retrouver la vue sélectionnée (sinon la défaut)
394
- const choixSelection = choix.value.find(
395
- x => valeurSelectionChoixColonnes.value === x.nomVue || (!valeurSelectionChoixColonnes.value && x.defaut),
396
- )
397
- colonnesBase = choixSelection
398
- ? (choixSelection.colonnes.map(k => colonnesFiltre.find(c => String(c.key) === k)).filter(Boolean) as Colonne[])
399
- : colonnesFiltre
400
- }
401
-
402
- // 3) ordre manuel actif : colonne ancre en tête + toutes les colonnes non triables
403
- if (estEnModeModificateurOrdre.value) {
404
- const colonneAncre = { key: 'ancre', title: '', sortable: false, width: '40px' } as Colonne
405
- return [colonneAncre, ...colonnesBase.map(c => ({ ...c, sortable: false }))]
406
- }
407
-
408
- return colonnesBase
409
- })
410
-
411
- // ============================================================================
412
- // Computed - Tri initial (Vuetify sort-by)
413
- // ============================================================================
414
- const triDepart = computed<SortItem[] | undefined>(() => {
415
- // ordre manuel : tri forcé sur le champ ordre, croissant
416
- if (estEnModeModificateurOrdre.value) return [{ key: props.modificateurOrdreChamp!, order: 'asc' }]
417
-
418
- if (props.triParDepart == null) return undefined
419
-
420
- const ordre = props.triDescDepart ? 'desc' : 'asc'
421
- if (typeof props.triParDepart === 'string') return [{ key: props.triParDepart, order: ordre }]
422
-
423
- const retour: SortItem[] = []
424
- for (let i = 0; i < props.triParDepart.length; i += 1) {
425
- const tri = props.triParDepart[i]!
426
- if (typeof tri === 'string') retour.push({ key: tri, order: ordre })
427
- else retour.push(tri)
428
- }
429
- return retour
430
- })
431
-
432
- const estMultiTriActif = computed(() => {
433
- if (estEnModeModificateurOrdre.value) return false
434
- if (props.triParDepart == null) return false
435
- if (typeof props.triParDepart === 'string') return false
436
- return true
437
- })
438
-
439
- // ============================================================================
440
- // Computed - Export (filtrage local basé sur recherche)
441
- // ============================================================================
442
- const filteredItems = computed(() => {
443
- if (!recherche.value) return props.liste
444
- const q = recherche.value.toLowerCase()
445
- return props.liste.filter(item => Object.values(item).some(val => String(val).toLowerCase().includes(q)))
446
- })
447
-
448
- const listeEffective = computed(() => (estEnModeModificateurOrdre.value ? listeInterne.value : props.liste))
449
-
450
- // ============================================================================
451
- // Drag & drop - Ordre manuel (Sortable.js)
452
- // ============================================================================
453
- let sortableInstance: Sortable | null = null
454
-
455
- // Synchronise listeInterne quand le parent met à jour props.liste
456
- watch(
457
- () => props.liste,
458
- val => {
459
- listeInterne.value = [...val]
460
- },
461
- { immediate: true, deep: true },
462
- )
463
-
464
- // Init/destroy Sortable si la prop change en cours de vie du composant
465
- watch(estEnModeModificateurOrdre, actif => {
466
- if (actif) nextTick(initSortable)
467
- else destroySortable()
468
- })
469
-
470
- onMounted(() => {
471
- if (estEnModeModificateurOrdre.value) nextTick(initSortable)
472
- })
473
-
474
- onBeforeUnmount(() => {
475
- destroySortable()
476
- })
477
-
478
- function initSortable() {
479
- const tableEl = datatable.value?.$el as HTMLElement | undefined
480
- if (!tableEl) return
481
- const tbody = tableEl.querySelector('tbody')
482
- if (!tbody) return
483
-
484
- destroySortable() // garantit une seule instance active
485
-
486
- sortableInstance = Sortable.create(tbody, {
487
- animation: 150,
488
- handle: '.ancre-drag',
489
- onEnd(event) {
490
- const oldIndex = event.oldIndex
491
- const newIndex = event.newIndex
492
- if (oldIndex == null || newIndex == null || oldIndex === newIndex) return
493
-
494
- const items = [...listeInterne.value]
495
- const moved = items.splice(oldIndex, 1)[0]
496
- items.splice(newIndex, 0, moved)
497
-
498
- // Recalcule la valeur du champ ordre pour maintenir la cohérence
499
- const champ = props.modificateurOrdreChamp!
500
- items.forEach((item, idx) => {
501
- item[champ] = idx + 1
502
- })
503
-
504
- listeInterne.value = items
505
- emit('ordreRangeeModifie', [...items])
506
- },
507
- })
508
- }
509
-
510
- function destroySortable() {
511
- sortableInstance?.destroy()
512
- sortableInstance = null
513
- }
514
-
515
- // ============================================================================
516
- // UI Actions - Recherche / Table events
517
- // ============================================================================
518
- function chargerRecherche(val: string) {
519
- recherche.value = val
520
- }
521
-
522
- function ajouter() {
523
- emit('ajouter')
524
- }
525
-
526
- function cliqueLigne(_e: Event, { item }: { item: any }) {
527
- emit('cliqueLigne', item)
528
- }
529
-
530
- function modifier(item: any) {
531
- emit('modifier', item)
532
- }
533
-
534
- // ============================================================================
535
- // Suppression - Texte + actions
536
- // ============================================================================
537
- const supprimerTexte = computed(() => {
538
- if (itemSelectionne.value == null) return ''
539
-
540
- if (props.modaleSupprimerTexte) return props.modaleSupprimerTexte
541
-
542
- return t('csqc.message.supprimerMessage', {
543
- nom: itemSelectionne?.value?.[props.modaleSupprimerChamp] ?? '',
544
- })
545
- })
546
-
547
- const supprimerTitreTexte = computed(() => props.modaleSupprimerTitre || t('csqc.message.supprimerTitre'))
548
-
549
- function ouvrirModaleSupprimer(item: any) {
550
- itemSelectionne.value = item
551
- emit('supprimer', itemSelectionne.value) // tu sembles gérer la modale ailleurs
552
- }
553
-
554
- function supprimer() {
555
- emit('supprimer', itemSelectionne.value)
556
- }
557
-
558
- // ============================================================================
559
- // UI - Panneau recherche avancée
560
- // ============================================================================
561
- function onPanelChange(val: boolean) {
562
- emit('panneau:etat', val)
563
- }
564
- </script>
565
-
566
- <style scoped lang="css">
567
- .ControlesDatatable {
568
- gap: 4px;
569
- }
570
-
571
- /* barre de recherche design QC*/
572
- .BarreRecherche {
573
- border-radius: 4px 0 0 4px !important;
574
- min-height: 40px;
575
- }
576
-
577
- .BarreRechercheBackIcone {
578
- border-right: 1px solid #808a9d;
579
- border-top: 1px solid #808a9d;
580
- border-bottom: 1px solid #808a9d;
581
- background-color: #095797 !important;
582
- height: 40px !important;
583
- width: 40px !important;
584
- max-width: 40px !important;
585
- min-width: 40px !important;
586
- padding-right: 0 !important;
587
- padding-left: 0 !important;
588
- border-radius: 0 4px 4px 0 !important;
589
- margin-left: -16px;
590
- }
591
-
592
- /* icone loupe */
593
- .BarreRechercheIcone {
594
- font-size: 34px !important;
595
- margin-left: 1px !important;
596
- margin-top: 2px !important;
597
- color: white !important;
598
- }
599
-
600
- /* datatable contour */
601
- .v-data-table {
602
- border: 1px solid #d3d3d3;
603
- border-radius: 5px;
604
- overflow: hidden;
605
- }
606
-
607
- /* hover */
608
- .v-data-table:hover {
609
- cursor: pointer !important;
610
- }
611
-
612
- /* datatable row */
613
- .v-data-table .v-table__wrapper > table > thead > tr > td,
614
- .v-data-table .v-table__wrapper > table > thead > tr th,
615
- .v-data-table .v-table__wrapper > table tbody > tr > td,
616
- .v-data-table .v-table__wrapper > table tbody > tr th {
617
- background-color: #d3d3d375 !important;
618
- }
619
-
620
- /* datatable header contour */
621
- .v-data-table .v-table__wrapper > table > thead > tr > th,
622
- .v-data-table .v-table__wrapper > table tbody > tr > th {
623
- background-color: white !important;
624
- border-bottom: 4px #223654 solid !important;
625
- }
626
-
627
- /* datatable header intérieur*/
628
- .v-data-table-header__content {
629
- background-color: white !important;
630
- color: #223654 !important;
631
- font-weight: 600;
632
- font-size: 15px;
633
- height: 46px;
634
- }
635
-
636
- /* datatable footer */
637
- .v-data-table-footer {
638
- background-color: white !important;
639
- border-top: 4px #223654 solid !important;
640
- }
641
-
642
- /* datatable row hover */
643
- .v-data-table .v-table__wrapper > table tbody > tr:hover > td,
644
- .v-data-table .v-table__wrapper > table tbody > tr:hover {
645
- background-color: #e0e0e0 !important;
646
- }
647
-
648
- .iconeSupprimer:hover {
649
- color: red;
650
- }
651
- .v-icon.v-icon.v-icon--link.iconeSupprimer:hover {
652
- color: red !important;
653
- }
654
- .iconeEditer:hover {
655
- color: #095797;
656
- }
657
-
658
- /* hover icone à droite du ajouter*/
659
- .text-grisMoyen:hover {
660
- color: #095797 !important;
661
- }
662
-
663
- /* icône d'ancre drag & drop */
664
- .ancre-drag {
665
- cursor: grab;
666
- color: #808a9d;
667
- }
668
- .ancre-drag:active {
669
- cursor: grabbing;
670
- }
671
- </style>
1
+ <template>
2
+ <div>
3
+ <v-row v-if="barreHautAfficher">
4
+ <slot name="ligne" />
5
+ <!-- Affichage de la boite de recherche-->
6
+ <v-col>
7
+ <slot name="recherche"></slot>
8
+ <Recherche
9
+ v-bind="$attrs"
10
+ :afficher="rechercheAfficher"
11
+ :recherche-texte="rechercheTexte"
12
+ :chargement="chargementListe"
13
+ :recherche="recherche"
14
+ :recherche-avancee="rechercheAvancee"
15
+ :recherche-avancee-texte="rechercheAvanceeTexte"
16
+ :recherche-avancee-largeur="rechercheAvanceeLargeur"
17
+ :recherche-avancee-style="rechercheAvanceeStyle"
18
+ class="flex-grow-1"
19
+ @update:recherche="chargerRecherche"
20
+ @panneau:etat="onPanelChange"
21
+ >
22
+ <template #milieu>
23
+ <slot name="milieu" />
24
+ </template>
25
+
26
+ <template #droite>
27
+ <slot name="droite" />
28
+ <v-btn
29
+ v-if="btnAjouter"
30
+ color="primary"
31
+ class="ml-1 float-right"
32
+ @click.stop="ajouter"
33
+ >
34
+ {{ props.btnAjouterTexte ? props.btnAjouterTexte : $t('csqc.bouton.ajouter') }}
35
+ </v-btn>
36
+
37
+ <exportExcelComponent
38
+ v-if="excel"
39
+ :liste="filteredItems"
40
+ :chargement-liste="chargementListe"
41
+ :nom-fichier="excelNomFichier"
42
+ :colonnes="colonnesAffichees"
43
+ class="mt-1 ml-1 float-right"
44
+ />
45
+ <v-icon
46
+ v-if="permettreChoixColonnes"
47
+ color="grisMoyen"
48
+ class="mt-1 ml-1 float-right"
49
+ @click.stop="ouvrirChoixColonnes"
50
+ >
51
+ mdi-table-edit
52
+ </v-icon>
53
+ </template>
54
+ <template #rechercheAvanceeTitre>
55
+ <slot name="rechercheAvanceeTitre" />
56
+ </template>
57
+ <template #rechercheAvanceeApresTitre>
58
+ <slot name="rechercheAvanceeApresTitre" />
59
+ </template>
60
+ <template #rechercheAvancee>
61
+ <slot name="rechercheAvancee" />
62
+ </template>
63
+ </Recherche>
64
+ </v-col>
65
+ </v-row>
66
+
67
+ <!-- datatable-->
68
+ <v-row>
69
+ <v-col
70
+ cols="12"
71
+ class="d-flex ControlesDatatable flex-wrap"
72
+ >
73
+ <v-data-table
74
+ ref="datatable"
75
+ v-bind="$attrs"
76
+ :headers="colonnesAffichees"
77
+ :item-key="itemKey"
78
+ :items="listeEffective"
79
+ :search="recherche"
80
+ :loading="chargementListe"
81
+ :hover="hover"
82
+ :density="densite"
83
+ :items-per-page="itemsParPage"
84
+ :item-per-page-options="itemsParPageOptions"
85
+ :sort-by="triDepart"
86
+ :multi-sort="estMultiTriActif"
87
+ @click:row="cliqueLigne"
88
+ >
89
+ <!-- utilisation des slots via le component parent-->
90
+ <!-- eslint-disable-next-line -->
91
+ <template
92
+ v-for="(_, slot) of $slots"
93
+ :key="slot"
94
+ #[slot]="scope"
95
+ >
96
+ <slot
97
+ :name="slot"
98
+ v-bind="scope"
99
+ />
100
+ </template>
101
+
102
+ <!-- Icône ancre (drag & drop) -->
103
+ <template
104
+ v-if="estEnModeModificateurOrdre"
105
+ #item.ancre
106
+ >
107
+ <v-icon class="ancre-drag">mdi-drag-vertical</v-icon>
108
+ </template>
109
+
110
+ <!-- boutons supprimer et modifier -->
111
+ <!-- eslint-disable-next-line -->
112
+ <template v-slot:item.action="{ item }">
113
+ <slot name="actionsCustom"></slot>
114
+ <v-icon
115
+ v-if="props.btnSupprimer"
116
+ size="large"
117
+ class="iconeSupprimer float-right"
118
+ @click.stop.prevent="ouvrirModaleSupprimer(item)"
119
+ >
120
+ mdi-delete
121
+ </v-icon>
122
+
123
+ <v-icon
124
+ v-if="props.btnModifier"
125
+ size="large"
126
+ class="iconeEditer float-right"
127
+ @click.stop.prevent="modifier(item)"
128
+ >
129
+ mdi-pencil
130
+ </v-icon>
131
+ </template>
132
+ </v-data-table>
133
+
134
+ <!-- Fenêtre de suppression -->
135
+ <confirmation
136
+ v-if="props.btnSupprimer"
137
+ ref="modaleSupprimer"
138
+ :texte="supprimerTexte"
139
+ :titre="supprimerTitreTexte"
140
+ :largeur="modaleSupprimerLargeur"
141
+ @confirmer="supprimer"
142
+ />
143
+ <modale-choix
144
+ ref="modaleChoix"
145
+ v-if="permettreChoixColonnes"
146
+ :urlbase="urlbase"
147
+ :formulaire-id="formulaireId"
148
+ :identifiant="identifiant"
149
+ :colonnes="colonnesPourChoix"
150
+ :choix-origine="choixOrigineNormalise"
151
+ @selection="selectionChoix"
152
+ @sauvegarde="sauvegardeChoix"
153
+ />
154
+ </v-col>
155
+ </v-row>
156
+ </div>
157
+ </template>
158
+ <script setup lang="ts">
159
+ /* eslint-disable @typescript-eslint/no-explicit-any */
160
+ import { computed, onBeforeUnmount, nextTick, onMounted, ref, watch, type PropType, type Slots } from 'vue'
161
+ import { useI18n } from 'vue-i18n'
162
+ import type { SortItem } from 'vuetify/lib/components/VDataTable/composables/sort.mjs'
163
+
164
+ import Recherche from '../csqcRecherche.vue'
165
+ import confirmation from '../csqcConfirmation.vue'
166
+ import exportExcelComponent from './csqcTableExportExcel.vue'
167
+ import ModaleChoix from './csqcTableModaleChoixColonnes.vue'
168
+ import axios from '../../outils/appAxios'
169
+ import Colonne from '../../modeles/composants/datatableColonne'
170
+ import Sortable from 'sortablejs'
171
+
172
+ type VueColonnes = {
173
+ nomVue: string
174
+ colonnes: string[]
175
+ defaut?: boolean
176
+ }
177
+
178
+ const slots = defineSlots<Slots>()
179
+ const emit = defineEmits([
180
+ 'ajouter',
181
+ 'cliqueLigne',
182
+ 'supprimer',
183
+ 'modifier',
184
+ 'donneesExportees',
185
+ 'panneau:etat',
186
+ 'ordreRangeeModifie',
187
+ ])
188
+
189
+ // ============================================================================
190
+ // Props
191
+ // ============================================================================
192
+ const props = defineProps({
193
+ barreHautAfficher: { type: Boolean, default: true },
194
+
195
+ btnAjouter: { type: Boolean, default: true },
196
+ btnAjouterTexte: { type: String, default: '' },
197
+ btnModifier: { type: Boolean, default: true },
198
+ btnSupprimer: { type: Boolean, default: true },
199
+
200
+ chargementListe: { type: Boolean, default: false },
201
+ operationEnCours: { type: Boolean, default: false },
202
+
203
+ // Headers Vuetify
204
+ colonnes: {
205
+ type: Array as PropType<Colonne[]>,
206
+ default: () => [],
207
+ },
208
+
209
+ densite: { type: String as PropType<'default' | 'comfortable' | 'compact'>, default: 'default' },
210
+
211
+ excel: { type: Boolean, default: false },
212
+ excelNomFichier: { type: String, default: 'csqc' },
213
+
214
+ // Choix colonnes
215
+ formulaireId: { type: Number, default: -1 },
216
+ identifiant: { type: String, default: '' },
217
+ urlbase: { type: String, default: '' },
218
+ permettreChoixColonnes: { type: Boolean, default: false },
219
+
220
+ // Table
221
+ itemKey: { type: String, default: 'id' },
222
+ itemsParPage: { type: Number, default: 10 },
223
+ itemsParPageOptions: { type: Array, default: () => [10, 25, 30, -1] },
224
+ liste: {
225
+ type: Array as PropType<Record<string, any>[]>,
226
+ default: () => [],
227
+ },
228
+ hover: { type: Boolean, default: false },
229
+
230
+ // Recherche
231
+ rechercheTexte: { type: String, default: '' },
232
+ rechercheAfficher: { type: Boolean, default: true },
233
+ rechercheAvancee: { type: Boolean, default: false },
234
+ rechercheAvanceeTexte: { type: String, default: '' },
235
+ rechercheAvanceeLargeur: { type: Number, default: 12 },
236
+ rechercheAvanceeStyle: { type: String, default: '' },
237
+
238
+ // Modale suppression
239
+ modaleSupprimerChamp: { type: String, default: '' },
240
+ modaleSupprimerTexte: { type: String, default: '' },
241
+ modaleSupprimerTitre: { type: String, default: '' },
242
+ modaleSupprimerLargeur: { type: String, default: '525px' },
243
+
244
+ // Tri
245
+ triDescDepart: { type: Boolean, default: false },
246
+ triParDepart: {
247
+ type: [Array, String] as PropType<string | string[] | SortItem[] | undefined>,
248
+ default: undefined,
249
+ },
250
+
251
+ // Ordre manuel (drag & drop)
252
+ modificateurOrdreChamp: { type: String, default: undefined },
253
+ })
254
+
255
+ const { t } = useI18n({ useScope: 'global' })
256
+ const recherche = ref<string>('')
257
+
258
+ // Modale suppression (on garde l’item sélectionné)
259
+ const itemSelectionne = ref<any>(null)
260
+
261
+ // ============================================================================
262
+ // State - Choix de colonnes (vues sauvegardées) + sélection courante
263
+ // ============================================================================
264
+ const datatable = ref<any>(null)
265
+ const listeInterne = ref<Record<string, any>[]>([])
266
+ const modaleChoix = ref<InstanceType<typeof ModaleChoix> | null>(null)
267
+ const openChoixColonnes = ref(false)
268
+ const choix = ref<VueColonnes[]>([]) // la liste des vues disponibles
269
+ const vueActive = ref<VueColonnes | null>(null) // optionnel: la vue "appliquée"
270
+ const valeurSelectionChoixColonnes = ref<string | null>(null) // nomVue sélectionné dans la modale / UI
271
+
272
+ // ============================================================================
273
+ // Charger le choix de colonnes sauvegardé s'il y a lieu
274
+ // ============================================================================
275
+ onMounted(async () => {
276
+ if (!props.permettreChoixColonnes) return
277
+
278
+ try {
279
+ const res: any = await axios
280
+ .getAxios()
281
+ .get(`${props.urlbase}/api/ComposantUI/Colonnes/${props.formulaireId}/Identifiant/${props.identifiant}`)
282
+
283
+ const payload = res?.data ?? res
284
+
285
+ if (!payload) {
286
+ choix.value = []
287
+ return
288
+ }
289
+
290
+ choix.value = typeof payload === 'string' ? JSON.parse(payload) : payload
291
+ } catch (e) {
292
+ choix.value = []
293
+ // console.debug(e)
294
+ }
295
+ })
296
+
297
+ // ============================================================================
298
+ // Computed - Normaliser pour la modale Choix colonne
299
+ // ============================================================================
300
+ const choixOrigineNormalise = computed(() =>
301
+ (choix.value ?? []).map(c => ({
302
+ _id: '',
303
+ nomVue: c.nomVue,
304
+ colonnes: c.colonnes ?? [],
305
+ defaut: c.defaut ?? false,
306
+ })),
307
+ )
308
+
309
+ // ============================================================================
310
+ // Actions - Modale choix colonnes
311
+ // ============================================================================
312
+ function ouvrirChoixColonnes() {
313
+ modaleChoix.value?.ouvrir()
314
+ }
315
+
316
+ function appliquerVue(vue: VueColonnes) {
317
+ vueActive.value = vue
318
+ openChoixColonnes.value = false
319
+ }
320
+
321
+ function selectionChoix(vue: VueColonnes) {
322
+ // on garde une trace du nom sélectionné (pour retrouver la vue plus tard)
323
+ valeurSelectionChoixColonnes.value = vue.nomVue
324
+ appliquerVue(vue)
325
+ }
326
+
327
+ function sauvegardeChoix(vues: VueColonnes[]) {
328
+ if (!props.permettreChoixColonnes) return
329
+
330
+ // si la sélection courante n’existe plus, on reset
331
+ if (
332
+ valeurSelectionChoixColonnes.value != null &&
333
+ vues.find(x => x.nomVue === valeurSelectionChoixColonnes.value) == null
334
+ ) {
335
+ valeurSelectionChoixColonnes.value = null
336
+ }
337
+
338
+ choix.value = vues
339
+
340
+ // optionnel: appliquer automatiquement la vue par défaut
341
+ const defaut = vues.find(v => v.defaut)
342
+ if (defaut) appliquerVue(defaut)
343
+ }
344
+
345
+ // ============================================================================
346
+ // Computed - Mode ordre manuel
347
+ // ============================================================================
348
+ const estEnModeModificateurOrdre = computed(() => {
349
+ if (!props.modificateurOrdreChamp) return false
350
+
351
+ if (props.liste == null) return false
352
+
353
+ // Vérifie que le champ existe dans les données (pas nécessairement une colonne visible)
354
+ if (!props.liste.length) return false // liste vide, on attend sans avertissement
355
+
356
+ const premierItem = props.liste[0]
357
+
358
+ if (!(props.modificateurOrdreChamp in premierItem)) {
359
+ console.log(
360
+ `[csqcTable] modificateurOrdreChamp="${props.modificateurOrdreChamp}" n'existe pas dans les données. Le mode d'ordonnancement est désactivé.`,
361
+ )
362
+ return false
363
+ }
364
+
365
+ return true
366
+ })
367
+
368
+ // ============================================================================
369
+ // Computed - Colonnes à envoyer à la modale (liste ordonnée + libellé)
370
+ // ============================================================================
371
+ const colonnesPourChoix = computed(() =>
372
+ props.colonnes
373
+ // on exclut l'action (souvent toujours visible) + colonnes "cachées"
374
+ .filter(c => c.key && c.key !== 'action' && (c as any).align !== 'd-none')
375
+ .map((c, idx) => ({
376
+ title: (c as any).title ?? (c as any).text ?? String((c as any).key ?? (c as any).value),
377
+ value: String((c as any).key ?? (c as any).value), // ← on force string
378
+ ordre: idx + 1,
379
+ })),
380
+ )
381
+
382
+ // ============================================================================
383
+ // Computed - Headers réellement affichés par la table (selon le choix actif)
384
+ // ============================================================================
385
+ const colonnesAffichees = computed(() => {
386
+ // 1) colonnes "disponibles" (on enlève celles explicitement cachées)
387
+ const colonnesFiltre = props.colonnes.filter(c => (c as any).align !== 'd-none')
388
+
389
+ // 2) résoudre la liste selon les vues sauvegardées (choix colonnes)
390
+ let colonnesBase: Colonne[]
391
+ if (!props.permettreChoixColonnes || !choix.value?.length) {
392
+ colonnesBase = colonnesFiltre
393
+ } else {
394
+ // retrouver la vue sélectionnée (sinon la défaut)
395
+ const choixSelection = choix.value.find(
396
+ x => valeurSelectionChoixColonnes.value === x.nomVue || (!valeurSelectionChoixColonnes.value && x.defaut),
397
+ )
398
+ colonnesBase = choixSelection
399
+ ? (choixSelection.colonnes.map(k => colonnesFiltre.find(c => String(c.key) === k)).filter(Boolean) as Colonne[])
400
+ : colonnesFiltre
401
+ }
402
+
403
+ // 3) ordre manuel actif : colonne ancre en tête + toutes les colonnes non triables
404
+ if (estEnModeModificateurOrdre.value) {
405
+ const colonneAncre = { key: 'ancre', title: '', sortable: false, width: '40px' } as Colonne
406
+ return [colonneAncre, ...colonnesBase.map(c => ({ ...c, sortable: false }))]
407
+ }
408
+
409
+ return colonnesBase
410
+ })
411
+
412
+ // ============================================================================
413
+ // Computed - Tri initial (Vuetify sort-by)
414
+ // ============================================================================
415
+ const triDepart = computed<SortItem[] | undefined>(() => {
416
+ // ordre manuel : tri forcé sur le champ ordre, croissant
417
+ if (estEnModeModificateurOrdre.value) return [{ key: props.modificateurOrdreChamp!, order: 'asc' }]
418
+
419
+ if (props.triParDepart == null) return undefined
420
+
421
+ const ordre = props.triDescDepart ? 'desc' : 'asc'
422
+ if (typeof props.triParDepart === 'string') return [{ key: props.triParDepart, order: ordre }]
423
+
424
+ const retour: SortItem[] = []
425
+ for (let i = 0; i < props.triParDepart.length; i += 1) {
426
+ const tri = props.triParDepart[i]!
427
+ if (typeof tri === 'string') retour.push({ key: tri, order: ordre })
428
+ else retour.push(tri)
429
+ }
430
+ return retour
431
+ })
432
+
433
+ const estMultiTriActif = computed(() => {
434
+ if (estEnModeModificateurOrdre.value) return false
435
+ if (props.triParDepart == null) return false
436
+ if (typeof props.triParDepart === 'string') return false
437
+ return true
438
+ })
439
+
440
+ // ============================================================================
441
+ // Computed - Export (filtrage local basé sur recherche)
442
+ // ============================================================================
443
+ const filteredItems = computed(() => {
444
+ if (!recherche.value) return props.liste
445
+ const q = recherche.value.toLowerCase()
446
+ return props.liste.filter(item => Object.values(item).some(val => String(val).toLowerCase().includes(q)))
447
+ })
448
+
449
+ const listeEffective = computed(() => (estEnModeModificateurOrdre.value ? listeInterne.value : props.liste))
450
+
451
+ // ============================================================================
452
+ // Drag & drop - Ordre manuel (Sortable.js)
453
+ // ============================================================================
454
+ let sortableInstance: Sortable | null = null
455
+
456
+ // Synchronise listeInterne quand le parent met à jour props.liste
457
+ watch(
458
+ () => props.liste,
459
+ val => {
460
+ listeInterne.value = [...val]
461
+ },
462
+ { immediate: true, deep: true },
463
+ )
464
+
465
+ // Init/destroy Sortable si la prop change en cours de vie du composant
466
+ watch(estEnModeModificateurOrdre, actif => {
467
+ if (actif) nextTick(initSortable)
468
+ else destroySortable()
469
+ })
470
+
471
+ onMounted(() => {
472
+ if (estEnModeModificateurOrdre.value) nextTick(initSortable)
473
+ })
474
+
475
+ onBeforeUnmount(() => {
476
+ destroySortable()
477
+ })
478
+
479
+ function initSortable() {
480
+ const tableEl = datatable.value?.$el as HTMLElement | undefined
481
+ if (!tableEl) return
482
+ const tbody = tableEl.querySelector('tbody')
483
+ if (!tbody) return
484
+
485
+ destroySortable() // garantit une seule instance active
486
+
487
+ sortableInstance = Sortable.create(tbody, {
488
+ animation: 150,
489
+ handle: '.ancre-drag',
490
+ onEnd(event) {
491
+ const oldIndex = event.oldIndex
492
+ const newIndex = event.newIndex
493
+ if (oldIndex == null || newIndex == null || oldIndex === newIndex) return
494
+
495
+ const items = [...listeInterne.value]
496
+ const moved = items.splice(oldIndex, 1)[0]
497
+ items.splice(newIndex, 0, moved)
498
+
499
+ // Recalcule la valeur du champ ordre pour maintenir la cohérence
500
+ const champ = props.modificateurOrdreChamp!
501
+ items.forEach((item, idx) => {
502
+ item[champ] = idx + 1
503
+ })
504
+
505
+ listeInterne.value = items
506
+ emit('ordreRangeeModifie', [...items])
507
+ },
508
+ })
509
+ }
510
+
511
+ function destroySortable() {
512
+ sortableInstance?.destroy()
513
+ sortableInstance = null
514
+ }
515
+
516
+ // ============================================================================
517
+ // UI Actions - Recherche / Table events
518
+ // ============================================================================
519
+ function chargerRecherche(val: string) {
520
+ recherche.value = val
521
+ }
522
+
523
+ function ajouter() {
524
+ emit('ajouter')
525
+ }
526
+
527
+ function cliqueLigne(_e: Event, { item }: { item: any }) {
528
+ emit('cliqueLigne', item)
529
+ }
530
+
531
+ function modifier(item: any) {
532
+ emit('modifier', item)
533
+ }
534
+
535
+ // ============================================================================
536
+ // Suppression - Texte + actions
537
+ // ============================================================================
538
+ const supprimerTexte = computed(() => {
539
+ if (itemSelectionne.value == null) return ''
540
+
541
+ if (props.modaleSupprimerTexte) return props.modaleSupprimerTexte
542
+
543
+ return t('csqc.message.supprimerMessage', {
544
+ nom: itemSelectionne?.value?.[props.modaleSupprimerChamp] ?? '',
545
+ })
546
+ })
547
+
548
+ const supprimerTitreTexte = computed(() => props.modaleSupprimerTitre || t('csqc.message.supprimerTitre'))
549
+
550
+ function ouvrirModaleSupprimer(item: any) {
551
+ itemSelectionne.value = item
552
+ emit('supprimer', itemSelectionne.value) // tu sembles gérer la modale ailleurs
553
+ }
554
+
555
+ function supprimer() {
556
+ emit('supprimer', itemSelectionne.value)
557
+ }
558
+
559
+ // ============================================================================
560
+ // UI - Panneau recherche avancée
561
+ // ============================================================================
562
+ function onPanelChange(val: boolean) {
563
+ emit('panneau:etat', val)
564
+ }
565
+ </script>
566
+
567
+ <style scoped lang="css">
568
+ .ControlesDatatable {
569
+ gap: 4px;
570
+ }
571
+
572
+ /* barre de recherche design QC*/
573
+ .BarreRecherche {
574
+ border-radius: 4px 0 0 4px !important;
575
+ min-height: 40px;
576
+ }
577
+
578
+ .BarreRechercheBackIcone {
579
+ border-right: 1px solid #808a9d;
580
+ border-top: 1px solid #808a9d;
581
+ border-bottom: 1px solid #808a9d;
582
+ background-color: #095797 !important;
583
+ height: 40px !important;
584
+ width: 40px !important;
585
+ max-width: 40px !important;
586
+ min-width: 40px !important;
587
+ padding-right: 0 !important;
588
+ padding-left: 0 !important;
589
+ border-radius: 0 4px 4px 0 !important;
590
+ margin-left: -16px;
591
+ }
592
+
593
+ /* icone loupe */
594
+ .BarreRechercheIcone {
595
+ font-size: 34px !important;
596
+ margin-left: 1px !important;
597
+ margin-top: 2px !important;
598
+ color: white !important;
599
+ }
600
+
601
+ /* datatable contour */
602
+ .v-data-table {
603
+ border: 1px solid #d3d3d3;
604
+ border-radius: 5px;
605
+ overflow: hidden;
606
+ }
607
+
608
+ /* hover */
609
+ .v-data-table:hover {
610
+ cursor: pointer !important;
611
+ }
612
+
613
+ /* datatable row */
614
+ .v-data-table .v-table__wrapper > table > thead > tr > td,
615
+ .v-data-table .v-table__wrapper > table > thead > tr th,
616
+ .v-data-table .v-table__wrapper > table tbody > tr > td,
617
+ .v-data-table .v-table__wrapper > table tbody > tr th {
618
+ background-color: #d3d3d375 !important;
619
+ }
620
+
621
+ /* datatable header contour */
622
+ .v-data-table .v-table__wrapper > table > thead > tr > th,
623
+ .v-data-table .v-table__wrapper > table tbody > tr > th {
624
+ background-color: white !important;
625
+ border-bottom: 4px #223654 solid !important;
626
+ }
627
+
628
+ /* datatable header intérieur*/
629
+ .v-data-table-header__content {
630
+ background-color: white !important;
631
+ color: #223654 !important;
632
+ font-weight: 600;
633
+ font-size: 15px;
634
+ height: 46px;
635
+ }
636
+
637
+ /* datatable footer */
638
+ .v-data-table-footer {
639
+ background-color: white !important;
640
+ border-top: 4px #223654 solid !important;
641
+ }
642
+
643
+ /* datatable row hover */
644
+ .v-data-table .v-table__wrapper > table tbody > tr:hover > td,
645
+ .v-data-table .v-table__wrapper > table tbody > tr:hover {
646
+ background-color: #e0e0e0 !important;
647
+ }
648
+
649
+ .iconeSupprimer:hover {
650
+ color: red;
651
+ }
652
+ .v-icon.v-icon.v-icon--link.iconeSupprimer:hover {
653
+ color: red !important;
654
+ }
655
+ .iconeEditer:hover {
656
+ color: #095797;
657
+ }
658
+
659
+ /* hover icone à droite du ajouter*/
660
+ .text-grisMoyen:hover {
661
+ color: #095797 !important;
662
+ }
663
+
664
+ /* icône d'ancre drag & drop */
665
+ .ancre-drag {
666
+ cursor: grab;
667
+ color: #808a9d;
668
+ }
669
+ .ancre-drag:active {
670
+ cursor: grabbing;
671
+ }
672
+ </style>