codevdesign 1.0.43 → 1.0.44

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,565 +1,791 @@
1
1
  <template>
2
2
  <csqc-modale
3
3
  ref="modale"
4
- :titre="$tc('csqc-table.choixColonnes.titre')"
5
- :afficher-ok="false"
6
- :afficher-annuler="!sauvegardeEnCours"
7
- width="90vw"
8
- max-width="4000px"
9
- @fermer="fermer"
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
10
  >
11
+ <!-- Erreurs -->
11
12
  <v-alert
12
13
  v-model="afficherErreur"
13
14
  type="error"
14
15
  closable
15
- >{{ erreur }}</v-alert
16
16
  >
17
+ {{ erreur }}
18
+ </v-alert>
19
+
17
20
  <v-row>
18
21
  <v-col
19
22
  cols="12"
20
23
  class="pa-0 relative"
21
24
  >
25
+ <!-- Retour en haut (table des vues) -->
22
26
  <v-btn
23
27
  v-show="choixRetourEnHaut"
24
28
  class="BarreRechercheBackIcone retourHautChoix"
25
- location="bottom right"
26
- absolute
29
+ icon
27
30
  color="primary"
31
+ style="position: absolute"
28
32
  @click="retourEnHaut('#choixColonnes-choix > .v-data-table__wrapper')"
29
33
  >
30
34
  <v-icon>mdi-arrow-up</v-icon>
31
35
  </v-btn>
32
- <v-data-table
33
- id="choixColonnes-choix"
34
- v-model:expanded="choixEnCours"
35
- class="limiteHauteurChoix"
36
- :headers="colonnesChoix"
37
- item-key="nomVue"
38
- :items="choix"
39
- single-expand
40
- disable-pagination
41
- hide-default-footer
42
- fixed-header
43
- @click:row="cliquer"
36
+
37
+ <v-card
38
+ rounded
39
+ class="pr-0"
44
40
  >
45
- <template #header.action>
46
- <v-btn
47
- color="primary"
48
- class="float-right"
49
- size="small"
50
- :disabled="desactiverAjout"
51
- @click.stop="ajouter"
52
- >
53
- {{ $tc('csqc-table.button.ajouter') }}
54
- </v-btn>
55
- </template>
56
-
57
- <template #expanded-item="{ headers }">
58
- <td
59
- :colspan="headers.length"
60
- class="pa-0 ma-0"
61
- >
62
- <v-row
63
- class="pa-0 ma-0"
64
- no-gutters
41
+ <!-- TABLE 1 : Liste des vues -->
42
+ <v-data-table
43
+ id="choixColonnes-choix"
44
+ class="limiteHauteurChoix"
45
+ :headers="colonnesChoix"
46
+ :items="choix"
47
+ :item-value="row => choixOf(row)._id"
48
+ v-model:expanded="expandedChoixIds"
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"
65
64
  >
66
- <v-col
67
- cols="12"
65
+ {{ t('csqc.bouton.ajouter') }}
66
+ </v-btn>
67
+ </template>
68
+ <template v-slot: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"
68
88
  class="pa-0 ma-0"
69
89
  >
70
- <v-btn
71
- v-show="colonnesRetourEnHaut"
72
- class="BarreRechercheBackIcone retourHautChoix colonnes"
73
- location="bottom right"
74
- absolute
75
- color="primary"
76
- @click="retourEnHaut('#choixColonnes-vue > .v-data-table__wrapper')"
90
+ <v-row
91
+ class="pa-0 ma-0"
92
+ no-gutters
77
93
  >
78
- <v-icon>mdi-arrow-up</v-icon>
79
- </v-btn>
80
- <v-data-table
81
- id="choixColonnes-vue"
82
- v-model:sort-by="triColonnesChoix"
83
- v-sortable-data-table
84
- class="limiteHauteurChoix colonnes mt-1 mb-4 ml-2 ordonable"
85
- :headers="colonnesChoixColonne"
86
- :items="colonnesEnCours"
87
- item-key="value"
88
- hide-default-footer
89
- disable-pagination
90
- fixed-header
91
- :style="`margin-right: ${90 - $vuetify.breakpoint.scrollBarWidth}px;`"
92
- @sorted="changeOrdre"
93
- >
94
- <template
95
- v-if="choixEnCours[0]?.colonnes.length <= 0"
96
- #header.action
94
+ <v-col
95
+ cols="12"
96
+ class="pa-0 ma-0"
97
97
  >
