codevdesign 1.0.68 → 1.0.70

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,671 @@
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 },
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
+ 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>