codevdesign 1.0.44 → 1.0.46

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,537 +1,537 @@
1
- <template>
2
- <div>
3
- <v-row v-if="barreHautAfficher">
4
- <slot name="ligne" />
5
- <!-- Affichage de la boite de recherche-->
6
- <v-col>
7
- <slot name="recherche"></slot>
8
- <Recherche
9
- v-bind="$attrs"
10
- :afficher="rechercheAfficher"
11
- :recherche-texte="rechercheTexte"
12
- :chargement="chargementListe"
13
- :recherche="recherche"
14
- :recherche-avancee="rechercheAvancee"
15
- :recherche-avancee-texte="rechercheAvanceeTexte"
16
- :recherche-avancee-largeur="rechercheAvanceeLargeur"
17
- :recherche-avancee-style="rechercheAvanceeStyle"
18
- class="flex-grow-1"
19
- @update:recherche="chargerRecherche"
20
- @panneau:etat="onPanelChange"
21
- >
22
- <template #milieu>
23
- <slot name="milieu" />
24
- </template>
25
-
26
- <template #droite>
27
- <slot name="droite" />
28
- <v-btn
29
- v-if="btnAjouter"
30
- color="primary"
31
- class="ml-1 float-right"
32
- @click.stop="ajouter"
33
- >
34
- {{ props.btnAjouterTexte ? props.btnAjouterTexte : $t('csqc.bouton.ajouter') }}
35
- </v-btn>
36
-
37
- <exportExcelComponent
38
- v-if="excel"
39
- :liste="filteredItems"
40
- :chargement-liste="chargementListe"
41
- :nom-fichier="excelNomFichier"
42
- class="mt-1 ml-1 float-right"
43
- />
44
- <v-icon
45
- v-if="permettreChoixColonnes"
46
- color="grisMoyen"
47
- class="mt-1 ml-1 float-right"
48
- @click.stop="ouvrirChoixColonnes"
49
- >
50
- mdi-table-edit
51
- </v-icon>
52
- </template>
53
- <template #rechercheAvanceeTitre>
54
- <slot name="rechercheAvanceeTitre" />
55
- </template>
56
- <template #rechercheAvanceeApresTitre>
57
- <slot name="rechercheAvanceeApresTitre" />
58
- </template>
59
- <template #rechercheAvancee>
60
- <slot name="rechercheAvancee" />
61
- </template>
62
- </Recherche>
63
- </v-col>
64
- </v-row>
65
-
66
- <!-- datatable-->
67
- <v-row>
68
- <v-col
69
- cols="12"
70
- class="d-flex ControlesDatatable flex-wrap"
71
- >
72
- <v-data-table
73
- ref="datatable"
74
- v-bind="$attrs"
75
- :headers="colonnesAffichees"
76
- :item-key="itemKey"
77
- :items="liste"
78
- :search="recherche"
79
- :loading="chargementListe"
80
- :hover="hover"
81
- :density="densite"
82
- :items-per-page="itemsParPage"
83
- :item-per-page-options="itemsParPageOptions"
84
- :sort-by="triDepart"
85
- :multi-sort="estMultiTriActif"
86
- @click:row="cliqueLigne"
87
- >
88
- <!-- utilisation des slots via le component parent-->
89
- <!-- eslint-disable-next-line -->
90
- <template
91
- v-for="(_, slot) of $slots"
92
- :key="slot"
93
- #[slot]="scope"
94
- >
95
- <slot
96
- :name="slot"
97
- v-bind="scope"
98
- />
99
- </template>
100
-
101
- <!-- boutons supprimer et modifier -->
102
- <!-- eslint-disable-next-line -->
103
- <template v-slot:item.action="{ item }">
104
- <slot name="actionsCustom"></slot>
105
- <v-icon
106
- v-if="props.btnSupprimer"
107
- size="large"
108
- class="iconeSupprimer float-right"
109
- @click.stop.prevent="ouvrirModaleSupprimer(item)"
110
- >
111
- mdi-delete
112
- </v-icon>
113
-
114
- <v-icon
115
- v-if="props.btnModifier"
116
- size="large"
117
- class="iconeEditer float-right"
118
- @click.stop.prevent="modifier(item)"
119
- >
120
- mdi-pencil
121
- </v-icon>
122
- </template>
123
- </v-data-table>
124
-
125
- <!-- Fenêtre de suppression -->
126
- <confirmation
127
- v-if="props.btnSupprimer"
128
- ref="modaleSupprimer"
129
- :texte="supprimerTexte"
130
- :titre="supprimerTitreTexte"
131
- :largeur="modaleSupprimerLargeur"
132
- @confirmer="supprimer"
133
- />
134
- <modale-choix
135
- ref="modaleChoix"
136
- v-if="permettreChoixColonnes"
137
- :urlbase="urlbase"
138
- :formulaire-id="formulaireId"
139
- :identifiant="identifiant"
140
- :colonnes="colonnesPourChoix"
141
- :choix-origine="choixOrigineNormalise"
142
- @selection="selectionChoix"
143
- @sauvegarde="sauvegardeChoix"
144
- />
145
- </v-col>
146
- </v-row>
147
- </div>
148
- </template>
149
- <script setup lang="ts">
150
- /* eslint-disable @typescript-eslint/no-explicit-any */
151
- import { computed, onMounted, ref, type PropType, type Slots } from 'vue'
152
- import { useI18n } from 'vue-i18n'
153
- import type { SortItem } from 'vuetify/lib/components/VDataTable/composables/sort.mjs'
154
-
155
- import Recherche from '../csqcRecherche.vue'
156
- import confirmation from '../csqcConfirmation.vue'
157
- import exportExcelComponent from './csqcTableExportExcel.vue'
158
- import ModaleChoix from './csqcTableModaleChoixColonnes.vue'
159
- import axios from '../../outils/appAxios'
160
- import Colonne from '../../modeles/composants/datatableColonne'
161
-
162
- type VueColonnes = {
163
- nomVue: string
164
- colonnes: string[]
165
- defaut?: boolean
166
- }
167
-
168
- const slots = defineSlots<Slots>()
169
- const emit = defineEmits(['ajouter', 'cliqueLigne', 'supprimer', 'modifier', 'donneesExportees', 'panneau:etat'])
170
-
171
- // ============================================================================
172
- // Props
173
- // ============================================================================
174
- const props = defineProps({
175
- barreHautAfficher: { type: Boolean, default: true },
176
-
177
- btnAjouter: { type: Boolean, default: true },
178
- btnAjouterTexte: { type: String, default: '' },
179
- btnModifier: { type: Boolean, default: true },
180
- btnSupprimer: { type: Boolean, default: true },
181
-
182
- chargementListe: { type: Boolean, default: false },
183
- operationEnCours: { type: Boolean, default: false },
184
-
185
- // Headers Vuetify
186
- colonnes: {
187
- type: Array as PropType<Colonne[]>,
188
- default: () => [],
189
- },
190
-
191
- densite: { type: String as PropType<'default' | 'comfortable' | 'compact'>, default: 'default' },
192
-
193
- excel: { type: Boolean, default: false },
194
- excelNomFichier: { type: String, default: 'csqc' },
195
-
196
- // Choix colonnes
197
- formulaireId: { type: Number, default: -1 },
198
- identifiant: { type: String, default: '' },
199
- urlbase: { type: String, default: '' },
200
- permettreChoixColonnes: { type: Boolean, default: false },
201
-
202
- // Table
203
- itemKey: { type: String, default: 'id' },
204
- itemsParPage: { type: Number, default: 10 },
205
- itemsParPageOptions: { type: Array, default: () => [10, 25, 30, -1] },
206
- liste: {
207
- type: Array as PropType<Record<string, any>[]>,
208
- default: () => [],
209
- },
210
- hover: { type: Boolean, default: false },
211
-
212
- // Recherche
213
- rechercheTexte: { type: String, default: '' },
214
- rechercheAfficher: { type: Boolean, default: true },
215
- rechercheAvancee: { type: Boolean, default: false },
216
- rechercheAvanceeTexte: { type: String, default: '' },
217
- rechercheAvanceeLargeur: { type: Number, default: 12 },
218
- rechercheAvanceeStyle: { type: String, default: '' },
219
-
220
- // Modale suppression
221
- modaleSupprimerChamp: { type: String, default: '' },
222
- modaleSupprimerTexte: { type: String, default: '' },
223
- modaleSupprimerTitre: { type: String, default: '' },
224
- modaleSupprimerLargeur: { type: String, default: '525px' },
225
-
226
- // Tri
227
- triDescDepart: { type: Boolean, default: false },
228
- triParDepart: {
229
- type: [Array, String] as PropType<string | string[] | SortItem[] | undefined>,
230
- default: undefined,
231
- },
232
- })
233
-
234
- const { t } = useI18n({ useScope: 'global' })
235
- const recherche = ref<string>('')
236
-
237
- // Modale suppression (on garde l’item sélectionné)
238
- const itemSelectionne = ref<any>(null)
239
-
240
- // ============================================================================
241
- // State - Choix de colonnes (vues sauvegardées) + sélection courante
242
- // ============================================================================
243
- const modaleChoix = ref<InstanceType<typeof ModaleChoix> | null>(null)
244
- const openChoixColonnes = ref(false)
245
- const choix = ref<VueColonnes[]>([]) // la liste des vues disponibles
246
- const vueActive = ref<VueColonnes | null>(null) // optionnel: la vue "appliquée"
247
- const valeurSelectionChoixColonnes = ref<string | null>(null) // nomVue sélectionné dans la modale / UI
248
-
249
- // ============================================================================
250
- // Charger le choix de colonnes sauvegardé s'il y a lieu
251
- // ============================================================================
252
- onMounted(async () => {
253
- if (!props.permettreChoixColonnes) return
254
-
255
- try {
256
- const res: any = await axios
257
- .getAxios()
258
- .get(`${props.urlbase}/api/ComposantUI/Colonnes/${props.formulaireId}/Identifiant/${props.identifiant}`)
259
-
260
- const payload = res?.data ?? res
261
-
262
- if (!payload) {
263
- choix.value = []
264
- return
265
- }
266
-
267
- choix.value = typeof payload === 'string' ? JSON.parse(payload) : payload
268
- } catch (e) {
269
- choix.value = []
270
- // console.debug(e)
271
- }
272
- })
273
-
274
- // ============================================================================
275
- // Computed - Normaliser pour la modale Choix colonne
276
- // ============================================================================
277
- const choixOrigineNormalise = computed(() =>
278
- (choix.value ?? []).map(c => ({
279
- _id: '',
280
- nomVue: c.nomVue,
281
- colonnes: c.colonnes ?? [],
282
- defaut: c.defaut ?? false,
283
- })),
284
- )
285
-
286
- // ============================================================================
287
- // Actions - Modale choix colonnes
288
- // ============================================================================
289
- function ouvrirChoixColonnes() {
290
- modaleChoix.value?.ouvrir()
291
- }
292
-
293
- function appliquerVue(vue: VueColonnes) {
294
- vueActive.value = vue
295
- openChoixColonnes.value = false
296
- }
297
-
298
- function selectionChoix(vue: VueColonnes) {
299
- // on garde une trace du nom sélectionné (pour retrouver la vue plus tard)
300
- valeurSelectionChoixColonnes.value = vue.nomVue
301
- appliquerVue(vue)
302
- }
303
-
304
- function sauvegardeChoix(vues: VueColonnes[]) {
305
- if (!props.permettreChoixColonnes) return
306
-
307
- // si la sélection courante n’existe plus, on reset
308
- if (
309
- valeurSelectionChoixColonnes.value != null &&
310
- vues.find(x => x.nomVue === valeurSelectionChoixColonnes.value) == null
311
- ) {
312
- valeurSelectionChoixColonnes.value = null
313
- }
314
-
315
- choix.value = vues
316
-
317
- // optionnel: appliquer automatiquement la vue par défaut
318
- const defaut = vues.find(v => v.defaut)
319
- if (defaut) appliquerVue(defaut)
320
- }
321
-
322
- // ============================================================================
323
- // Computed - Colonnes à envoyer à la modale (liste ordonnée + libellé)
324
- // ============================================================================
325
- const colonnesPourChoix = computed(() =>
326
- props.colonnes
327
- // on exclut l'action (souvent toujours visible) + colonnes "cachées"
328
- .filter(c => c.key && c.key !== 'action' && (c as any).align !== 'd-none')
329
- .map((c, idx) => ({
330
- title: (c as any).title ?? (c as any).text ?? String((c as any).key ?? (c as any).value),
331
- value: String((c as any).key ?? (c as any).value), // ← on force string
332
- ordre: idx + 1,
333
- })),
334
- )
335
-
336
- // ============================================================================
337
- // Computed - Headers réellement affichés par la table (selon le choix actif)
338
- // ============================================================================
339
- const colonnesAffichees = computed(() => {
340
- // 1) colonnes "disponibles" (on enlève celles explicitement cachées)
341
- const colonnesFiltre = props.colonnes.filter(c => (c as any).align !== 'd-none')
342
-
343
- // 2) si feature OFF ou pas de vues sauvegardées => on retourne tout
344
- if (!props.permettreChoixColonnes) return colonnesFiltre
345
- if (!choix.value?.length) return colonnesFiltre
346
-
347
- // 3) retrouver la vue sélectionnée (sinon la défaut)
348
- const choixSelection = choix.value.find(
349
- x => valeurSelectionChoixColonnes.value === x.nomVue || (!valeurSelectionChoixColonnes.value && x.defaut),
350
- )
351
- if (!choixSelection) return colonnesFiltre
352
-
353
- // 4) reconstruire la liste de headers dans l’ordre sauvegardé
354
- return choixSelection.colonnes.map(k => colonnesFiltre.find(c => String(c.key) === k)).filter(Boolean) as Colonne[]
355
- })
356
-
357
- // ============================================================================
358
- // Computed - Tri initial (Vuetify sort-by)
359
- // ============================================================================
360
- const triDepart = computed<SortItem[] | undefined>(() => {
361
- if (props.triParDepart == null) return undefined
362
-
363
- const ordre = props.triDescDepart ? 'desc' : 'asc'
364
- if (typeof props.triParDepart === 'string') return [{ key: props.triParDepart, order: ordre }]
365
-
366
- const retour: SortItem[] = []
367
- for (let i = 0; i < props.triParDepart.length; i += 1) {
368
- const tri = props.triParDepart[i]!
369
- if (typeof tri === 'string') retour.push({ key: tri, order: ordre })
370
- else retour.push(tri)
371
- }
372
- return retour
373
- })
374
-
375
- const estMultiTriActif = computed(() => {
376
- if (props.triParDepart == null) return false
377
- if (typeof props.triParDepart === 'string') return false
378
- return true
379
- })
380
-
381
- // ============================================================================
382
- // Computed - Export (filtrage local basé sur recherche)
383
- // ============================================================================
384
- const filteredItems = computed(() => {
385
- if (!recherche.value) return props.liste
386
- const q = recherche.value.toLowerCase()
387
- return props.liste.filter(item => Object.values(item).some(val => String(val).toLowerCase().includes(q)))
388
- })
389
-
390
- // ============================================================================
391
- // UI Actions - Recherche / Table events
392
- // ============================================================================
393
- function chargerRecherche(val: string) {
394
- recherche.value = val
395
- }
396
-
397
- function ajouter() {
398
- emit('ajouter')
399
- }
400
-
401
- function cliqueLigne(_e: Event, { item }: { item: any }) {
402
- emit('cliqueLigne', item)
403
- }
404
-
405
- function modifier(item: any) {
406
- emit('modifier', item)
407
- }
408
-
409
- // ============================================================================
410
- // Suppression - Texte + actions
411
- // ============================================================================
412
- const supprimerTexte = computed(() => {
413
- if (itemSelectionne.value == null) return ''
414
-
415
- if (props.modaleSupprimerTexte) return props.modaleSupprimerTexte
416
-
417
- return t('csqc.message.supprimerMessage', {
418
- nom: itemSelectionne?.value?.[props.modaleSupprimerChamp] ?? '',
419
- })
420
- })
421
-
422
- const supprimerTitreTexte = computed(() => props.modaleSupprimerTitre || t('csqc.message.supprimerTitre'))
423
-
424
- function ouvrirModaleSupprimer(item: any) {
425
- itemSelectionne.value = item
426
- emit('supprimer', itemSelectionne.value) // tu sembles gérer la modale ailleurs
427
- }
428
-
429
- function supprimer() {
430
- emit('supprimer', itemSelectionne.value)
431
- }
432
-
433
- // ============================================================================
434
- // UI - Panneau recherche avancée
435
- // ============================================================================
436
- function onPanelChange(val: boolean) {
437
- emit('panneau:etat', val)
438
- }
439
- </script>
440
-
441
- <style scoped lang="css">
442
- .ControlesDatatable {
443
- gap: 4px;
444
- }
445
-
446
- /* barre de recherche design QC*/
447
- .BarreRecherche {
448
- border-radius: 4px 0 0 4px !important;
449
- min-height: 40px;
450
- }
451
-
452
- .BarreRechercheBackIcone {
453
- border-right: 1px solid #808a9d;
454
- border-top: 1px solid #808a9d;
455
- border-bottom: 1px solid #808a9d;
456
- background-color: #095797 !important;
457
- height: 40px !important;
458
- width: 40px !important;
459
- max-width: 40px !important;
460
- min-width: 40px !important;
461
- padding-right: 0 !important;
462
- padding-left: 0 !important;
463
- border-radius: 0 4px 4px 0 !important;
464
- margin-left: -16px;
465
- }
466
-
467
- /* icone loupe */
468
- .BarreRechercheIcone {
469
- font-size: 34px !important;
470
- margin-left: 1px !important;
471
- margin-top: 2px !important;
472
- color: white !important;
473
- }
474
-
475
- /* datatable contour */
476
- .v-data-table {
477
- border: 1px solid #d3d3d3;
478
- border-radius: 5px;
479
- overflow: hidden;
480
- }
481
-
482
- /* hover */
483
- .v-data-table:hover {
484
- cursor: pointer !important;
485
- }
486
-
487
- /* datatable row */
488
- .v-data-table .v-table__wrapper > table > thead > tr > td,
489
- .v-data-table .v-table__wrapper > table > thead > tr th,
490
- .v-data-table .v-table__wrapper > table tbody > tr > td,
491
- .v-data-table .v-table__wrapper > table tbody > tr th {
492
- background-color: #d3d3d375 !important;
493
- }
494
-
495
- /* datatable header contour */
496
- .v-data-table .v-table__wrapper > table > thead > tr > th,
497
- .v-data-table .v-table__wrapper > table tbody > tr > th {
498
- background-color: white !important;
499
- border-bottom: 4px #223654 solid !important;
500
- }
501
-
502
- /* datatable header intérieur*/
503
- .v-data-table-header__content {
504
- background-color: white !important;
505
- color: #223654 !important;
506
- font-weight: 600;
507
- font-size: 15px;
508
- height: 46px;
509
- }
510
-
511
- /* datatable footer */
512
- .v-data-table-footer {
513
- background-color: white !important;
514
- border-top: 4px #223654 solid !important;
515
- }
516
-
517
- /* datatable row hover */
518
- .v-data-table .v-table__wrapper > table tbody > tr:hover > td,
519
- .v-data-table .v-table__wrapper > table tbody > tr:hover {
520
- background-color: #e0e0e0 !important;
521
- }
522
-
523
- .iconeSupprimer:hover {
524
- color: red;
525
- }
526
- .v-icon.v-icon.v-icon--link.iconeSupprimer:hover {
527
- color: red !important;
528
- }
529
- .iconeEditer:hover {
530
- color: #095797;
531
- }
532
-
533
- /* hover icone à droite du ajouter*/
534
- .text-grisMoyen:hover {
535
- color: #095797 !important;
536
- }
537
- </style>
1
+ <template>
2
+ <div>
3
+ <v-row v-if="barreHautAfficher">
4
+ <slot name="ligne" />
5
+ <!-- Affichage de la boite de recherche-->
6
+ <v-col>
7
+ <slot name="recherche"></slot>
8
+ <Recherche
9
+ v-bind="$attrs"
10
+ :afficher="rechercheAfficher"
11
+ :recherche-texte="rechercheTexte"
12
+ :chargement="chargementListe"
13
+ :recherche="recherche"
14
+ :recherche-avancee="rechercheAvancee"
15
+ :recherche-avancee-texte="rechercheAvanceeTexte"
16
+ :recherche-avancee-largeur="rechercheAvanceeLargeur"
17
+ :recherche-avancee-style="rechercheAvanceeStyle"
18
+ class="flex-grow-1"
19
+ @update:recherche="chargerRecherche"
20
+ @panneau:etat="onPanelChange"
21
+ >
22
+ <template #milieu>
23
+ <slot name="milieu" />
24
+ </template>
25
+
26
+ <template #droite>
27
+ <slot name="droite" />
28
+ <v-btn
29
+ v-if="btnAjouter"
30
+ color="primary"
31
+ class="ml-1 float-right"
32
+ @click.stop="ajouter"
33
+ >
34
+ {{ props.btnAjouterTexte ? props.btnAjouterTexte : $t('csqc.bouton.ajouter') }}
35
+ </v-btn>
36
+
37
+ <exportExcelComponent
38
+ v-if="excel"
39
+ :liste="filteredItems"
40
+ :chargement-liste="chargementListe"
41
+ :nom-fichier="excelNomFichier"
42
+ class="mt-1 ml-1 float-right"
43
+ />
44
+ <v-icon
45
+ v-if="permettreChoixColonnes"
46
+ color="grisMoyen"
47
+ class="mt-1 ml-1 float-right"
48
+ @click.stop="ouvrirChoixColonnes"
49
+ >
50
+ mdi-table-edit
51
+ </v-icon>
52
+ </template>
53
+ <template #rechercheAvanceeTitre>
54
+ <slot name="rechercheAvanceeTitre" />
55
+ </template>
56
+ <template #rechercheAvanceeApresTitre>
57
+ <slot name="rechercheAvanceeApresTitre" />
58
+ </template>
59
+ <template #rechercheAvancee>
60
+ <slot name="rechercheAvancee" />
61
+ </template>
62
+ </Recherche>
63
+ </v-col>
64
+ </v-row>
65
+
66
+ <!-- datatable-->
67
+ <v-row>
68
+ <v-col
69
+ cols="12"
70
+ class="d-flex ControlesDatatable flex-wrap"
71
+ >
72
+ <v-data-table
73
+ ref="datatable"
74
+ v-bind="$attrs"
75
+ :headers="colonnesAffichees"
76
+ :item-key="itemKey"
77
+ :items="liste"
78
+ :search="recherche"
79
+ :loading="chargementListe"
80
+ :hover="hover"
81
+ :density="densite"
82
+ :items-per-page="itemsParPage"
83
+ :item-per-page-options="itemsParPageOptions"
84
+ :sort-by="triDepart"
85
+ :multi-sort="estMultiTriActif"
86
+ @click:row="cliqueLigne"
87
+ >
88
+ <!-- utilisation des slots via le component parent-->
89
+ <!-- eslint-disable-next-line -->
90
+ <template
91
+ v-for="(_, slot) of $slots"
92
+ :key="slot"
93
+ #[slot]="scope"
94
+ >
95
+ <slot
96
+ :name="slot"
97
+ v-bind="scope"
98
+ />
99
+ </template>
100
+
101
+ <!-- boutons supprimer et modifier -->
102
+ <!-- eslint-disable-next-line -->
103
+ <template v-slot:item.action="{ item }">
104
+ <slot name="actionsCustom"></slot>
105
+ <v-icon
106
+ v-if="props.btnSupprimer"
107
+ size="large"
108
+ class="iconeSupprimer float-right"
109
+ @click.stop.prevent="ouvrirModaleSupprimer(item)"
110
+ >
111
+ mdi-delete
112
+ </v-icon>
113
+
114
+ <v-icon
115
+ v-if="props.btnModifier"
116
+ size="large"
117
+ class="iconeEditer float-right"
118
+ @click.stop.prevent="modifier(item)"
119
+ >
120
+ mdi-pencil
121
+ </v-icon>
122
+ </template>
123
+ </v-data-table>
124
+
125
+ <!-- Fenêtre de suppression -->
126
+ <confirmation
127
+ v-if="props.btnSupprimer"
128
+ ref="modaleSupprimer"
129
+ :texte="supprimerTexte"
130
+ :titre="supprimerTitreTexte"
131
+ :largeur="modaleSupprimerLargeur"
132
+ @confirmer="supprimer"
133
+ />
134
+ <modale-choix
135
+ ref="modaleChoix"
136
+ v-if="permettreChoixColonnes"
137
+ :urlbase="urlbase"
138
+ :formulaire-id="formulaireId"
139
+ :identifiant="identifiant"
140
+ :colonnes="colonnesPourChoix"
141
+ :choix-origine="choixOrigineNormalise"
142
+ @selection="selectionChoix"
143
+ @sauvegarde="sauvegardeChoix"
144
+ />
145
+ </v-col>
146
+ </v-row>
147
+ </div>
148
+ </template>
149
+ <script setup lang="ts">
150
+ /* eslint-disable @typescript-eslint/no-explicit-any */
151
+ import { computed, onMounted, ref, type PropType, type Slots } from 'vue'
152
+ import { useI18n } from 'vue-i18n'
153
+ import type { SortItem } from 'vuetify/lib/components/VDataTable/composables/sort.mjs'
154
+
155
+ import Recherche from '../csqcRecherche.vue'
156
+ import confirmation from '../csqcConfirmation.vue'
157
+ import exportExcelComponent from './csqcTableExportExcel.vue'
158
+ import ModaleChoix from './csqcTableModaleChoixColonnes.vue'
159
+ import axios from '../../outils/appAxios'
160
+ import Colonne from '../../modeles/composants/datatableColonne'
161
+
162
+ type VueColonnes = {
163
+ nomVue: string
164
+ colonnes: string[]
165
+ defaut?: boolean
166
+ }
167
+
168
+ const slots = defineSlots<Slots>()
169
+ const emit = defineEmits(['ajouter', 'cliqueLigne', 'supprimer', 'modifier', 'donneesExportees', 'panneau:etat'])
170
+
171
+ // ============================================================================
172
+ // Props
173
+ // ============================================================================
174
+ const props = defineProps({
175
+ barreHautAfficher: { type: Boolean, default: true },
176
+
177
+ btnAjouter: { type: Boolean, default: true },
178
+ btnAjouterTexte: { type: String, default: '' },
179
+ btnModifier: { type: Boolean, default: true },
180
+ btnSupprimer: { type: Boolean, default: true },
181
+
182
+ chargementListe: { type: Boolean, default: false },
183
+ operationEnCours: { type: Boolean, default: false },
184
+
185
+ // Headers Vuetify
186
+ colonnes: {
187
+ type: Array as PropType<Colonne[]>,
188
+ default: () => [],
189
+ },
190
+
191
+ densite: { type: String as PropType<'default' | 'comfortable' | 'compact'>, default: 'default' },
192
+
193
+ excel: { type: Boolean, default: false },
194
+ excelNomFichier: { type: String, default: 'csqc' },
195
+
196
+ // Choix colonnes
197
+ formulaireId: { type: Number, default: -1 },
198
+ identifiant: { type: String, default: '' },
199
+ urlbase: { type: String, default: '' },
200
+ permettreChoixColonnes: { type: Boolean, default: false },
201
+
202
+ // Table
203
+ itemKey: { type: String, default: 'id' },
204
+ itemsParPage: { type: Number, default: 10 },
205
+ itemsParPageOptions: { type: Array, default: () => [10, 25, 30, -1] },
206
+ liste: {
207
+ type: Array as PropType<Record<string, any>[]>,
208
+ default: () => [],
209
+ },
210
+ hover: { type: Boolean, default: false },
211
+
212
+ // Recherche
213
+ rechercheTexte: { type: String, default: '' },
214
+ rechercheAfficher: { type: Boolean, default: true },
215
+ rechercheAvancee: { type: Boolean, default: false },
216
+ rechercheAvanceeTexte: { type: String, default: '' },
217
+ rechercheAvanceeLargeur: { type: Number, default: 12 },
218
+ rechercheAvanceeStyle: { type: String, default: '' },
219
+
220
+ // Modale suppression
221
+ modaleSupprimerChamp: { type: String, default: '' },
222
+ modaleSupprimerTexte: { type: String, default: '' },
223
+ modaleSupprimerTitre: { type: String, default: '' },
224
+ modaleSupprimerLargeur: { type: String, default: '525px' },
225
+
226
+ // Tri
227
+ triDescDepart: { type: Boolean, default: false },
228
+ triParDepart: {
229
+ type: [Array, String] as PropType<string | string[] | SortItem[] | undefined>,
230
+ default: undefined,
231
+ },
232
+ })
233
+
234
+ const { t } = useI18n({ useScope: 'global' })
235
+ const recherche = ref<string>('')
236
+
237
+ // Modale suppression (on garde l’item sélectionné)
238
+ const itemSelectionne = ref<any>(null)
239
+
240
+ // ============================================================================
241
+ // State - Choix de colonnes (vues sauvegardées) + sélection courante
242
+ // ============================================================================
243
+ const modaleChoix = ref<InstanceType<typeof ModaleChoix> | null>(null)
244
+ const openChoixColonnes = ref(false)
245
+ const choix = ref<VueColonnes[]>([]) // la liste des vues disponibles
246
+ const vueActive = ref<VueColonnes | null>(null) // optionnel: la vue "appliquée"
247
+ const valeurSelectionChoixColonnes = ref<string | null>(null) // nomVue sélectionné dans la modale / UI
248
+
249
+ // ============================================================================
250
+ // Charger le choix de colonnes sauvegardé s'il y a lieu
251
+ // ============================================================================
252
+ onMounted(async () => {
253
+ if (!props.permettreChoixColonnes) return
254
+
255
+ try {
256
+ const res: any = await axios
257
+ .getAxios()
258
+ .get(`${props.urlbase}/api/ComposantUI/Colonnes/${props.formulaireId}/Identifiant/${props.identifiant}`)
259
+
260
+ const payload = res?.data ?? res
261
+
262
+ if (!payload) {
263
+ choix.value = []
264
+ return
265
+ }
266
+
267
+ choix.value = typeof payload === 'string' ? JSON.parse(payload) : payload
268
+ } catch (e) {
269
+ choix.value = []
270
+ // console.debug(e)
271
+ }
272
+ })
273
+
274
+ // ============================================================================
275
+ // Computed - Normaliser pour la modale Choix colonne
276
+ // ============================================================================
277
+ const choixOrigineNormalise = computed(() =>
278
+ (choix.value ?? []).map(c => ({
279
+ _id: '',
280
+ nomVue: c.nomVue,
281
+ colonnes: c.colonnes ?? [],
282
+ defaut: c.defaut ?? false,
283
+ })),
284
+ )
285
+
286
+ // ============================================================================
287
+ // Actions - Modale choix colonnes
288
+ // ============================================================================
289
+ function ouvrirChoixColonnes() {
290
+ modaleChoix.value?.ouvrir()
291
+ }
292
+
293
+ function appliquerVue(vue: VueColonnes) {
294
+ vueActive.value = vue
295
+ openChoixColonnes.value = false
296
+ }
297
+
298
+ function selectionChoix(vue: VueColonnes) {
299
+ // on garde une trace du nom sélectionné (pour retrouver la vue plus tard)
300
+ valeurSelectionChoixColonnes.value = vue.nomVue
301
+ appliquerVue(vue)
302
+ }
303
+
304
+ function sauvegardeChoix(vues: VueColonnes[]) {
305
+ if (!props.permettreChoixColonnes) return
306
+
307
+ // si la sélection courante n’existe plus, on reset
308
+ if (
309
+ valeurSelectionChoixColonnes.value != null &&
310
+ vues.find(x => x.nomVue === valeurSelectionChoixColonnes.value) == null
311
+ ) {
312
+ valeurSelectionChoixColonnes.value = null
313
+ }
314
+
315
+ choix.value = vues
316
+
317
+ // optionnel: appliquer automatiquement la vue par défaut
318
+ const defaut = vues.find(v => v.defaut)
319
+ if (defaut) appliquerVue(defaut)
320
+ }
321
+
322
+ // ============================================================================
323
+ // Computed - Colonnes à envoyer à la modale (liste ordonnée + libellé)
324
+ // ============================================================================
325
+ const colonnesPourChoix = computed(() =>
326
+ props.colonnes
327
+ // on exclut l'action (souvent toujours visible) + colonnes "cachées"
328
+ .filter(c => c.key && c.key !== 'action' && (c as any).align !== 'd-none')
329
+ .map((c, idx) => ({
330
+ title: (c as any).title ?? (c as any).text ?? String((c as any).key ?? (c as any).value),
331
+ value: String((c as any).key ?? (c as any).value), // ← on force string
332
+ ordre: idx + 1,
333
+ })),
334
+ )
335
+
336
+ // ============================================================================
337
+ // Computed - Headers réellement affichés par la table (selon le choix actif)
338
+ // ============================================================================
339
+ const colonnesAffichees = computed(() => {
340
+ // 1) colonnes "disponibles" (on enlève celles explicitement cachées)
341
+ const colonnesFiltre = props.colonnes.filter(c => (c as any).align !== 'd-none')
342
+
343
+ // 2) si feature OFF ou pas de vues sauvegardées => on retourne tout
344
+ if (!props.permettreChoixColonnes) return colonnesFiltre
345
+ if (!choix.value?.length) return colonnesFiltre
346
+
347
+ // 3) retrouver la vue sélectionnée (sinon la défaut)
348
+ const choixSelection = choix.value.find(
349
+ x => valeurSelectionChoixColonnes.value === x.nomVue || (!valeurSelectionChoixColonnes.value && x.defaut),
350
+ )
351
+ if (!choixSelection) return colonnesFiltre
352
+
353
+ // 4) reconstruire la liste de headers dans l’ordre sauvegardé
354
+ return choixSelection.colonnes.map(k => colonnesFiltre.find(c => String(c.key) === k)).filter(Boolean) as Colonne[]
355
+ })
356
+
357
+ // ============================================================================
358
+ // Computed - Tri initial (Vuetify sort-by)
359
+ // ============================================================================
360
+ const triDepart = computed<SortItem[] | undefined>(() => {
361
+ if (props.triParDepart == null) return undefined
362
+
363
+ const ordre = props.triDescDepart ? 'desc' : 'asc'
364
+ if (typeof props.triParDepart === 'string') return [{ key: props.triParDepart, order: ordre }]
365
+
366
+ const retour: SortItem[] = []
367
+ for (let i = 0; i < props.triParDepart.length; i += 1) {
368
+ const tri = props.triParDepart[i]!
369
+ if (typeof tri === 'string') retour.push({ key: tri, order: ordre })
370
+ else retour.push(tri)
371
+ }
372
+ return retour
373
+ })
374
+
375
+ const estMultiTriActif = computed(() => {
376
+ if (props.triParDepart == null) return false
377
+ if (typeof props.triParDepart === 'string') return false
378
+ return true
379
+ })
380
+
381
+ // ============================================================================
382
+ // Computed - Export (filtrage local basé sur recherche)
383
+ // ============================================================================
384
+ const filteredItems = computed(() => {
385
+ if (!recherche.value) return props.liste
386
+ const q = recherche.value.toLowerCase()
387
+ return props.liste.filter(item => Object.values(item).some(val => String(val).toLowerCase().includes(q)))
388
+ })
389
+
390
+ // ============================================================================
391
+ // UI Actions - Recherche / Table events
392
+ // ============================================================================
393
+ function chargerRecherche(val: string) {
394
+ recherche.value = val
395
+ }
396
+
397
+ function ajouter() {
398
+ emit('ajouter')
399
+ }
400
+
401
+ function cliqueLigne(_e: Event, { item }: { item: any }) {
402
+ emit('cliqueLigne', item)
403
+ }
404
+
405
+ function modifier(item: any) {
406
+ emit('modifier', item)
407
+ }
408
+
409
+ // ============================================================================
410
+ // Suppression - Texte + actions
411
+ // ============================================================================
412
+ const supprimerTexte = computed(() => {
413
+ if (itemSelectionne.value == null) return ''
414
+
415
+ if (props.modaleSupprimerTexte) return props.modaleSupprimerTexte
416
+
417
+ return t('csqc.message.supprimerMessage', {
418
+ nom: itemSelectionne?.value?.[props.modaleSupprimerChamp] ?? '',
419
+ })
420
+ })
421
+
422
+ const supprimerTitreTexte = computed(() => props.modaleSupprimerTitre || t('csqc.message.supprimerTitre'))
423
+
424
+ function ouvrirModaleSupprimer(item: any) {
425
+ itemSelectionne.value = item
426
+ emit('supprimer', itemSelectionne.value) // tu sembles gérer la modale ailleurs
427
+ }
428
+
429
+ function supprimer() {
430
+ emit('supprimer', itemSelectionne.value)
431
+ }
432
+
433
+ // ============================================================================
434
+ // UI - Panneau recherche avancée
435
+ // ============================================================================
436
+ function onPanelChange(val: boolean) {
437
+ emit('panneau:etat', val)
438
+ }
439
+ </script>
440
+
441
+ <style scoped lang="css">
442
+ .ControlesDatatable {
443
+ gap: 4px;
444
+ }
445
+
446
+ /* barre de recherche design QC*/
447
+ .BarreRecherche {
448
+ border-radius: 4px 0 0 4px !important;
449
+ min-height: 40px;
450
+ }
451
+
452
+ .BarreRechercheBackIcone {
453
+ border-right: 1px solid #808a9d;
454
+ border-top: 1px solid #808a9d;
455
+ border-bottom: 1px solid #808a9d;
456
+ background-color: #095797 !important;
457
+ height: 40px !important;
458
+ width: 40px !important;
459
+ max-width: 40px !important;
460
+ min-width: 40px !important;
461
+ padding-right: 0 !important;
462
+ padding-left: 0 !important;
463
+ border-radius: 0 4px 4px 0 !important;
464
+ margin-left: -16px;
465
+ }
466
+
467
+ /* icone loupe */
468
+ .BarreRechercheIcone {
469
+ font-size: 34px !important;
470
+ margin-left: 1px !important;
471
+ margin-top: 2px !important;
472
+ color: white !important;
473
+ }
474
+
475
+ /* datatable contour */
476
+ .v-data-table {
477
+ border: 1px solid #d3d3d3;
478
+ border-radius: 5px;
479
+ overflow: hidden;
480
+ }
481
+
482
+ /* hover */
483
+ .v-data-table:hover {
484
+ cursor: pointer !important;
485
+ }
486
+
487
+ /* datatable row */
488
+ .v-data-table .v-table__wrapper > table > thead > tr > td,
489
+ .v-data-table .v-table__wrapper > table > thead > tr th,
490
+ .v-data-table .v-table__wrapper > table tbody > tr > td,
491
+ .v-data-table .v-table__wrapper > table tbody > tr th {
492
+ background-color: #d3d3d375 !important;
493
+ }
494
+
495
+ /* datatable header contour */
496
+ .v-data-table .v-table__wrapper > table > thead > tr > th,
497
+ .v-data-table .v-table__wrapper > table tbody > tr > th {
498
+ background-color: white !important;
499
+ border-bottom: 4px #223654 solid !important;
500
+ }
501
+
502
+ /* datatable header intérieur*/
503
+ .v-data-table-header__content {
504
+ background-color: white !important;
505
+ color: #223654 !important;
506
+ font-weight: 600;
507
+ font-size: 15px;
508
+ height: 46px;
509
+ }
510
+
511
+ /* datatable footer */
512
+ .v-data-table-footer {
513
+ background-color: white !important;
514
+ border-top: 4px #223654 solid !important;
515
+ }
516
+
517
+ /* datatable row hover */
518
+ .v-data-table .v-table__wrapper > table tbody > tr:hover > td,
519
+ .v-data-table .v-table__wrapper > table tbody > tr:hover {
520
+ background-color: #e0e0e0 !important;
521
+ }
522
+
523
+ .iconeSupprimer:hover {
524
+ color: red;
525
+ }
526
+ .v-icon.v-icon.v-icon--link.iconeSupprimer:hover {
527
+ color: red !important;
528
+ }
529
+ .iconeEditer:hover {
530
+ color: #095797;
531
+ }
532
+
533
+ /* hover icone à droite du ajouter*/
534
+ .text-grisMoyen:hover {
535
+ color: #095797 !important;
536
+ }
537
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevdesign",
3
- "version": "1.0.44",
3
+ "version": "1.0.46",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",
@@ -13,12 +13,14 @@
13
13
  "build": "vite build"
14
14
  },
15
15
  "dependencies": {
16
- "vuetify": "^3.10.2",
17
- "vue-i18n": "^11.0.0",
18
- "@e965/xlsx": "^0.20.3",
19
- "tinymce": "^8.0.2",
20
- "tinymce-i18n": "^25.9.1",
21
- "@tinymce/tinymce-vue": "^6.3.0"
16
+ "vuetify": "^3.11.2",
17
+ "vue-i18n": "^11.2.1",
18
+ "tinymce": "^8.3.2",
19
+ "tinymce-i18n": "^26.1.12",
20
+ "@tinymce/tinymce-vue": "^6.3.0",
21
+ "vue": "^3.5.18",
22
+ "sortablejs": "^1.15.6",
23
+ "@e965/xlsx": "^0.20.3"
22
24
  },
23
25
  "devDependencies": {
24
26
  "@types/node": "^22.13.5",
@@ -26,7 +28,5 @@
26
28
  "typescript": "^5.8.3",
27
29
  "vite": "^7.0.0"
28
30
  },
29
- "peerDependencies": {
30
- "vue": "^3.5.0"
31
- }
31
+ "peerDependencies": {}
32
32
  }