98
- <v-tooltip
99
- location="left"
100
- color="warning"
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')"
101
106
  >
102
- <template #activator="{ props }">
103
- <v-icon
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
+ class="limiteHauteurChoix colonnes mt-1 mb-4 ordonable"
114
+ :headers="colonnesChoixColonne"
115
+ :items="colonnesEnCours"
116
+ hover
117
+ item-value="value"
118
+ hide-default-footer
119
+ :items-per-page="-1"
120
+ fixed-header
121
+ :sort-by="triColonnesChoix"
122
+ v-sortable-data-table
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"
104
130
  color="warning"
105
- v-bind="props"
106
- >mdi-alert</v-icon
107
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>
108
142
  </template>
109
- {{ $tc('csqc-table.choixColonnes.activezUneColonne') }}
110
- </v-tooltip>
111
- </template>
112
143
 
113
- <template #item.action="{ item }">
114
- <v-btn
115
- class="mr-1"
116
- icon
117
- @click.stop="basculeColonneClique(item)"
118
- >
119
- <v-icon :color="couleurColonneCliquee(item)">{{
120
- colonneEstClique(item) ? 'mdi-eye' : 'mdi-eye-off'
121
- }}</v-icon>
122
- </v-btn>
123
- </template>
124
- <template #item.text="{ item }">{{ item.text ? item.text : item.value }}</template>
125
- </v-data-table>
126
- </v-col>
127
- </v-row>
128
- </td>
129
- </template>
130
-
131
- <template #item.nomVue="{ item }">
132
- <v-text-field
133
- v-if="item.nomVue === vueEnEdition"
134
- v-model.trim="nomVueEnCours"
135
- :label="$tc('csqc-table.choixColonnes.nomVue')"
136
- single-line
137
- density="compact"
138
- autofocus
139
- class="py-0 my-0"
140
- :rules="regles.nomVue"
141
- hide-details
142
- @keydown.enter="accepterEdition"
143
- @keydown.esc.stop="annulerEdition"
144
- ></v-text-field>
145
- <span
146
- v-else
147
- :class="{ 'error--text': !item.nomVue }"
148
- >
149
- {{ item.nomVue || $tc('csqc-table.choixColonnes.nomVueRequis') }}
150
- </span>
151
- </template>
152
- <template #item.action="{ item }">
153
- <template v-if="item.nomVue === vueEnEdition">
154
- <v-btn
155
- icon
156
- @click.stop.prevent="annulerEdition"
157
- ><v-icon class="iconeSupprimer">mdi-window-close</v-icon></v-btn
158
- >
159
- <v-btn
160
- icon
161
- @click.stop.prevent="accepterEdition"
162
- ><v-icon class="couleurHover">mdi-check</v-icon></v-btn
163
- >
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>
164
163
  </template>
165
- <template v-else>
166
- <v-btn
167
- icon
168
- @click.stop.prevent="selectionner(item)"
169
- ><v-icon class="couleurHover">mdi-eye</v-icon></v-btn
170
- >
171
- <v-btn
172
- icon
173
- @click.stop.prevent="mettreDefaut(item)"
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 }"
174
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">
175
191
  <v-icon
176
- class="couleurHover"
177
- :color="couleurDefaut(item)"
178
- >mdi-star</v-icon
192
+ class="iconeSupprimer GouttiereSmall"
193
+ @click.stop.prevent="annulerEdition"
179
194
  >
180
- </v-btn>
181
- <v-btn
182
- icon
183
- @click.stop.prevent="editer(item)"
184
- ><v-icon class="couleurHover">mdi-pencil</v-icon></v-btn
185
- >
186
- <v-btn
187
- icon
188
- @click.stop.prevent="supprimer(item)"
189
- ><v-icon class="iconeSupprimer">mdi-delete</v-icon></v-btn
190
- >
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>
191
252
  </template>
192
- </template>
193
- </v-data-table>
253
+ </v-data-table>
254
+ </v-card>
194
255
  </v-col>
195
256
  </v-row>
196
-
197
- <template #actions>
198
- <v-btn
199
- color="primary"
200
- class="elevation-0"
201
- :disabled="!formValide"
202
- :loading="sauvegardeEnCours"
203
- @click.stop="sauvegarder"
204
- >
205
- {{ $tc('csqc-table.choixColonnes.ok') }}
206
- </v-btn>
207
- </template>
208
257
  </csqc-modale>
