codevdesign 2.0.18 → 2.0.20

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,829 +1,829 @@
1
- <template>
2
- <csqc-modale
3
- ref="modale"
4
- :titre="t('csqc.csqcDatatable.choixColonnes.titre')"
5
- :btn-annuler="!sauvegardeEnCours"
6
- :largeur="'80vw'"
7
- :max-width="'1000px'"
8
- @annuler="fermer"
9
- @ok="sauvegarder"
10
- >
11
- <!-- Erreurs -->
12
- <v-alert
13
- v-model="afficherErreur"
14
- type="error"
15
- closable
16
- >
17
- {{ erreur }}
18
- </v-alert>
19
-
20
- <v-row>
21
- <v-col
22
- cols="12"
23
- class="pa-0 relative"
24
- >
25
- <!-- Retour en haut (table des vues) -->
26
- <v-btn
27
- v-show="choixRetourEnHaut"
28
- class="BarreRechercheBackIcone retourHautChoix"
29
- icon
30
- color="primary"
31
- style="position: absolute"
32
- @click="retourEnHaut('#choixColonnes-choix > .v-data-table__wrapper')"
33
- >
34
- <v-icon>mdi-arrow-up</v-icon>
35
- </v-btn>
36
-
37
- <v-card
38
- rounded
39
- class="pr-0"
40
- >
41
- <!-- TABLE 1 : Liste des vues -->
42
- <v-data-table
43
- id="choixColonnes-choix"
44
- v-model:expanded="expandedChoixIds"
45
- class="limiteHauteurChoix"
46
- :headers="colonnesChoix"
47
- :items="choix"
48
- :item-value="row => choixOf(row)._id"
49
- show-expand
50
- :single-expand="true"
51
- :items-per-page="-1"
52
- hide-default-footer
53
- fixed-header
54
- @click:row="onClickRowChoix"
55
- >
56
- <!-- Bouton ajouter une vue -->
57
- <template #header.action>
58
- <bouton-primaire
59
- class="float-right"
60
- size="small"
61
- :disabled="desactiverAjout"
62
- @click.stop="ajouter"
63
- >
64
- {{ t('csqc.bouton.ajouter') }}
65
- </bouton-primaire>
66
- </template>
67
- <template #item.data-table-expand="{ internalItem, isExpanded, toggleExpand }">
68
- <bouton-tertiaire
69
- :append-icon="isExpanded(internalItem) ? 'mdi-chevron-up' : 'mdi-chevron-down'"
70
- :text="isExpanded(internalItem) ? 'Fermer' : 'Ouvrir'"
71
- class="text-none"
72
- color="medium-emphasis"
73
- size="small"
74
- width="105"
75
- border
76
- slim
77
- data-expander
78
- @click.stop="() => toggleExpand(internalItem)"
79
- ></bouton-tertiaire>
80
- </template>
81
- <!-- Expansion : Table des colonnes -->
82
- <template #expanded-row="{ columns }">
83
- <tr>
84
- <td
85
- :colspan="columns.length"
86
- class="pa-0 ma-0"
87
- >
88
- <v-row
89
- class="pa-0 ma-0"
90
- no-gutters
91
- >
92
- <v-col
93
- cols="12"
94
- class="pa-0 ma-0"
95
- >
96
- <!-- Retour en haut (table des colonnes) -->
97
- <v-btn
98
- v-show="colonnesRetourEnHaut"
99
- class="BarreRechercheBackIcone retourHautChoix colonnes"
100
- icon
101
- color="primary"
102
- style="position: absolute"
103
- @click="retourEnHaut('#choixColonnes-vue > .v-data-table__wrapper')"
104
- >
105
- <v-icon>mdi-arrow-up</v-icon>
106
- </v-btn>
107
-
108
- <!-- TABLE 2 : Colonnes de la vue sélectionnée -->
109
- <v-data-table
110
- id="choixColonnes-vue"
111
- v-sortable-data-table
112
- class="limiteHauteurChoix colonnes mt-1 mb-4 ordonable"
113
- :headers="colonnesChoixColonne"
114
- :items="colonnesEnCours"
115
- hover
116
- item-value="value"
117
- hide-default-footer
118
- :items-per-page="-1"
119
- fixed-header
120
- :sort-by="triColonnesChoix"
121
- @sorted="changeOrdre"
122
- >
123
- <!-- Alerte dans l'entête si aucune colonne n'est sélectionnée -->
124
- <template #header.action>
125
- <v-tooltip
126
- v-if="nbColonnesSelectionnees <= 0"
127
- location="start"
128
- color="warning"
129
- >
130
- <template #activator="{ props }">
131
- <v-icon
132
- color="warning"
133
- v-bind="props"
134
- >
135
- mdi-alert
136
- </v-icon>
137
- </template>
138
- {{ t('csqc.csqcDatatable.choixColonnes.activezUneColonne') }}
139
- </v-tooltip>
140
- </template>
141
-
142
- <!-- Toggle (oeil) -->
143
- <template #item.action="{ item }">
144
- <v-icon
145
- :color="couleurColonneCliquee(colonneOf(item))"
146
- @click.stop="basculeColonneClique(colonneOf(item))"
147
- >
148
- {{ colonneEstClique(colonneOf(item)) ? 'mdi-eye' : 'mdi-eye-off' }}
149
- </v-icon>
150
- </template>
151
-
152
- <!-- Label de colonne -->
153
- <template #item.title="{ value, item }">
154
- {{ value ?? colonneOf(item).value }}
155
- </template>
156
- </v-data-table>
157
- </v-col>
158
- </v-row>
159
- </td>
160
- </tr>
161
- </template>
162
-
163
- <!-- Nom de vue (édition inline) -->
164
- <template #item.nomVue="{ item }">
165
- <v-text-field
166
- v-if="choixOf(item).nomVue === vueEnEdition"
167
- v-model.trim="nomVueEnCours"
168
- :label="t('csqc.csqcDatatable.choixColonnes.nomVue')"
169
- single-line
170
- density="compact"
171
- autofocus
172
- class="py-0 my-0"
173
- :rules="regles.nomVue"
174
- hide-details
175
- @keydown.enter="accepterEdition"
176
- @keydown.esc.stop="annulerEdition"
177
- />
178
- <span
179
- v-else
180
- :class="{ 'error--text': !choixOf(item).nomVue }"
181
- >
182
- {{ choixOf(item).nomVue || t('csqc.csqcDatatable.choixColonnes.nomVueRequis') }}
183
- </span>
184
- </template>
185
-
186
- <!-- Actions (vue) -->
187
- <template #item.action="{ item }">
188
- <template v-if="choixOf(item).nomVue === vueEnEdition">
189
- <v-icon
190
- class="iconeSupprimer GouttiereSmall"
191
- @click.stop.prevent="annulerEdition"
192
- >
193
- mdi-window-close
194
- </v-icon>
195
-
196
- <v-icon
197
- class="iconeHover GouttiereSmall"
198
- @click.stop.prevent="accepterEdition"
199
- >
200
- mdi-check
201
- </v-icon>
202
- </template>
203
-
204
- <template v-else>
205
- <v-tooltip
206
- location="start"
207
- color="warning"
208
- >
209
- <template #activator="{ props }">
210
- <v-icon
211
- class="iconeHover GouttiereSmall"
212
- v-bind="props"
213
- @click.stop.prevent="selectionner(choixOf(item))"
214
- >
215
- mdi-eye
216
- </v-icon>
217
- </template>
218
- {{ t('csqc.csqcDatatable.choixColonnes.activerTooltip') }}
219
- </v-tooltip>
220
- <v-tooltip
221
- location="start"
222
- color="warning"
223
- >
224
- <template #activator="{ props }">
225
- <v-icon
226
- v-bind="props"
227
- class="iconeHover GouttiereSmall"
228
- :color="couleurDefaut(choixOf(item))"
229
- @click.stop.prevent="mettreDefaut(choixOf(item))"
230
- >
231
- mdi-star
232
- </v-icon>
233
- </template>
234
- {{ t('csqc.csqcDatatable.choixColonnes.defautTooltip') }}
235
- </v-tooltip>
236
- <!-- <v-icon
237
- class="iconeHover GouttiereSmall"
238
- @click.stop.prevent="editer(choixOf(item))"
239
- >
240
- mdi-pencil
241
- </v-icon> -->
242
-
243
- <v-icon
244
- class="iconeSupprimer GouttiereSmall"
245
- @click.stop.prevent="supprimer(choixOf(item))"
246
- >
247
- mdi-delete
248
- </v-icon>
249
- </template>
250
- </template>
251
- </v-data-table>
252
- </v-card>
253
- </v-col>
254
- </v-row>
255
- </csqc-modale>
256
- </template>
257
-
258
- <script setup lang="ts">
259
- /* eslint-disable @typescript-eslint/no-explicit-any */
260
-
261
- /**
262
- * csqcTableModaleChoixColonnes.vue
263
- * - Permet de créer/éditer des "vues" de colonnes (ensembles de colonnes visibles)
264
- * - Drag & drop pour réordonner les colonnes
265
- * - Persistance via endpoint ComposantUI/Colonnes/...
266
- */
267
-
268
- import axios from '../../outils/appAxios'
269
- import Sortable from 'sortablejs'
270
- import { computed, nextTick, reactive, ref, watch } from 'vue'
271
- import { useGoTo } from 'vuetify'
272
- import { useI18n } from 'vue-i18n'
273
- import CsqcModale from '../csqcDialogue.vue'
274
-
275
- type SortableEvent = import('sortablejs').SortableEvent
276
-
277
- // ─────────────────────────────────────────────────────────────────────────────
278
- // Types
279
- // ─────────────────────────────────────────────────────────────────────────────
280
- type ColonneChoix = {
281
- value: string
282
- text?: string
283
- ordre?: number
284
- }
285
-
286
- type Colonne = {
287
- value: string
288
- title?: string
289
- ordre?: number
290
- }
291
-
292
- type ChoixVue = {
293
- nomVue: string
294
- colonnes: string[]
295
- defaut: boolean
296
- _id: string // interne (clé stable si nomVue est vide)
297
- }
298
-
299
- type DataTableItemLike<T> = { raw: T }
300
-
301
- // ─────────────────────────────────────────────────────────────────────────────
302
- // Helpers "unwrap" (évite item.raw dans le template + évite génériques TS dans template)
303
- // ─────────────────────────────────────────────────────────────────────────────
304
- function asRaw<T>(item: T | DataTableItemLike<T>): T {
305
- return (item as any) ?? (item as T)
306
- }
307
-
308
- const choixOf = (x: any) => asRaw<ChoixVue>(x)
309
- const colonneOf = (x: any) => asRaw<Colonne>(x)
310
-
311
- // Génère des ids pour stabiliser l’expansion / l’édition même si nomVue = ''
312
- function assurerIds(vues: ChoixVue[]) {
313
- for (const v of vues) {
314
- if (!v._id) v._id = crypto.randomUUID()
315
- }
316
- }
317
-
318
- // Clone profond simple (OK ici car données sérialisables)
319
- function deepClone<T>(x: T): T {
320
- return JSON.parse(JSON.stringify(x)) as T
321
- }
322
-
323
- // ─────────────────────────────────────────────────────────────────────────────
324
- // Props / Emits / Expose
325
- // ─────────────────────────────────────────────────────────────────────────────
326
- type ModaleExpose = { ouvrir: () => void; fermer: () => void }
327
-
328
- const props = defineProps<{
329
- urlbase: string
330
- formulaireId: number
331
- identifiant: string
332
- colonnes: ColonneChoix[]
333
- choixOrigine: ChoixVue[]
334
- }>()
335
-
336
- const emit = defineEmits<{
337
- (e: 'selection', choix: ChoixVue): void
338
- (e: 'sauvegarde', choix: ChoixVue[]): void
339
- }>()
340
-
341
- const modale = ref<ModaleExpose | null>(null)
342
-
343
- defineExpose({ ouvrir, fermer })
344
-
345
- // ─────────────────────────────────────────────────────────────────────────────
346
- // Directive: drag & drop (sortablejs)
347
- // ─────────────────────────────────────────────────────────────────────────────
348
- defineOptions({
349
- directives: {
350
- sortableDataTable: {
351
- mounted(el: HTMLElement, _binding: any, vnode: any) {
352
- const tbody = el.getElementsByTagName('tbody')?.[0]
353
- if (!tbody) return
354
-
355
- Sortable.create(tbody, {
356
- animation: 150,
357
- onUpdate(event: SortableEvent) {
358
- if (event.oldIndex == null || event.newIndex == null) return
359
- vnode?.component?.emit?.('sorted', event)
360
- },
361
- })
362
- },
363
- },
364
- },
365
- })
366
-
367
- // ─────────────────────────────────────────────────────────────────────────────
368
- // UI / i18n / scroll helpers
369
- // ─────────────────────────────────────────────────────────────────────────────
370
- const { t } = useI18n()
371
- const goTo = useGoTo()
372
-
373
- const erreur = ref('')
374
- const afficherErreur = ref(false)
375
-
376
- const choixRetourEnHaut = ref(false)
377
- const colonnesRetourEnHaut = ref(false)
378
-
379
- // ─────────────────────────────────────────────────────────────────────────────
380
- // State
381
- // ─────────────────────────────────────────────────────────────────────────────
382
- const choix = ref<ChoixVue[]>([])
383
- const choixEnCours = ref<ChoixVue | null>(null)
384
- const colonnesEnCours = ref<Colonne[]>([])
385
-
386
- const sauvegardeEnCours = ref(false)
387
-
388
- // Edition du nom de vue
389
- const nomVueEnCours = ref('')
390
- const vueEnEdition = ref('')
391
- const desactiverAjout = ref(false)
392
-
393
- // Tri visuel (on veut ordre asc)
394
- const triColonnesChoix = ref<{ key: string; order?: 'asc' | 'desc' }[]>([{ key: 'ordre', order: 'asc' }])
395
-
396
- // Règles validation
397
- const regles = reactive({
398
- nomVue: [
399
- (v: string) => (v && v.trim().length >= 1) || t('csqc.csqcDatatable.choixColonnes.nomVueRequis'),
400
- (v: string) =>
401
- !choix.value.some(({ nomVue }) => v.trim() === nomVue) || t('csqc.csqcDatatable.choixColonnes.nomVueExiste'),
402
- ],
403
- })
404
-
405
- // Affiche l'alerte si aucune colonne sélectionnée dans la vue courante
406
- const nbColonnesSelectionnees = computed(() => choixEnCours.value?.colonnes?.length ?? 0)
407
-
408
- function ouvrirVue(ch: ChoixVue) {
409
- // remonte en haut la table des colonnes quand on change
410
- if (choixEnCours.value) retourEnHaut('#choixColonnes-vue > .v-data-table__wrapper')
411
-
412
- // Build colonnesEnCours à partir des colonnes disponibles
413
- colonnesEnCours.value = deepClone(props.colonnes)
414
-
415
- // Référence réactive dans la liste
416
- const refDansListe = choix.value.find(x => x._id === ch._id) ?? ch
417
- choixEnCours.value = refDansListe
418
-
419
- // Colonnes sélectionnées d'abord, puis le reste
420
- const selected: Colonne[] = []
421
- const missing: string[] = []
422
-
423
- for (const key of choixEnCours.value.colonnes) {
424
- const col = colonnesEnCours.value.find(c => c.value === key)
425
- if (!col) missing.push(key)
426
- else selected.push(col)
427
- }
428
-
429
- // Nettoyage des colonnes disparues
430
- if (missing.length) {
431
- choixEnCours.value.colonnes = choixEnCours.value.colonnes.filter(k => !missing.includes(k))
432
- }
433
-
434
- // selected puis disponibles non sélectionnées
435
- colonnesEnCours.value = selected.concat(
436
- colonnesEnCours.value.filter(c => !choixEnCours.value!.colonnes.includes(c.value)),
437
- )
438
-
439
- calculOrdreColonnes()
440
- ecouteDefilerColonnes()
441
- }
442
-
443
- const expandedChoixIds = ref<string[]>([])
444
-
445
- // colonnes
446
- const colonnesChoixColonne = computed(() => [
447
- {
448
- title: t('csqc.csqcDatatable.choixColonnes.ordre'),
449
- key: 'ordre',
450
- align: 'start' as const,
451
- sortable: false,
452
- width: '5%',
453
- },
454
- {
455
- title: t('csqc.csqcDatatable.choixColonnes.nomColonne', colonnesEnCours.value.length),
456
- key: 'title',
457
- align: 'start' as const,
458
- sortable: false,
459
- width: '85%',
460
- },
461
- { title: '', key: 'action', align: 'end' as const, sortable: false, width: '10%' },
462
- ])
463
-
464
- const colonnesChoix = computed(() => [
465
- {
466
- title: t('csqc.csqcDatatable.choixColonnes.vues', choix.value.length),
467
- key: 'nomVue',
468
- width: '70%',
469
- align: 'start' as const,
470
- },
471
- { title: '', key: 'action', width: '30%', align: 'end' as const, sortable: false },
472
- ])
473
-
474
- // ─────────────────────────────────────────────────────────────────────────────
475
- // actions
476
- // ─────────────────────────────────────────────────────────────────────────────
477
- function ouvrir() {
478
- choix.value = deepClone(props.choixOrigine)
479
- assurerIds(choix.value) // obligatoire
480
- expandedChoixIds.value = [] // reset
481
- modale.value?.ouvrir()
482
- }
483
- function fermer() {
484
- // Reset UI
485
- sauvegardeEnCours.value = false
486
- desactiverAjout.value = false
487
- vueEnEdition.value = ''
488
- nomVueEnCours.value = ''
489
-
490
- // Reset sélection
491
- choixEnCours.value = null
492
- colonnesEnCours.value = []
493
-
494
- // Reset erreurs
495
- afficherErreur.value = false
496
- erreur.value = ''
497
-
498
- modale.value?.fermer()
499
- }
500
-
501
- // ─────────────────────────────────────────────────────────────────────────────
502
- // Actions : vues
503
- // ─────────────────────────────────────────────────────────────────────────────
504
- function ajouter() {
505
- const ch: ChoixVue = {
506
- _id: crypto.randomUUID(),
507
- nomVue: '',
508
- colonnes: [],
509
- defaut: choix.value.length === 0,
510
- }
511
- choix.value.push(ch)
512
- expandedChoixIds.value = [ch._id] // ouvre celle-là
513
- editer(ch)
514
- }
515
-
516
- function editer(ch: ChoixVue) {
517
- desactiverAjout.value = true
518
-
519
- // Nettoyage d’un placeholder vide précédent (si on relance edit)
520
- if (ch.nomVue !== '') supprimer({ _id: 'placeholder', nomVue: '', colonnes: [], defaut: false })
521
-
522
- erreur.value = ''
523
- nomVueEnCours.value = ch.nomVue
524
- vueEnEdition.value = ch.nomVue
525
- }
526
-
527
- function accepterEdition() {
528
- const nom = nomVueEnCours.value?.trim()
529
-
530
- if (!nom) {
531
- erreur.value = t('csqc.csqcDatatable.choixColonnes.nomVueRequis')
532
- return
533
- }
534
-
535
- if (choix.value.some(({ nomVue }) => nomVue === nom)) {
536
- erreur.value = t('csqc.csqcDatatable.choixColonnes.nomVueExiste')
537
- return
538
- }
539
-
540
- const ch = choix.value.find(({ nomVue }) => nomVue === vueEnEdition.value)
541
- if (!ch) return
542
-
543
- const etaitSelectionnee = choixEstClique(ch)
544
- ch.nomVue = nom
545
-
546
- // Si c’était la vue active, on s’assure que la ref active reste valide
547
- if (etaitSelectionnee) {
548
- choixEnCours.value = ch
549
- }
550
-
551
- // Reset édition
552
- vueEnEdition.value = ''
553
- nomVueEnCours.value = ''
554
- desactiverAjout.value = false
555
- }
556
-
557
- function annulerEdition() {
558
- const ch = choix.value.find(({ nomVue }) => nomVue === vueEnEdition.value)
559
- if (!ch) return
560
-
561
- // Si c'était une nouvelle vue vide, on la retire
562
- if (ch.nomVue === '') supprimer(ch)
563
-
564
- vueEnEdition.value = ''
565
- nomVueEnCours.value = ''
566
- desactiverAjout.value = false
567
- }
568
-
569
- function selectionner(ch: ChoixVue) {
570
- emit('selection', ch)
571
- fermer()
572
- }
573
-
574
- function supprimer(ch: ChoixVue) {
575
- if (choixEstClique(ch)) choixEnCours.value = null
576
-
577
- const index = choix.value.findIndex(({ nomVue }) => nomVue === ch.nomVue)
578
- if (index === -1) return
579
-
580
- // Si on supprime le défaut, on transfère le défaut ailleurs
581
- if (choix.value[index].defaut) {
582
- const autre = choix.value.find(({ nomVue }) => nomVue !== ch.nomVue)
583
- if (autre) autre.defaut = true
584
- }
585
-
586
- choix.value.splice(index, 1)
587
- }
588
-
589
- function mettreDefaut(ch: ChoixVue) {
590
- const actuel = choix.value.find(v => v.defaut)
591
- if (actuel) actuel.defaut = false
592
-
593
- const cible = choix.value.find(v => v.nomVue === ch.nomVue)
594
- if (cible) cible.defaut = true
595
- }
596
-
597
- function couleurDefaut(ch: ChoixVue) {
598
- return ch.defaut ? '#daa520' : 'gray'
599
- }
600
-
601
- function choixEstClique(ch: ChoixVue) {
602
- return ch.nomVue === choixEnCours.value?.nomVue
603
- }
604
-
605
- // Click row Vuetify (payload = { item })
606
- function onClickRowChoix(_e: Event, payload: any) {
607
- cliquer(choixOf(payload?.item ?? payload))
608
- }
609
-
610
- /**
611
- * Sélection d’une vue :
612
- * - recalcul de colonnesEnCours (selected d'abord, puis le reste)
613
- * - nettoyage des colonnes "non disponibles" (si props.colonnes a changé)
614
- */
615
- function cliquer(ch: ChoixVue) {
616
- const id = ch._id
617
- if (!id) return
618
-
619
- expandedChoixIds.value = expandedChoixIds.value[0] === id ? [] : [id]
620
- }
621
-
622
- // ─────────────────────────────────────────────────────────────────────────────
623
- // Actions : colonnes
624
- // ─────────────────────────────────────────────────────────────────────────────
625
- function colonneEstClique(colonne: Colonne) {
626
- if (!choixEnCours.value) return false
627
- return choixEnCours.value.colonnes.includes(colonne.value)
628
- }
629
-
630
- function couleurColonneCliquee(colonne: Colonne) {
631
- return colonneEstClique(colonne) ? 'primary' : 'gray'
632
- }
633
-
634
- function basculeColonneClique(colonne: Colonne) {
635
- if (!choixEnCours.value) return
636
-
637
- if (colonneEstClique(colonne)) {
638
- choixEnCours.value.colonnes = choixEnCours.value.colonnes.filter(x => x !== colonne.value)
639
- return
640
- }
641
-
642
- choixEnCours.value.colonnes.push(colonne.value)
643
- }
644
-
645
- // Liste des colonnes actuellement "cliquées" selon l’ordre affiché
646
- const colonnesCliquees = computed(() => {
647
- if (!choixEnCours.value) return []
648
- return colonnesEnCours.value.filter(c => choixEnCours.value!.colonnes.includes(c.value)).map(c => c.value)
649
- })
650
-
651
- function changeOrdre(event: SortableEvent) {
652
- if (event.oldIndex == null || event.newIndex == null) return
653
-
654
- const moved = colonnesEnCours.value.splice(event.oldIndex, 1)[0]
655
- colonnesEnCours.value.splice(event.newIndex, 0, moved)
656
-
657
- calculOrdreColonnes()
658
-
659
- // Après reorder, on sauvegarde la sélection dans le nouvel ordre visuel
660
- if (choixEnCours.value) {
661
- choixEnCours.value.colonnes = colonnesCliquees.value
662
- }
663
- }
664
-
665
- function calculOrdreColonnes() {
666
- for (let i = 0; i < colonnesEnCours.value.length; i += 1) {
667
- colonnesEnCours.value[i].ordre = i + 1
668
- }
669
- }
670
-
671
- // ─────────────────────────────────────────────────────────────────────────────
672
- // axios
673
- // ─────────────────────────────────────────────────────────────────────────────
674
- async function sauvegarder() {
675
- sauvegardeEnCours.value = true
676
-
677
- try {
678
- const res: any = await axios
679
- .getAxios()
680
- .post(`${props.urlbase}/api/ComposantUI/Colonnes/${props.formulaireId}/Identifiant/${props.identifiant}`, {
681
- valeur: JSON.stringify(choix.value),
682
- })
683
-
684
- const payload = res?.data ?? res
685
-
686
- // payload peut être un json string ou déjà un tableau
687
- choix.value = typeof payload === 'string' ? (JSON.parse(payload) as ChoixVue[]) : (payload as ChoixVue[])
688
-
689
- assurerIds(choix.value)
690
-
691
- // Notifie le parent
692
- emit('sauvegarde', choix.value)
693
- if (choixEnCours.value) emit('selection', choixEnCours.value)
694
-
695
- fermer()
696
- } catch (e) {
697
- erreur.value = String(e)
698
- } finally {
699
- sauvegardeEnCours.value = false
700
- }
701
- }
702
-
703
- // ─────────────────────────────────────────────────────────────────────────────
704
- // Scroll helpers
705
- // ─────────────────────────────────────────────────────────────────────────────
706
- function retourEnHaut(cible: string) {
707
- goTo(0, { container: cible })
708
- }
709
-
710
- function defilerChoix(e: Event) {
711
- const target = e.target as HTMLElement
712
- choixRetourEnHaut.value = target.scrollTop >= 200
713
- }
714
-
715
- function ecouteDefilerChoix() {
716
- const el = document.querySelector('#choixColonnes-choix > .v-data-table__wrapper')
717
- if (!el) {
718
- setTimeout(ecouteDefilerChoix, 100)
719
- return
720
- }
721
- el.addEventListener('scroll', defilerChoix)
722
- }
723
-
724
- function defilerColonnes(e: Event) {
725
- const target = e.target as HTMLElement
726
- colonnesRetourEnHaut.value = target.scrollTop >= 200
727
- }
728
-
729
- function ecouteDefilerColonnes() {
730
- nextTick(() => {
731
- const el = document.querySelector('#choixColonnes-vue > .v-data-table__wrapper')
732
- if (!el) {
733
- setTimeout(ecouteDefilerColonnes, 100)
734
- return
735
- }
736
- el.addEventListener('scroll', defilerColonnes)
737
- })
738
- }
739
-
740
- // ─────────────────────────────────────────────────────────────────────────────
741
- // Watchers
742
- // ─────────────────────────────────────────────────────────────────────────────
743
- watch(erreur, () => {
744
- afficherErreur.value = erreur.value !== ''
745
- })
746
-
747
- watch(afficherErreur, v => {
748
- if (v === false) erreur.value = ''
749
- })
750
-
751
- watch(
752
- expandedChoixIds,
753
- v => {
754
- const id = v?.[0]
755
-
756
- // Fermeture
757
- if (!id) {
758
- choixEnCours.value = null
759
- colonnesEnCours.value = []
760
- return
761
- }
762
-
763
- // Ouverture / changement
764
- const ch = choix.value.find(x => x._id === id)
765
- if (!ch) return
766
-
767
- ouvrirVue(ch) // IMPORTANT: ouverture "force", pas toggle
768
- },
769
- { flush: 'post' }, // <-- clé du fix
770
- )
771
- watch(
772
- expandedChoixIds,
773
- v => {
774
- if (v.length > 1) expandedChoixIds.value = [v[v.length - 1]] // garde le dernier
775
- },
776
- { flush: 'sync' },
777
- )
778
- </script>
779
-
780
- <style scoped>
781
- /* Layout */
782
- .relative {
783
- position: relative;
784
- }
785
-
786
- /* Boutons "retour en haut" */
787
- .retourHautChoix {
788
- bottom: 4px !important;
789
- right: 4px;
790
- }
791
- .retourHautChoix.colonnes {
792
- bottom: 8px !important;
793
- right: 48px;
794
- }
795
-
796
- /* Scroll vertical des tables */
797
- .limiteHauteurChoix > .v-data-table__wrapper {
798
- max-height: min(80vh - 200px, 900px);
799
- overflow-y: auto;
800
- }
801
- .limiteHauteurChoix.colonnes > .v-data-table__wrapper {
802
- max-height: min(80vh - 294px, 600px);
803
- }
804
-
805
- /* Z-index du header table 1 */
806
- #choixColonnes-choix > .v-data-table__wrapper > table > thead > tr > th {
807
- z-index: 3;
808
- }
809
-
810
- /* Drag cursor */
811
- .v-data-table.ordonable > .v-data-table__wrapper > table > tbody > tr {
812
- cursor: move;
813
- }
814
-
815
- /* Icons */
816
- .iconeSupprimer:hover {
817
- color: red;
818
- }
819
- .v-icon.v-icon.v-icon--link.iconeSupprimer:hover {
820
- color: red !important;
821
- }
822
- .iconeEditer:hover {
823
- color: #095797;
824
- }
825
-
826
- .GouttiereSmall {
827
- margin-left: 8px;
828
- }
829
- </style>
1
+ <template>
2
+ <csqc-modale
3
+ ref="modale"
4
+ :titre="t('csqc.csqcDatatable.choixColonnes.titre')"
5
+ :btn-annuler="!sauvegardeEnCours"
6
+ :largeur="'80vw'"
7
+ :max-width="'1000px'"
8
+ @annuler="fermer"
9
+ @ok="sauvegarder"
10
+ >
11
+ <!-- Erreurs -->
12
+ <v-alert
13
+ v-model="afficherErreur"
14
+ type="error"
15
+ closable
16
+ >
17
+ {{ erreur }}
18
+ </v-alert>
19
+
20
+ <v-row>
21
+ <v-col
22
+ cols="12"
23
+ class="pa-0 relative"
24
+ >
25
+ <!-- Retour en haut (table des vues) -->
26
+ <v-btn
27
+ v-show="choixRetourEnHaut"
28
+ class="BarreRechercheBackIcone retourHautChoix"
29
+ icon
30
+ color="primary"
31
+ style="position: absolute"
32
+ @click="retourEnHaut('#choixColonnes-choix > .v-data-table__wrapper')"
33
+ >
34
+ <v-icon>mdi-arrow-up</v-icon>
35
+ </v-btn>
36
+
37
+ <v-card
38
+ rounded
39
+ class="pr-0"
40
+ >
41
+ <!-- TABLE 1 : Liste des vues -->
42
+ <v-data-table
43
+ id="choixColonnes-choix"
44
+ v-model:expanded="expandedChoixIds"
45
+ class="limiteHauteurChoix"
46
+ :headers="colonnesChoix"
47
+ :items="choix"
48
+ :item-value="row => choixOf(row)._id"
49
+ show-expand
50
+ :single-expand="true"
51
+ :items-per-page="-1"
52
+ hide-default-footer
53
+ fixed-header
54
+ @click:row="onClickRowChoix"
55
+ >
56
+ <!-- Bouton ajouter une vue -->
57
+ <template #header.action>
58
+ <bouton-primaire
59
+ class="float-right"
60
+ size="small"
61
+ :disabled="desactiverAjout"
62
+ @click.stop="ajouter"
63
+ >
64
+ {{ t('csqc.bouton.ajouter') }}
65
+ </bouton-primaire>
66
+ </template>
67
+ <template #item.data-table-expand="{ internalItem, isExpanded, toggleExpand }">
68
+ <bouton-tertiaire
69
+ :append-icon="isExpanded(internalItem) ? 'mdi-chevron-up' : 'mdi-chevron-down'"
70
+ :text="isExpanded(internalItem) ? 'Fermer' : 'Ouvrir'"
71
+ class="text-none"
72
+ color="medium-emphasis"
73
+ size="small"
74
+ width="105"
75
+ border
76
+ slim
77
+ data-expander
78
+ @click.stop="() => toggleExpand(internalItem)"
79
+ ></bouton-tertiaire>
80
+ </template>
81
+ <!-- Expansion : Table des colonnes -->
82
+ <template #expanded-row="{ columns }">
83
+ <tr>
84
+ <td
85
+ :colspan="columns.length"
86
+ class="pa-0 ma-0"
87
+ >
88
+ <v-row
89
+ class="pa-0 ma-0"
90
+ no-gutters
91
+ >
92
+ <v-col
93
+ cols="12"
94
+ class="pa-0 ma-0"
95
+ >
96
+ <!-- Retour en haut (table des colonnes) -->
97
+ <v-btn
98
+ v-show="colonnesRetourEnHaut"
99
+ class="BarreRechercheBackIcone retourHautChoix colonnes"
100
+ icon
101
+ color="primary"
102
+ style="position: absolute"
103
+ @click="retourEnHaut('#choixColonnes-vue > .v-data-table__wrapper')"
104
+ >
105
+ <v-icon>mdi-arrow-up</v-icon>
106
+ </v-btn>
107
+
108
+ <!-- TABLE 2 : Colonnes de la vue sélectionnée -->
109
+ <v-data-table
110
+ id="choixColonnes-vue"
111
+ v-sortable-data-table
112
+ class="limiteHauteurChoix colonnes mt-1 mb-4 ordonable"
113
+ :headers="colonnesChoixColonne"
114
+ :items="colonnesEnCours"
115
+ hover
116
+ item-value="value"
117
+ hide-default-footer
118
+ :items-per-page="-1"
119
+ fixed-header
120
+ :sort-by="triColonnesChoix"
121
+ @sorted="changeOrdre"
122
+ >
123
+ <!-- Alerte dans l'entête si aucune colonne n'est sélectionnée -->
124
+ <template #header.action>
125
+ <v-tooltip
126
+ v-if="nbColonnesSelectionnees <= 0"
127
+ location="start"
128
+ color="warning"
129
+ >
130
+ <template #activator="{ props }">
131
+ <v-icon
132
+ color="warning"
133
+ v-bind="props"
134
+ >
135
+ mdi-alert
136
+ </v-icon>
137
+ </template>
138
+ {{ t('csqc.csqcDatatable.choixColonnes.activezUneColonne') }}
139
+ </v-tooltip>
140
+ </template>
141
+
142
+ <!-- Toggle (oeil) -->
143
+ <template #item.action="{ item }">
144
+ <v-icon
145
+ :color="couleurColonneCliquee(colonneOf(item))"
146
+ @click.stop="basculeColonneClique(colonneOf(item))"
147
+ >
148
+ {{ colonneEstClique(colonneOf(item)) ? 'mdi-eye' : 'mdi-eye-off' }}
149
+ </v-icon>
150
+ </template>
151
+
152
+ <!-- Label de colonne -->
153
+ <template #item.title="{ value, item }">
154
+ {{ value ?? colonneOf(item).value }}
155
+ </template>
156
+ </v-data-table>
157
+ </v-col>
158
+ </v-row>
159
+ </td>
160
+ </tr>
161
+ </template>
162
+
163
+ <!-- Nom de vue (édition inline) -->
164
+ <template #item.nomVue="{ item }">
165
+ <v-text-field
166
+ v-if="choixOf(item).nomVue === vueEnEdition"
167
+ v-model.trim="nomVueEnCours"
168
+ :label="t('csqc.csqcDatatable.choixColonnes.nomVue')"
169
+ single-line
170
+ density="compact"
171
+ autofocus
172
+ class="py-0 my-0"
173
+ :rules="regles.nomVue"
174
+ hide-details
175
+ @keydown.enter="accepterEdition"
176
+ @keydown.esc.stop="annulerEdition"
177
+ />
178
+ <span
179
+ v-else
180
+ :class="{ 'error--text': !choixOf(item).nomVue }"
181
+ >
182
+ {{ choixOf(item).nomVue || t('csqc.csqcDatatable.choixColonnes.nomVueRequis') }}
183
+ </span>
184
+ </template>
185
+
186
+ <!-- Actions (vue) -->
187
+ <template #item.action="{ item }">
188
+ <template v-if="choixOf(item).nomVue === vueEnEdition">
189
+ <v-icon
190
+ class="iconeSupprimer GouttiereSmall"
191
+ @click.stop.prevent="annulerEdition"
192
+ >
193
+ mdi-window-close
194
+ </v-icon>
195
+
196
+ <v-icon
197
+ class="iconeHover GouttiereSmall"
198
+ @click.stop.prevent="accepterEdition"
199
+ >
200
+ mdi-check
201
+ </v-icon>
202
+ </template>
203
+
204
+ <template v-else>
205
+ <v-tooltip
206
+ location="start"
207
+ color="warning"
208
+ >
209
+ <template #activator="{ props }">
210
+ <v-icon
211
+ class="iconeHover GouttiereSmall"
212
+ v-bind="props"
213
+ @click.stop.prevent="selectionner(choixOf(item))"
214
+ >
215
+ mdi-eye
216
+ </v-icon>
217
+ </template>
218
+ {{ t('csqc.csqcDatatable.choixColonnes.activerTooltip') }}
219
+ </v-tooltip>
220
+ <v-tooltip
221
+ location="start"
222
+ color="warning"
223
+ >
224
+ <template #activator="{ props }">
225
+ <v-icon
226
+ v-bind="props"
227
+ class="iconeHover GouttiereSmall"
228
+ :color="couleurDefaut(choixOf(item))"
229
+ @click.stop.prevent="mettreDefaut(choixOf(item))"
230
+ >
231
+ mdi-star
232
+ </v-icon>
233
+ </template>
234
+ {{ t('csqc.csqcDatatable.choixColonnes.defautTooltip') }}
235
+ </v-tooltip>
236
+ <!-- <v-icon
237
+ class="iconeHover GouttiereSmall"
238
+ @click.stop.prevent="editer(choixOf(item))"
239
+ >
240
+ mdi-pencil
241
+ </v-icon> -->
242
+
243
+ <v-icon
244
+ class="iconeSupprimer GouttiereSmall"
245
+ @click.stop.prevent="supprimer(choixOf(item))"
246
+ >
247
+ mdi-delete
248
+ </v-icon>
249
+ </template>
250
+ </template>
251
+ </v-data-table>
252
+ </v-card>
253
+ </v-col>
254
+ </v-row>
255
+ </csqc-modale>
256
+ </template>
257
+
258
+ <script setup lang="ts">
259
+ /* eslint-disable @typescript-eslint/no-explicit-any */
260
+
261
+ /**
262
+ * csqcTableModaleChoixColonnes.vue
263
+ * - Permet de créer/éditer des "vues" de colonnes (ensembles de colonnes visibles)
264
+ * - Drag & drop pour réordonner les colonnes
265
+ * - Persistance via endpoint ComposantUI/Colonnes/...
266
+ */
267
+
268
+ import axios from '../../outils/appAxios'
269
+ import Sortable from 'sortablejs'
270
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
271
+ import { useGoTo } from 'vuetify'
272
+ import { useI18n } from 'vue-i18n'
273
+ import CsqcModale from '../csqcDialogue.vue'
274
+
275
+ type SortableEvent = import('sortablejs').SortableEvent
276
+
277
+ // ─────────────────────────────────────────────────────────────────────────────
278
+ // Types
279
+ // ─────────────────────────────────────────────────────────────────────────────
280
+ type ColonneChoix = {
281
+ value: string
282
+ text?: string
283
+ ordre?: number
284
+ }
285
+
286
+ type Colonne = {
287
+ value: string
288
+ title?: string
289
+ ordre?: number
290
+ }
291
+
292
+ type ChoixVue = {
293
+ nomVue: string
294
+ colonnes: string[]
295
+ defaut: boolean
296
+ _id: string // interne (clé stable si nomVue est vide)
297
+ }
298
+
299
+ type DataTableItemLike<T> = { raw: T }
300
+
301
+ // ─────────────────────────────────────────────────────────────────────────────
302
+ // Helpers "unwrap" (évite item.raw dans le template + évite génériques TS dans template)
303
+ // ─────────────────────────────────────────────────────────────────────────────
304
+ function asRaw<T>(item: T | DataTableItemLike<T>): T {
305
+ return (item as any) ?? (item as T)
306
+ }
307
+
308
+ const choixOf = (x: any) => asRaw<ChoixVue>(x)
309
+ const colonneOf = (x: any) => asRaw<Colonne>(x)
310
+
311
+ // Génère des ids pour stabiliser l’expansion / l’édition même si nomVue = ''
312
+ function assurerIds(vues: ChoixVue[]) {
313
+ for (const v of vues) {
314
+ if (!v._id) v._id = crypto.randomUUID()
315
+ }
316
+ }
317
+
318
+ // Clone profond simple (OK ici car données sérialisables)
319
+ function deepClone<T>(x: T): T {
320
+ return JSON.parse(JSON.stringify(x)) as T
321
+ }
322
+
323
+ // ─────────────────────────────────────────────────────────────────────────────
324
+ // Props / Emits / Expose
325
+ // ─────────────────────────────────────────────────────────────────────────────
326
+ type ModaleExpose = { ouvrir: () => void; fermer: () => void }
327
+
328
+ const props = defineProps<{
329
+ urlbase: string
330
+ formulaireId: number
331
+ identifiant: string
332
+ colonnes: ColonneChoix[]
333
+ choixOrigine: ChoixVue[]
334
+ }>()
335
+
336
+ const emit = defineEmits<{
337
+ (e: 'selection', choix: ChoixVue): void
338
+ (e: 'sauvegarde', choix: ChoixVue[]): void
339
+ }>()
340
+
341
+ const modale = ref<ModaleExpose | null>(null)
342
+
343
+ defineExpose({ ouvrir, fermer })
344
+
345
+ // ─────────────────────────────────────────────────────────────────────────────
346
+ // Directive: drag & drop (sortablejs)
347
+ // ─────────────────────────────────────────────────────────────────────────────
348
+ defineOptions({
349
+ directives: {
350
+ sortableDataTable: {
351
+ mounted(el: HTMLElement, _binding: any, vnode: any) {
352
+ const tbody = el.getElementsByTagName('tbody')?.[0]
353
+ if (!tbody) return
354
+
355
+ Sortable.create(tbody, {
356
+ animation: 150,
357
+ onUpdate(event: SortableEvent) {
358
+ if (event.oldIndex == null || event.newIndex == null) return
359
+ vnode?.component?.emit?.('sorted', event)
360
+ },
361
+ })
362
+ },
363
+ },
364
+ },
365
+ })
366
+
367
+ // ─────────────────────────────────────────────────────────────────────────────
368
+ // UI / i18n / scroll helpers
369
+ // ─────────────────────────────────────────────────────────────────────────────
370
+ const { t } = useI18n()
371
+ const goTo = useGoTo()
372
+
373
+ const erreur = ref('')
374
+ const afficherErreur = ref(false)
375
+
376
+ const choixRetourEnHaut = ref(false)
377
+ const colonnesRetourEnHaut = ref(false)
378
+
379
+ // ─────────────────────────────────────────────────────────────────────────────
380
+ // State
381
+ // ─────────────────────────────────────────────────────────────────────────────
382
+ const choix = ref<ChoixVue[]>([])
383
+ const choixEnCours = ref<ChoixVue | null>(null)
384
+ const colonnesEnCours = ref<Colonne[]>([])
385
+
386
+ const sauvegardeEnCours = ref(false)
387
+
388
+ // Edition du nom de vue
389
+ const nomVueEnCours = ref('')
390
+ const vueEnEdition = ref('')
391
+ const desactiverAjout = ref(false)
392
+
393
+ // Tri visuel (on veut ordre asc)
394
+ const triColonnesChoix = ref<{ key: string; order?: 'asc' | 'desc' }[]>([{ key: 'ordre', order: 'asc' }])
395
+
396
+ // Règles validation
397
+ const regles = reactive({
398
+ nomVue: [
399
+ (v: string) => (v && v.trim().length >= 1) || t('csqc.csqcDatatable.choixColonnes.nomVueRequis'),
400
+ (v: string) =>
401
+ !choix.value.some(({ nomVue }) => v.trim() === nomVue) || t('csqc.csqcDatatable.choixColonnes.nomVueExiste'),
402
+ ],
403
+ })
404
+
405
+ // Affiche l'alerte si aucune colonne sélectionnée dans la vue courante
406
+ const nbColonnesSelectionnees = computed(() => choixEnCours.value?.colonnes?.length ?? 0)
407
+
408
+ function ouvrirVue(ch: ChoixVue) {
409
+ // remonte en haut la table des colonnes quand on change
410
+ if (choixEnCours.value) retourEnHaut('#choixColonnes-vue > .v-data-table__wrapper')
411
+
412
+ // Build colonnesEnCours à partir des colonnes disponibles
413
+ colonnesEnCours.value = deepClone(props.colonnes)
414
+
415
+ // Référence réactive dans la liste
416
+ const refDansListe = choix.value.find(x => x._id === ch._id) ?? ch
417
+ choixEnCours.value = refDansListe
418
+
419
+ // Colonnes sélectionnées d'abord, puis le reste
420
+ const selected: Colonne[] = []
421
+ const missing: string[] = []
422
+
423
+ for (const key of choixEnCours.value.colonnes) {
424
+ const col = colonnesEnCours.value.find(c => c.value === key)
425
+ if (!col) missing.push(key)
426
+ else selected.push(col)
427
+ }
428
+
429
+ // Nettoyage des colonnes disparues
430
+ if (missing.length) {
431
+ choixEnCours.value.colonnes = choixEnCours.value.colonnes.filter(k => !missing.includes(k))
432
+ }
433
+
434
+ // selected puis disponibles non sélectionnées
435
+ colonnesEnCours.value = selected.concat(
436
+ colonnesEnCours.value.filter(c => !choixEnCours.value!.colonnes.includes(c.value)),
437
+ )
438
+
439
+ calculOrdreColonnes()
440
+ ecouteDefilerColonnes()
441
+ }
442
+
443
+ const expandedChoixIds = ref<string[]>([])
444
+
445
+ // colonnes
446
+ const colonnesChoixColonne = computed(() => [
447
+ {
448
+ title: t('csqc.csqcDatatable.choixColonnes.ordre'),
449
+ key: 'ordre',
450
+ align: 'start' as const,
451
+ sortable: false,
452
+ width: '5%',
453
+ },
454
+ {
455
+ title: t('csqc.csqcDatatable.choixColonnes.nomColonne', colonnesEnCours.value.length),
456
+ key: 'title',
457
+ align: 'start' as const,
458
+ sortable: false,
459
+ width: '85%',
460
+ },
461
+ { title: '', key: 'action', align: 'end' as const, sortable: false, width: '10%' },
462
+ ])
463
+
464
+ const colonnesChoix = computed(() => [
465
+ {
466
+ title: t('csqc.csqcDatatable.choixColonnes.vues', choix.value.length),
467
+ key: 'nomVue',
468
+ width: '70%',
469
+ align: 'start' as const,
470
+ },
471
+ { title: '', key: 'action', width: '30%', align: 'end' as const, sortable: false },
472
+ ])
473
+
474
+ // ─────────────────────────────────────────────────────────────────────────────
475
+ // actions
476
+ // ─────────────────────────────────────────────────────────────────────────────
477
+ function ouvrir() {
478
+ choix.value = deepClone(props.choixOrigine)
479
+ assurerIds(choix.value) // obligatoire
480
+ expandedChoixIds.value = [] // reset
481
+ modale.value?.ouvrir()
482
+ }
483
+ function fermer() {
484
+ // Reset UI
485
+ sauvegardeEnCours.value = false
486
+ desactiverAjout.value = false
487
+ vueEnEdition.value = ''
488
+ nomVueEnCours.value = ''
489
+
490
+ // Reset sélection
491
+ choixEnCours.value = null
492
+ colonnesEnCours.value = []
493
+
494
+ // Reset erreurs
495
+ afficherErreur.value = false
496
+ erreur.value = ''
497
+
498
+ modale.value?.fermer()
499
+ }
500
+
501
+ // ─────────────────────────────────────────────────────────────────────────────
502
+ // Actions : vues
503
+ // ─────────────────────────────────────────────────────────────────────────────
504
+ function ajouter() {
505
+ const ch: ChoixVue = {
506
+ _id: crypto.randomUUID(),
507
+ nomVue: '',
508
+ colonnes: [],
509
+ defaut: choix.value.length === 0,
510
+ }
511
+ choix.value.push(ch)
512
+ expandedChoixIds.value = [ch._id] // ouvre celle-là
513
+ editer(ch)
514
+ }
515
+
516
+ function editer(ch: ChoixVue) {
517
+ desactiverAjout.value = true
518
+
519
+ // Nettoyage d’un placeholder vide précédent (si on relance edit)
520
+ if (ch.nomVue !== '') supprimer({ _id: 'placeholder', nomVue: '', colonnes: [], defaut: false })
521
+
522
+ erreur.value = ''
523
+ nomVueEnCours.value = ch.nomVue
524
+ vueEnEdition.value = ch.nomVue
525
+ }
526
+
527
+ function accepterEdition() {
528
+ const nom = nomVueEnCours.value?.trim()
529
+
530
+ if (!nom) {
531
+ erreur.value = t('csqc.csqcDatatable.choixColonnes.nomVueRequis')
532
+ return
533
+ }
534
+
535
+ if (choix.value.some(({ nomVue }) => nomVue === nom)) {
536
+ erreur.value = t('csqc.csqcDatatable.choixColonnes.nomVueExiste')
537
+ return
538
+ }
539
+
540
+ const ch = choix.value.find(({ nomVue }) => nomVue === vueEnEdition.value)
541
+ if (!ch) return
542
+
543
+ const etaitSelectionnee = choixEstClique(ch)
544
+ ch.nomVue = nom
545
+
546
+ // Si c’était la vue active, on s’assure que la ref active reste valide
547
+ if (etaitSelectionnee) {
548
+ choixEnCours.value = ch
549
+ }
550
+
551
+ // Reset édition
552
+ vueEnEdition.value = ''
553
+ nomVueEnCours.value = ''
554
+ desactiverAjout.value = false
555
+ }
556
+
557
+ function annulerEdition() {
558
+ const ch = choix.value.find(({ nomVue }) => nomVue === vueEnEdition.value)
559
+ if (!ch) return
560
+
561
+ // Si c'était une nouvelle vue vide, on la retire
562
+ if (ch.nomVue === '') supprimer(ch)
563
+
564
+ vueEnEdition.value = ''
565
+ nomVueEnCours.value = ''
566
+ desactiverAjout.value = false
567
+ }
568
+
569
+ function selectionner(ch: ChoixVue) {
570
+ emit('selection', ch)
571
+ fermer()
572
+ }
573
+
574
+ function supprimer(ch: ChoixVue) {
575
+ if (choixEstClique(ch)) choixEnCours.value = null
576
+
577
+ const index = choix.value.findIndex(({ nomVue }) => nomVue === ch.nomVue)
578
+ if (index === -1) return
579
+
580
+ // Si on supprime le défaut, on transfère le défaut ailleurs
581
+ if (choix.value[index].defaut) {
582
+ const autre = choix.value.find(({ nomVue }) => nomVue !== ch.nomVue)
583
+ if (autre) autre.defaut = true
584
+ }
585
+
586
+ choix.value.splice(index, 1)
587
+ }
588
+
589
+ function mettreDefaut(ch: ChoixVue) {
590
+ const actuel = choix.value.find(v => v.defaut)
591
+ if (actuel) actuel.defaut = false
592
+
593
+ const cible = choix.value.find(v => v.nomVue === ch.nomVue)
594
+ if (cible) cible.defaut = true
595
+ }
596
+
597
+ function couleurDefaut(ch: ChoixVue) {
598
+ return ch.defaut ? '#daa520' : 'gray'
599
+ }
600
+
601
+ function choixEstClique(ch: ChoixVue) {
602
+ return ch.nomVue === choixEnCours.value?.nomVue
603
+ }
604
+
605
+ // Click row Vuetify (payload = { item })
606
+ function onClickRowChoix(_e: Event, payload: any) {
607
+ cliquer(choixOf(payload?.item ?? payload))
608
+ }
609
+
610
+ /**
611
+ * Sélection d’une vue :
612
+ * - recalcul de colonnesEnCours (selected d'abord, puis le reste)
613
+ * - nettoyage des colonnes "non disponibles" (si props.colonnes a changé)
614
+ */
615
+ function cliquer(ch: ChoixVue) {
616
+ const id = ch._id
617
+ if (!id) return
618
+
619
+ expandedChoixIds.value = expandedChoixIds.value[0] === id ? [] : [id]
620
+ }
621
+
622
+ // ─────────────────────────────────────────────────────────────────────────────
623
+ // Actions : colonnes
624
+ // ─────────────────────────────────────────────────────────────────────────────
625
+ function colonneEstClique(colonne: Colonne) {
626
+ if (!choixEnCours.value) return false
627
+ return choixEnCours.value.colonnes.includes(colonne.value)
628
+ }
629
+
630
+ function couleurColonneCliquee(colonne: Colonne) {
631
+ return colonneEstClique(colonne) ? 'primary' : 'gray'
632
+ }
633
+
634
+ function basculeColonneClique(colonne: Colonne) {
635
+ if (!choixEnCours.value) return
636
+
637
+ if (colonneEstClique(colonne)) {
638
+ choixEnCours.value.colonnes = choixEnCours.value.colonnes.filter(x => x !== colonne.value)
639
+ return
640
+ }
641
+
642
+ choixEnCours.value.colonnes.push(colonne.value)
643
+ }
644
+
645
+ // Liste des colonnes actuellement "cliquées" selon l’ordre affiché
646
+ const colonnesCliquees = computed(() => {
647
+ if (!choixEnCours.value) return []
648
+ return colonnesEnCours.value.filter(c => choixEnCours.value!.colonnes.includes(c.value)).map(c => c.value)
649
+ })
650
+
651
+ function changeOrdre(event: SortableEvent) {
652
+ if (event.oldIndex == null || event.newIndex == null) return
653
+
654
+ const moved = colonnesEnCours.value.splice(event.oldIndex, 1)[0]
655
+ colonnesEnCours.value.splice(event.newIndex, 0, moved)
656
+
657
+ calculOrdreColonnes()
658
+
659
+ // Après reorder, on sauvegarde la sélection dans le nouvel ordre visuel
660
+ if (choixEnCours.value) {
661
+ choixEnCours.value.colonnes = colonnesCliquees.value
662
+ }
663
+ }
664
+
665
+ function calculOrdreColonnes() {
666
+ for (let i = 0; i < colonnesEnCours.value.length; i += 1) {
667
+ colonnesEnCours.value[i].ordre = i + 1
668
+ }
669
+ }
670
+
671
+ // ─────────────────────────────────────────────────────────────────────────────
672
+ // axios
673
+ // ─────────────────────────────────────────────────────────────────────────────
674
+ async function sauvegarder() {
675
+ sauvegardeEnCours.value = true
676
+
677
+ try {
678
+ const res: any = await axios
679
+ .getAxios()
680
+ .post(`${props.urlbase}/api/ComposantUI/Colonnes/${props.formulaireId}/Identifiant/${props.identifiant}`, {
681
+ valeur: JSON.stringify(choix.value),
682
+ })
683
+
684
+ const payload = res?.data ?? res
685
+
686
+ // payload peut être un json string ou déjà un tableau
687
+ choix.value = typeof payload === 'string' ? (JSON.parse(payload) as ChoixVue[]) : (payload as ChoixVue[])
688
+
689
+ assurerIds(choix.value)
690
+
691
+ // Notifie le parent
692
+ emit('sauvegarde', choix.value)
693
+ if (choixEnCours.value) emit('selection', choixEnCours.value)
694
+
695
+ fermer()
696
+ } catch (e) {
697
+ erreur.value = String(e)
698
+ } finally {
699
+ sauvegardeEnCours.value = false
700
+ }
701
+ }
702
+
703
+ // ─────────────────────────────────────────────────────────────────────────────
704
+ // Scroll helpers
705
+ // ─────────────────────────────────────────────────────────────────────────────
706
+ function retourEnHaut(cible: string) {
707
+ goTo(0, { container: cible })
708
+ }
709
+
710
+ function defilerChoix(e: Event) {
711
+ const target = e.target as HTMLElement
712
+ choixRetourEnHaut.value = target.scrollTop >= 200
713
+ }
714
+
715
+ function ecouteDefilerChoix() {
716
+ const el = document.querySelector('#choixColonnes-choix > .v-data-table__wrapper')
717
+ if (!el) {
718
+ setTimeout(ecouteDefilerChoix, 100)
719
+ return
720
+ }
721
+ el.addEventListener('scroll', defilerChoix)
722
+ }
723
+
724
+ function defilerColonnes(e: Event) {
725
+ const target = e.target as HTMLElement
726
+ colonnesRetourEnHaut.value = target.scrollTop >= 200
727
+ }
728
+
729
+ function ecouteDefilerColonnes() {
730
+ nextTick(() => {
731
+ const el = document.querySelector('#choixColonnes-vue > .v-data-table__wrapper')
732
+ if (!el) {
733
+ setTimeout(ecouteDefilerColonnes, 100)
734
+ return
735
+ }
736
+ el.addEventListener('scroll', defilerColonnes)
737
+ })
738
+ }
739
+
740
+ // ─────────────────────────────────────────────────────────────────────────────
741
+ // Watchers
742
+ // ─────────────────────────────────────────────────────────────────────────────
743
+ watch(erreur, () => {
744
+ afficherErreur.value = erreur.value !== ''
745
+ })
746
+
747
+ watch(afficherErreur, v => {
748
+ if (v === false) erreur.value = ''
749
+ })
750
+
751
+ watch(
752
+ expandedChoixIds,
753
+ v => {
754
+ const id = v?.[0]
755
+
756
+ // Fermeture
757
+ if (!id) {
758
+ choixEnCours.value = null
759
+ colonnesEnCours.value = []
760
+ return
761
+ }
762
+
763
+ // Ouverture / changement
764
+ const ch = choix.value.find(x => x._id === id)
765
+ if (!ch) return
766
+
767
+ ouvrirVue(ch) // IMPORTANT: ouverture "force", pas toggle
768
+ },
769
+ { flush: 'post' }, // <-- clé du fix
770
+ )
771
+ watch(
772
+ expandedChoixIds,
773
+ v => {
774
+ if (v.length > 1) expandedChoixIds.value = [v[v.length - 1]] // garde le dernier
775
+ },
776
+ { flush: 'sync' },
777
+ )
778
+ </script>
779
+
780
+ <style scoped>
781
+ /* Layout */
782
+ .relative {
783
+ position: relative;
784
+ }
785
+
786
+ /* Boutons "retour en haut" */
787
+ .retourHautChoix {
788
+ bottom: 4px !important;
789
+ right: 4px;
790
+ }
791
+ .retourHautChoix.colonnes {
792
+ bottom: 8px !important;
793
+ right: 48px;
794
+ }
795
+
796
+ /* Scroll vertical des tables */
797
+ .limiteHauteurChoix > .v-data-table__wrapper {
798
+ max-height: min(80vh - 200px, 900px);
799
+ overflow-y: auto;
800
+ }
801
+ .limiteHauteurChoix.colonnes > .v-data-table__wrapper {
802
+ max-height: min(80vh - 294px, 600px);
803
+ }
804
+
805
+ /* Z-index du header table 1 */
806
+ #choixColonnes-choix > .v-data-table__wrapper > table > thead > tr > th {
807
+ z-index: 3;
808
+ }
809
+
810
+ /* Drag cursor */
811
+ .v-data-table.ordonable > .v-data-table__wrapper > table > tbody > tr {
812
+ cursor: move;
813
+ }
814
+
815
+ /* Icons */
816
+ .iconeSupprimer:hover {
817
+ color: red;
818
+ }
819
+ .v-icon.v-icon.v-icon--link.iconeSupprimer:hover {
820
+ color: red !important;
821
+ }
822
+ .iconeEditer:hover {
823
+ color: #095797;
824
+ }
825
+
826
+ .GouttiereSmall {
827
+ margin-left: 8px;
828
+ }
829
+ </style>