codevdesign 2.0.12 → 2.0.13

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