209
258
  </template>
210
259
 
211
- <script>
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'
212
271
  import Sortable from 'sortablejs'
213
- import axios from '@/outils/appAxios'
214
- import CsqcModale from '../CsqcDialogue/csqc-boite-dialogue.vue'
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)?.raw ?? (item as T)
308
+ }
215
309
 
216
- export default {
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({
217
351
  directives: {
218
352
  sortableDataTable: {
219
- bind(el, binding, vnode) {
220
- const options = {
353
+ mounted(el: HTMLElement, _binding: any, vnode: any) {
354
+ const tbody = el.getElementsByTagName('tbody')?.[0]
355
+ if (!tbody) return
356
+
357
+ Sortable.create(tbody, {
221
358
  animation: 150,
222
- onUpdate(event) {
223
- vnode.child.$emit('sorted', event)
359
+ onUpdate(event: SortableEvent) {
360
+ if (event.oldIndex == null || event.newIndex == null) return
361
+ vnode?.component?.emit?.('sorted', event)
224
362
  },
225
- }
226
- Sortable.create(el.getElementsByTagName('tbody')[0], options)
363
+ })
227
364
  },
228
365
  },
229
366
  },
230
- components: {
231
- 'csqc-modale': CsqcModale,
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<Array<{ 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%',
232
455
  },
233
- props: {
234
- urlbase: {
235
- required: true,
236
- type: String,
237
- },
238
- formulaireId: {
239
- required: true,
240
- type: Number,
241
- },
242
- identifiant: {
243
- required: true,
244
- type: String,
245
- },
246
- colonnes: {
247
- required: true,
248
- type: Array,
249
- },
250
- choixOrigine: {
251
- required: true,
252
- type: Array,
253
- },
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%',
254
462
  },
255
- data() {
256
- return {
257
- erreur: '',
258
- afficherErreur: false,
259
- choixEnCours: [],
260
- choix: [],
261
- colonnesEnCours: [],
262
- triColonnesChoix: ['ordre'],
263
- sauvegardeEnCours: false,
264
- choixRetourEnHaut: false,
265
- colonnesRetourEnHaut: false,
266
- nomVueEnCours: '',
267
- vueEnEdition: '',
268
- desactiverAjout: false,
269
- regles: {
270
- nomVue: [
271
- v => (v && v.trim().length >= 1) || this.$tc('csqc-table.choixColonnes.nomVueRequis'),
272
- v =>
273
- !this.choix.some(({ nomVue }) => v.trim() === nomVue) ||
274
- this.$tc('csqc-table.choixColonnes.nomVueExiste'),
275
- ],
276
- },
277
- }
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,
278
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
+ }
279
502
 
280
- computed: {
281
- colonnesCliquees() {
282
- if (this.choixEnCours.length <= 0) return []
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
+ }
283
517
 
284
- const retour = this.colonnesEnCours
285
- .filter(c => this.choixEnCours[0].colonnes.some(x => x === c.value))
286
- .map(c => c.value)
287
- return retour
288
- },
289
- colonnesChoixColonne() {
290
- const cols = [
291
- {
292
- text: this.$tc('csqc-table.choixColonnes.ordre'),
293
- align: 'start',
294
- sortable: false,
295
- value: 'ordre',
296
- width: '5%',
297
- },
298
- {
299
- text: this.$tc('csqc-table.choixColonnes.nomColonne', this.colonnesEnCours.length),
300
- align: 'start',
301
- sortable: false,
302
- value: 'text',
303
- width: '85%',
304
- },
305
- {
306
- value: 'action',
307
- sortable: false,
308
- width: '10%',
309
- align: 'right',
310
- },
311
- ]
312
- return cols
313
- },
314
- colonnesChoix() {
315
- return [
316
- {
317
- text: this.$tc('csqc-table.choixColonnes.vues', this.choix.length),
318
- value: 'nomVue',
319
- width: '70%',
320
- align: 'start',
321
- },
322
- {
323
- value: 'action',
324
- width: '30%',
325
- align: 'right',
326
- sortable: false,
327
- },
328
- ]
329
- },
330
- formValide() {
331
- return !this.choix.some(vue => vue.nomVue === '' || vue.colonnes.length <= 0)
332
- },
333
- },
518
+ function editer(ch: ChoixVue) {
519
+ desactiverAjout.value = true
334
520
 
335
- watch: {
336
- erreur() {
337
- this.afficherErreur = this.erreur !== ''
338
- },
339
- afficherErreur() {
340
- if (this.afficherErreur === false) {
341
- this.erreur = ''
342
- }
343
- },
344
- },
345
- methods: {
346
- ouvrir() {
347
- this.choix = JSON.parse(JSON.stringify(this.choixOrigine))
348
- this.$refs.modale.ouvrir()
349
- this.ecouteDefilerChoix()
350
- },
521
+ // Nettoyage d’un placeholder vide précédent (si on relance edit)
522
+ if (ch.nomVue !== '') supprimer({ _id: 'placeholder', nomVue: '', colonnes: [], defaut: false })
351
523
 
352
- fermer() {
353
- this.sauvegardeEnCours = false
354
- this.desactiverAjout = false
355
- this.vueEnEdition = ''
356
- this.choixEnCours = []
357
- this.colonnesEnCours = []
358
- this.erreur = ''
359
- this.$refs.modale.fermer()
360
- },
524
+ erreur.value = ''
525
+ nomVueEnCours.value = ch.nomVue
526
+ vueEnEdition.value = ch.nomVue
527
+ }
361
528
 
362
- ajouter() {
363
- const ch = {
364
- nomVue: '',
365
- colonnes: [],
366
- defaut: this.choix.length === 0,
367
- }
368
- this.choix.push(ch)
529
+ function accepterEdition() {
530
+ const nom = nomVueEnCours.value?.trim()
369
531
 
370
- this.cliquer(ch)
371
- this.editer(ch)
372
- },
373
- editer(ch) {
374
- this.desactiverAjout = true
375
- if (ch.nomVue !== '') this.supprimer({ nomVue: '' })
532
+ if (!nom) {
533
+ erreur.value = t('csqc.csqcDatatable.choixColonnes.nomVueRequis')
534
+ return
535
+ }
376
536
 
377
- this.erreur = ''
378
- this.nomVueEnCours = ch.nomVue
379
- this.vueEnEdition = ch.nomVue
380
- },
381
- accepterEdition() {
382
- if (!this.nomVueEnCours) {
383
- this.erreur = this.$tc('csqc-table.choixColonnes.nomVueRequis')
384
- return
385
- }
386
-
387
- if (this.choix.some(({ nomVue }) => nomVue === this.nomVueEnCours)) {
388
- this.erreur = this.$tc('csqc-table.choixColonnes.nomVueExiste')
389
- return
390
- }
391
-
392
- const ch = this.choix.find(({ nomVue }) => nomVue === this.vueEnEdition)
393
- const choixClique = this.choixEstClique(ch)
394
-
395
- ch.nomVue = this.nomVueEnCours
396
- if (choixClique) this.choixEnCours = [ch]
397
- this.vueEnEdition = ''
398
- this.nomVueEnCours = ''
399
- this.desactiverAjout = false
400
- },
401
- annulerEdition() {
402
- const ch = this.choix.find(({ nomVue }) => nomVue === this.vueEnEdition)
403
- if (ch.nomVue === '') this.supprimer(ch)
404
- this.vueEnEdition = ''
405
- this.nomVueEnCours = ''
406
- this.desactiverAjout = false
407
- },
537
+ if (choix.value.some(({ nomVue }) => nomVue === nom)) {
538
+ erreur.value = t('csqc.csqcDatatable.choixColonnes.nomVueExiste')
539
+ return
540
+ }
408
541
 
409
- selectionner(ch) {
410
- this.$emit('selection', ch)
411
- this.fermer()
412
- },
542
+ const ch = choix.value.find(({ nomVue }) => nomVue === vueEnEdition.value)
543
+ if (!ch) return
413
544
 
414
- cliquer(ch) {
415
- if (this.choixEnCours.length > 0) this.retourEnHaut('#choixColonnes-vue > .v-data-table__wrapper')
416
- if (this.choixEstClique(ch)) {
417
- this.choixEnCours = []
418
- this.colonnesEnCours = []
419
- return
420
- }
421
-
422
- this.colonnesEnCours = JSON.parse(JSON.stringify(this.colonnes)) // Toujours commencer avec les colonnes par défaut
423
- this.choixEnCours = [ch]
424
- const tempCols = []
425
- const colsPlusDispo = []
426
-
427
- this.choixEnCours[0].colonnes.forEach(colonne => {
428
- const tCol = this.colonnesEnCours.find(c => c.value === colonne)
429
- if (tCol == null)
430
- colsPlusDispo.push(colonne) // Ne supprime pas dans la collection lors son itération
431
- else tempCols.push(tCol)
432
- })
545
+ const etaitSelectionnee = choixEstClique(ch)
546
+ ch.nomVue = nom
433
547
 
434
- // Nettoyer la vue au besoin (effectif seulement si l'utilisateur sauve les choix)
435
- if (colsPlusDispo.length > 0) {
436
- this.choixEnCours[0].colonnes = this.choixEnCours[0].colonnes.filter(
437
- c => !colsPlusDispo.some(cpd => c === cpd),
438
- )
439
- }
440
-
441
- this.colonnesEnCours = tempCols.concat(
442
- this.colonnesEnCours.filter(c => !this.choixEnCours[0].colonnes.some(cch => cch === c.value)),
443
- )
444
- this.calculOrdreColonnes()
445
- this.ecouteDefilerColonnes()
446
- },
548
+ // Si c’était la vue active, on s’assure que la ref active reste valide
549
+ if (etaitSelectionnee) {
550
+ choixEnCours.value = ch
551
+ }
447
552
 
448
- choixEstClique(ch) {
449
- return ch.nomVue === this.choixEnCours[0]?.nomVue
450
- },
553
+ // Reset édition
554
+ vueEnEdition.value = ''
555
+ nomVueEnCours.value = ''
556
+ desactiverAjout.value = false
557
+ }
451
558
 
452
- mettreDefaut(ch) {
453
- this.choix.find(({ defaut }) => defaut).defaut = false
454
- this.choix.find(({ nomVue }) => nomVue === ch.nomVue).defaut = true
455
- },
456
- couleurDefaut(ch) {
457
- if (ch.defaut === true) {
458
- return '#daa520'
459
- }
559
+ function annulerEdition() {
560
+ const ch = choix.value.find(({ nomVue }) => nomVue === vueEnEdition.value)
561
+ if (!ch) return
460
562
 
461
- return 'gray'
462
- },
563
+ // Si c'était une nouvelle vue vide, on la retire
564
+ if (ch.nomVue === '') supprimer(ch)
463
565
 
464
- colonneEstClique(colonne) {
465
- if (this.choixEnCours.length <= 0) {
466
- return false
467
- }
566
+ vueEnEdition.value = ''
567
+ nomVueEnCours.value = ''
568
+ desactiverAjout.value = false
569
+ }
468
570
 
469
- return this.choixEnCours[0].colonnes.some(x => x === colonne.value)
470
- },
471
- couleurColonneCliquee(colonne) {
472
- if (this.colonneEstClique(colonne)) return 'primary'
473
- return 'gray'
474
- },
475
- basculeColonneClique(colonne) {
476
- if (this.colonneEstClique(colonne)) {
477
- this.choixEnCours[0].colonnes = this.choixEnCours[0].colonnes.filter(x => x !== colonne.value)
478
- return
479
- }
571
+ function selectionner(ch: ChoixVue) {
572
+ emit('selection', ch)
573
+ fermer()
574
+ }
480
575
 
481
- this.choixEnCours[0].colonnes.push(colonne.value)
482
- },
576
+ function supprimer(ch: ChoixVue) {
577
+ if (choixEstClique(ch)) choixEnCours.value = null
483
578
 
484
- changeOrdre(event) {
485
- const movedItem = this.colonnesEnCours.splice(event.oldIndex, 1)[0]
486
- this.colonnesEnCours.splice(event.newIndex, 0, movedItem)
487
- this.calculOrdreColonnes()
488
- this.choixEnCours[0].colonnes = this.colonnesCliquees
489
- },
490
- calculOrdreColonnes() {
491
- for (let i = 0; i < this.colonnesEnCours.length; i += 1) {
492
- this.colonnesEnCours[i].ordre = 1 + i
493
- }
494
- },
579
+ const index = choix.value.findIndex(({ nomVue }) => nomVue === ch.nomVue)
580
+ if (index === -1) return
495
581
 
496
- supprimer(ch) {
497
- if (this.choixEstClique(ch)) this.choixEnCours = []
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
+ }
498
587
 
499
- const index = this.choix.findIndex(({ nomVue }) => nomVue === ch.nomVue)
500
- if (index === -1) {
501
- return
502
- }
588
+ choix.value.splice(index, 1)
589
+ }
503
590
 
504
- if (this.choix[index].defaut) {
505
- const nonDefaut = this.choix.find(({ nomVue }) => nomVue !== ch.nomVue)
506
- if (nonDefaut) {
507
- nonDefaut.defaut = true
508
- }
509
- }
591
+ function mettreDefaut(ch: ChoixVue) {
592
+ const actuel = choix.value.find(v => v.defaut)
593
+ if (actuel) actuel.defaut = false
510
594
 
511
- this.choix.splice(index, 1)
512
- },
595
+ const cible = choix.value.find(v => v.nomVue === ch.nomVue)
596
+ if (cible) cible.defaut = true
597
+ }
513
598
 
514
- // ENREGISTREMENT
515
- sauvegarder() {
516
- axios
517
- .post(`${this.urlbase}/api/ComposantUI/Colonnes/${this.formulaireId}/Identifiant/${this.identifiant}`, {
518
- valeur: JSON.stringify(this.choix),
519
- })
520
- .then(({ data }) => {
521
- this.choix = JSON.parse(data.resultat)
522
- this.$emit('sauvegarde', this.choix)
523
- if (this.choixEnCours.length > 0) this.$emit('selection', this.choixEnCours[0])
524
- this.fermer()
525
- })
526
- .catch(e => {
527
- this.erreur = e
528
- })
529
- },
599
+ function couleurDefaut(ch: ChoixVue) {
600
+ return ch.defaut ? '#daa520' : 'gray'
601
+ }
530
602
 
531
- retourEnHaut(cible) {
532
- this.$vuetify.goTo(0, { container: cible })
533
- },
534
- defilerChoix(e) {
535
- this.choixRetourEnHaut = e.target.scrollTop >= 200
536
- },
537
- ecouteDefilerChoix() {
538
- const vdtwChoix = document.querySelector('#choixColonnes-choix > .v-data-table__wrapper')
539
- if (vdtwChoix == null) {
540
- setTimeout(this.ecouteDefilerChoix.bind(this), 100)
541
- return
542
- }
543
- vdtwChoix.addEventListener('scroll', this.defilerChoix.bind(this))
544
- },
545
- defilerColonnes(e) {
546
- this.colonnesRetourEnHaut = e.target.scrollTop >= 200
547
- },
548
- ecouteDefilerColonnes() {
549
- const vdtwCols = document.querySelector('#choixColonnes-vue > .v-data-table__wrapper')
550
- if (vdtwCols == null) {
551
- setTimeout(this.ecouteDefilerColonnes.bind(this), 100)
552
- return
553
- }
554
- vdtwCols.addEventListener('scroll', this.defilerColonnes.bind(this))
555
- },
556
- },
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
+ })
557
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
+ )
558
780
  </script>
781
+
559
782
  <style scoped>
783
+ /* Layout */
560
784
  .relative {
561
785
  position: relative;
562
786
  }
787
+
788
+ /* Boutons "retour en haut" */
563
789
  .retourHautChoix {
564
790
  bottom: 4px !important;
565
791
  right: 4px;
@@ -568,19 +794,38 @@
568
794
  bottom: 8px !important;
569
795
  right: 48px;
570
796
  }
571
- </style>
572
- <style>
797
+
798
+ /* Scroll vertical des tables */
573
799
  .limiteHauteurChoix > .v-data-table__wrapper {
574
800
  max-height: min(80vh - 200px, 900px);
575
801
  overflow-y: auto;
576
802
  }
577
- #choixColonnes-choix > .v-data-table__wrapper > table > thead > tr > th {
578
- z-index: 3;
579
- }
580
803
  .limiteHauteurChoix.colonnes > .v-data-table__wrapper {
581
804
  max-height: min(80vh - 294px, 600px);
582
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 */
583
813
  .v-data-table.ordonable > .v-data-table__wrapper > table > tbody > tr {
584
814
  cursor: move;
585
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
+ }
586
831
  </style>