codevdesign 1.0.70 → 1.0.71
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.
- package/composants/csqcAide.vue +55 -55
- package/composants/csqcAlerteErreur.vue +87 -87
- package/composants/csqcChaise/chaiseConteneur.vue +372 -372
- package/composants/csqcChaise/chaiseItem.vue +54 -54
- package/composants/csqcConfirmation.vue +75 -75
- package/composants/csqcEditeurTexteRiche.vue +378 -378
- package/composants/csqcEntete.vue +229 -229
- package/composants/csqcImportCSV.vue +125 -125
- package/composants/csqcSnackbar.vue +207 -207
- package/composants/csqcSwitch.vue +220 -220
- package/composants/csqcTable/csqcTable.vue +671 -671
- package/composants/csqcTexteBilingue.vue +175 -175
- package/composants/csqcTiroir.vue +195 -156
- package/composants/gabarit/csqcMenu.vue +281 -281
- package/editeur.ts +1 -1
- package/importCSV.ts +1 -1
- package/index.ts +80 -80
- package/modeles/composants/csqcMenuModele.ts +18 -18
- package/modeles/composants/datatableColonne.ts +31 -31
- package/outils/appAxios.ts +113 -113
- package/package.json +1 -1
|
@@ -1,671 +1,671 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<v-row v-if="barreHautAfficher">
|
|
4
|
-
<slot name="ligne" />
|
|
5
|
-
<!-- Affichage de la boite de recherche-->
|
|
6
|
-
<v-col>
|
|
7
|
-
<slot name="recherche"></slot>
|
|
8
|
-
<Recherche
|
|
9
|
-
v-bind="$attrs"
|
|
10
|
-
:afficher="rechercheAfficher"
|
|
11
|
-
:recherche-texte="rechercheTexte"
|
|
12
|
-
:chargement="chargementListe"
|
|
13
|
-
:recherche="recherche"
|
|
14
|
-
:recherche-avancee="rechercheAvancee"
|
|
15
|
-
:recherche-avancee-texte="rechercheAvanceeTexte"
|
|
16
|
-
:recherche-avancee-largeur="rechercheAvanceeLargeur"
|
|
17
|
-
:recherche-avancee-style="rechercheAvanceeStyle"
|
|
18
|
-
class="flex-grow-1"
|
|
19
|
-
@update:recherche="chargerRecherche"
|
|
20
|
-
@panneau:etat="onPanelChange"
|
|
21
|
-
>
|
|
22
|
-
<template #milieu>
|
|
23
|
-
<slot name="milieu" />
|
|
24
|
-
</template>
|
|
25
|
-
|
|
26
|
-
<template #droite>
|
|
27
|
-
<slot name="droite" />
|
|
28
|
-
<v-btn
|
|
29
|
-
v-if="btnAjouter"
|
|
30
|
-
color="primary"
|
|
31
|
-
class="ml-1 float-right"
|
|
32
|
-
@click.stop="ajouter"
|
|
33
|
-
>
|
|
34
|
-
{{ props.btnAjouterTexte ? props.btnAjouterTexte : $t('csqc.bouton.ajouter') }}
|
|
35
|
-
</v-btn>
|
|
36
|
-
|
|
37
|
-
<exportExcelComponent
|
|
38
|
-
v-if="excel"
|
|
39
|
-
:liste="filteredItems"
|
|
40
|
-
:chargement-liste="chargementListe"
|
|
41
|
-
:nom-fichier="excelNomFichier"
|
|
42
|
-
class="mt-1 ml-1 float-right"
|
|
43
|
-
/>
|
|
44
|
-
<v-icon
|
|
45
|
-
v-if="permettreChoixColonnes"
|
|
46
|
-
color="grisMoyen"
|
|
47
|
-
class="mt-1 ml-1 float-right"
|
|
48
|
-
@click.stop="ouvrirChoixColonnes"
|
|
49
|
-
>
|
|
50
|
-
mdi-table-edit
|
|
51
|
-
</v-icon>
|
|
52
|
-
</template>
|
|
53
|
-
<template #rechercheAvanceeTitre>
|
|
54
|
-
<slot name="rechercheAvanceeTitre" />
|
|
55
|
-
</template>
|
|
56
|
-
<template #rechercheAvanceeApresTitre>
|
|
57
|
-
<slot name="rechercheAvanceeApresTitre" />
|
|
58
|
-
</template>
|
|
59
|
-
<template #rechercheAvancee>
|
|
60
|
-
<slot name="rechercheAvancee" />
|
|
61
|
-
</template>
|
|
62
|
-
</Recherche>
|
|
63
|
-
</v-col>
|
|
64
|
-
</v-row>
|
|
65
|
-
|
|
66
|
-
<!-- datatable-->
|
|
67
|
-
<v-row>
|
|
68
|
-
<v-col
|
|
69
|
-
cols="12"
|
|
70
|
-
class="d-flex ControlesDatatable flex-wrap"
|
|
71
|
-
>
|
|
72
|
-
<v-data-table
|
|
73
|
-
ref="datatable"
|
|
74
|
-
v-bind="$attrs"
|
|
75
|
-
:headers="colonnesAffichees"
|
|
76
|
-
:item-key="itemKey"
|
|
77
|
-
:items="listeEffective"
|
|
78
|
-
:search="recherche"
|
|
79
|
-
:loading="chargementListe"
|
|
80
|
-
:hover="hover"
|
|
81
|
-
:density="densite"
|
|
82
|
-
:items-per-page="itemsParPage"
|
|
83
|
-
:item-per-page-options="itemsParPageOptions"
|
|
84
|
-
:sort-by="triDepart"
|
|
85
|
-
:multi-sort="estMultiTriActif"
|
|
86
|
-
@click:row="cliqueLigne"
|
|
87
|
-
>
|
|
88
|
-
<!-- utilisation des slots via le component parent-->
|
|
89
|
-
<!-- eslint-disable-next-line -->
|
|
90
|
-
<template
|
|
91
|
-
v-for="(_, slot) of $slots"
|
|
92
|
-
:key="slot"
|
|
93
|
-
#[slot]="scope"
|
|
94
|
-
>
|
|
95
|
-
<slot
|
|
96
|
-
:name="slot"
|
|
97
|
-
v-bind="scope"
|
|
98
|
-
/>
|
|
99
|
-
</template>
|
|
100
|
-
|
|
101
|
-
<!-- Icône ancre (drag & drop) -->
|
|
102
|
-
<template
|
|
103
|
-
v-if="estEnModeModificateurOrdre"
|
|
104
|
-
#item.ancre
|
|
105
|
-
>
|
|
106
|
-
<v-icon class="ancre-drag">mdi-drag-vertical</v-icon>
|
|
107
|
-
</template>
|
|
108
|
-
|
|
109
|
-
<!-- boutons supprimer et modifier -->
|
|
110
|
-
<!-- eslint-disable-next-line -->
|
|
111
|
-
<template v-slot:item.action="{ item }">
|
|
112
|
-
<slot name="actionsCustom"></slot>
|
|
113
|
-
<v-icon
|
|
114
|
-
v-if="props.btnSupprimer"
|
|
115
|
-
size="large"
|
|
116
|
-
class="iconeSupprimer float-right"
|
|
117
|
-
@click.stop.prevent="ouvrirModaleSupprimer(item)"
|
|
118
|
-
>
|
|
119
|
-
mdi-delete
|
|
120
|
-
</v-icon>
|
|
121
|
-
|
|
122
|
-
<v-icon
|
|
123
|
-
v-if="props.btnModifier"
|
|
124
|
-
size="large"
|
|
125
|
-
class="iconeEditer float-right"
|
|
126
|
-
@click.stop.prevent="modifier(item)"
|
|
127
|
-
>
|
|
128
|
-
mdi-pencil
|
|
129
|
-
</v-icon>
|
|
130
|
-
</template>
|
|
131
|
-
</v-data-table>
|
|
132
|
-
|
|
133
|
-
<!-- Fenêtre de suppression -->
|
|
134
|
-
<confirmation
|
|
135
|
-
v-if="props.btnSupprimer"
|
|
136
|
-
ref="modaleSupprimer"
|
|
137
|
-
:texte="supprimerTexte"
|
|
138
|
-
:titre="supprimerTitreTexte"
|
|
139
|
-
:largeur="modaleSupprimerLargeur"
|
|
140
|
-
@confirmer="supprimer"
|
|
141
|
-
/>
|
|
142
|
-
<modale-choix
|
|
143
|
-
ref="modaleChoix"
|
|
144
|
-
v-if="permettreChoixColonnes"
|
|
145
|
-
:urlbase="urlbase"
|
|
146
|
-
:formulaire-id="formulaireId"
|
|
147
|
-
:identifiant="identifiant"
|
|
148
|
-
:colonnes="colonnesPourChoix"
|
|
149
|
-
:choix-origine="choixOrigineNormalise"
|
|
150
|
-
@selection="selectionChoix"
|
|
151
|
-
@sauvegarde="sauvegardeChoix"
|
|
152
|
-
/>
|
|
153
|
-
</v-col>
|
|
154
|
-
</v-row>
|
|
155
|
-
</div>
|
|
156
|
-
</template>
|
|
157
|
-
<script setup lang="ts">
|
|
158
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
159
|
-
import { computed, onBeforeUnmount, nextTick, onMounted, ref, watch, type PropType, type Slots } from 'vue'
|
|
160
|
-
import { useI18n } from 'vue-i18n'
|
|
161
|
-
import type { SortItem } from 'vuetify/lib/components/VDataTable/composables/sort.mjs'
|
|
162
|
-
|
|
163
|
-
import Recherche from '../csqcRecherche.vue'
|
|
164
|
-
import confirmation from '../csqcConfirmation.vue'
|
|
165
|
-
import exportExcelComponent from './csqcTableExportExcel.vue'
|
|
166
|
-
import ModaleChoix from './csqcTableModaleChoixColonnes.vue'
|
|
167
|
-
import axios from '../../outils/appAxios'
|
|
168
|
-
import Colonne from '../../modeles/composants/datatableColonne'
|
|
169
|
-
import Sortable from 'sortablejs'
|
|
170
|
-
|
|
171
|
-
type VueColonnes = {
|
|
172
|
-
nomVue: string
|
|
173
|
-
colonnes: string[]
|
|
174
|
-
defaut?: boolean
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const slots = defineSlots<Slots>()
|
|
178
|
-
const emit = defineEmits([
|
|
179
|
-
'ajouter',
|
|
180
|
-
'cliqueLigne',
|
|
181
|
-
'supprimer',
|
|
182
|
-
'modifier',
|
|
183
|
-
'donneesExportees',
|
|
184
|
-
'panneau:etat',
|
|
185
|
-
'ordreRangeeModifie',
|
|
186
|
-
])
|
|
187
|
-
|
|
188
|
-
// ============================================================================
|
|
189
|
-
// Props
|
|
190
|
-
// ============================================================================
|
|
191
|
-
const props = defineProps({
|
|
192
|
-
barreHautAfficher: { type: Boolean, default: true },
|
|
193
|
-
|
|
194
|
-
btnAjouter: { type: Boolean, default: true },
|
|
195
|
-
btnAjouterTexte: { type: String, default: '' },
|
|
196
|
-
btnModifier: { type: Boolean, default: true },
|
|
197
|
-
btnSupprimer: { type: Boolean, default: true },
|
|
198
|
-
|
|
199
|
-
chargementListe: { type: Boolean, default: false },
|
|
200
|
-
operationEnCours: { type: Boolean, default: false },
|
|
201
|
-
|
|
202
|
-
// Headers Vuetify
|
|
203
|
-
colonnes: {
|
|
204
|
-
type: Array as PropType<Colonne[]>,
|
|
205
|
-
default: () => [],
|
|
206
|
-
},
|
|
207
|
-
|
|
208
|
-
densite: { type: String as PropType<'default' | 'comfortable' | 'compact'>, default: 'default' },
|
|
209
|
-
|
|
210
|
-
excel: { type: Boolean, default: false },
|
|
211
|
-
excelNomFichier: { type: String, default: 'csqc' },
|
|
212
|
-
|
|
213
|
-
// Choix colonnes
|
|
214
|
-
formulaireId: { type: Number, default: -1 },
|
|
215
|
-
identifiant: { type: String, default: '' },
|
|
216
|
-
urlbase: { type: String, default: '' },
|
|
217
|
-
permettreChoixColonnes: { type: Boolean, default: false },
|
|
218
|
-
|
|
219
|
-
// Table
|
|
220
|
-
itemKey: { type: String, default: 'id' },
|
|
221
|
-
itemsParPage: { type: Number, default: 10 },
|
|
222
|
-
itemsParPageOptions: { type: Array, default: () => [10, 25, 30, -1] },
|
|
223
|
-
liste: {
|
|
224
|
-
type: Array as PropType<Record<string, any>[]>,
|
|
225
|
-
default: () => [],
|
|
226
|
-
},
|
|
227
|
-
hover: { type: Boolean, default: false },
|
|
228
|
-
|
|
229
|
-
// Recherche
|
|
230
|
-
rechercheTexte: { type: String, default: '' },
|
|
231
|
-
rechercheAfficher: { type: Boolean, default: true },
|
|
232
|
-
rechercheAvancee: { type: Boolean, default: false },
|
|
233
|
-
rechercheAvanceeTexte: { type: String, default: '' },
|
|
234
|
-
rechercheAvanceeLargeur: { type: Number, default: 12 },
|
|
235
|
-
rechercheAvanceeStyle: { type: String, default: '' },
|
|
236
|
-
|
|
237
|
-
// Modale suppression
|
|
238
|
-
modaleSupprimerChamp: { type: String, default: '' },
|
|
239
|
-
modaleSupprimerTexte: { type: String, default: '' },
|
|
240
|
-
modaleSupprimerTitre: { type: String, default: '' },
|
|
241
|
-
modaleSupprimerLargeur: { type: String, default: '525px' },
|
|
242
|
-
|
|
243
|
-
// Tri
|
|
244
|
-
triDescDepart: { type: Boolean, default: false },
|
|
245
|
-
triParDepart: {
|
|
246
|
-
type: [Array, String] as PropType<string | string[] | SortItem[] | undefined>,
|
|
247
|
-
default: undefined,
|
|
248
|
-
},
|
|
249
|
-
|
|
250
|
-
// Ordre manuel (drag & drop)
|
|
251
|
-
modificateurOrdreChamp: { type: String, default: undefined },
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
const { t } = useI18n({ useScope: 'global' })
|
|
255
|
-
const recherche = ref<string>('')
|
|
256
|
-
|
|
257
|
-
// Modale suppression (on garde l’item sélectionné)
|
|
258
|
-
const itemSelectionne = ref<any>(null)
|
|
259
|
-
|
|
260
|
-
// ============================================================================
|
|
261
|
-
// State - Choix de colonnes (vues sauvegardées) + sélection courante
|
|
262
|
-
// ============================================================================
|
|
263
|
-
const datatable = ref<any>(null)
|
|
264
|
-
const listeInterne = ref<Record<string, any>[]>([])
|
|
265
|
-
const modaleChoix = ref<InstanceType<typeof ModaleChoix> | null>(null)
|
|
266
|
-
const openChoixColonnes = ref(false)
|
|
267
|
-
const choix = ref<VueColonnes[]>([]) // la liste des vues disponibles
|
|
268
|
-
const vueActive = ref<VueColonnes | null>(null) // optionnel: la vue "appliquée"
|
|
269
|
-
const valeurSelectionChoixColonnes = ref<string | null>(null) // nomVue sélectionné dans la modale / UI
|
|
270
|
-
|
|
271
|
-
// ============================================================================
|
|
272
|
-
// Charger le choix de colonnes sauvegardé s'il y a lieu
|
|
273
|
-
// ============================================================================
|
|
274
|
-
onMounted(async () => {
|
|
275
|
-
if (!props.permettreChoixColonnes) return
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
const res: any = await axios
|
|
279
|
-
.getAxios()
|
|
280
|
-
.get(`${props.urlbase}/api/ComposantUI/Colonnes/${props.formulaireId}/Identifiant/${props.identifiant}`)
|
|
281
|
-
|
|
282
|
-
const payload = res?.data ?? res
|
|
283
|
-
|
|
284
|
-
if (!payload) {
|
|
285
|
-
choix.value = []
|
|
286
|
-
return
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
choix.value = typeof payload === 'string' ? JSON.parse(payload) : payload
|
|
290
|
-
} catch (e) {
|
|
291
|
-
choix.value = []
|
|
292
|
-
// console.debug(e)
|
|
293
|
-
}
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
// ============================================================================
|
|
297
|
-
// Computed - Normaliser pour la modale Choix colonne
|
|
298
|
-
// ============================================================================
|
|
299
|
-
const choixOrigineNormalise = computed(() =>
|
|
300
|
-
(choix.value ?? []).map(c => ({
|
|
301
|
-
_id: '',
|
|
302
|
-
nomVue: c.nomVue,
|
|
303
|
-
colonnes: c.colonnes ?? [],
|
|
304
|
-
defaut: c.defaut ?? false,
|
|
305
|
-
})),
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
// ============================================================================
|
|
309
|
-
// Actions - Modale choix colonnes
|
|
310
|
-
// ============================================================================
|
|
311
|
-
function ouvrirChoixColonnes() {
|
|
312
|
-
modaleChoix.value?.ouvrir()
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function appliquerVue(vue: VueColonnes) {
|
|
316
|
-
vueActive.value = vue
|
|
317
|
-
openChoixColonnes.value = false
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function selectionChoix(vue: VueColonnes) {
|
|
321
|
-
// on garde une trace du nom sélectionné (pour retrouver la vue plus tard)
|
|
322
|
-
valeurSelectionChoixColonnes.value = vue.nomVue
|
|
323
|
-
appliquerVue(vue)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function sauvegardeChoix(vues: VueColonnes[]) {
|
|
327
|
-
if (!props.permettreChoixColonnes) return
|
|
328
|
-
|
|
329
|
-
// si la sélection courante n’existe plus, on reset
|
|
330
|
-
if (
|
|
331
|
-
valeurSelectionChoixColonnes.value != null &&
|
|
332
|
-
vues.find(x => x.nomVue === valeurSelectionChoixColonnes.value) == null
|
|
333
|
-
) {
|
|
334
|
-
valeurSelectionChoixColonnes.value = null
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
choix.value = vues
|
|
338
|
-
|
|
339
|
-
// optionnel: appliquer automatiquement la vue par défaut
|
|
340
|
-
const defaut = vues.find(v => v.defaut)
|
|
341
|
-
if (defaut) appliquerVue(defaut)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// ============================================================================
|
|
345
|
-
// Computed - Mode ordre manuel
|
|
346
|
-
// ============================================================================
|
|
347
|
-
const estEnModeModificateurOrdre = computed(() => {
|
|
348
|
-
if (!props.modificateurOrdreChamp) return false
|
|
349
|
-
|
|
350
|
-
if (props.liste == null) return false
|
|
351
|
-
|
|
352
|
-
// Vérifie que le champ existe dans les données (pas nécessairement une colonne visible)
|
|
353
|
-
if (!props.liste.length) return false // liste vide, on attend sans avertissement
|
|
354
|
-
|
|
355
|
-
const premierItem = props.liste[0]
|
|
356
|
-
|
|
357
|
-
if (!(props.modificateurOrdreChamp in premierItem)) {
|
|
358
|
-
console.log(
|
|
359
|
-
`[csqcTable] modificateurOrdreChamp="${props.modificateurOrdreChamp}" n'existe pas dans les données. Le mode d'ordonnancement est désactivé.`,
|
|
360
|
-
)
|
|
361
|
-
return false
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return true
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
// ============================================================================
|
|
368
|
-
// Computed - Colonnes à envoyer à la modale (liste ordonnée + libellé)
|
|
369
|
-
// ============================================================================
|
|
370
|
-
const colonnesPourChoix = computed(() =>
|
|
371
|
-
props.colonnes
|
|
372
|
-
// on exclut l'action (souvent toujours visible) + colonnes "cachées"
|
|
373
|
-
.filter(c => c.key && c.key !== 'action' && (c as any).align !== 'd-none')
|
|
374
|
-
.map((c, idx) => ({
|
|
375
|
-
title: (c as any).title ?? (c as any).text ?? String((c as any).key ?? (c as any).value),
|
|
376
|
-
value: String((c as any).key ?? (c as any).value), // ← on force string
|
|
377
|
-
ordre: idx + 1,
|
|
378
|
-
})),
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
// ============================================================================
|
|
382
|
-
// Computed - Headers réellement affichés par la table (selon le choix actif)
|
|
383
|
-
// ============================================================================
|
|
384
|
-
const colonnesAffichees = computed(() => {
|
|
385
|
-
// 1) colonnes "disponibles" (on enlève celles explicitement cachées)
|
|
386
|
-
const colonnesFiltre = props.colonnes.filter(c => (c as any).align !== 'd-none')
|
|
387
|
-
|
|
388
|
-
// 2) résoudre la liste selon les vues sauvegardées (choix colonnes)
|
|
389
|
-
let colonnesBase: Colonne[]
|
|
390
|
-
if (!props.permettreChoixColonnes || !choix.value?.length) {
|
|
391
|
-
colonnesBase = colonnesFiltre
|
|
392
|
-
} else {
|
|
393
|
-
// retrouver la vue sélectionnée (sinon la défaut)
|
|
394
|
-
const choixSelection = choix.value.find(
|
|
395
|
-
x => valeurSelectionChoixColonnes.value === x.nomVue || (!valeurSelectionChoixColonnes.value && x.defaut),
|
|
396
|
-
)
|
|
397
|
-
colonnesBase = choixSelection
|
|
398
|
-
? (choixSelection.colonnes.map(k => colonnesFiltre.find(c => String(c.key) === k)).filter(Boolean) as Colonne[])
|
|
399
|
-
: colonnesFiltre
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// 3) ordre manuel actif : colonne ancre en tête + toutes les colonnes non triables
|
|
403
|
-
if (estEnModeModificateurOrdre.value) {
|
|
404
|
-
const colonneAncre = { key: 'ancre', title: '', sortable: false, width: '40px' } as Colonne
|
|
405
|
-
return [colonneAncre, ...colonnesBase.map(c => ({ ...c, sortable: false }))]
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
return colonnesBase
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
// ============================================================================
|
|
412
|
-
// Computed - Tri initial (Vuetify sort-by)
|
|
413
|
-
// ============================================================================
|
|
414
|
-
const triDepart = computed<SortItem[] | undefined>(() => {
|
|
415
|
-
// ordre manuel : tri forcé sur le champ ordre, croissant
|
|
416
|
-
if (estEnModeModificateurOrdre.value) return [{ key: props.modificateurOrdreChamp!, order: 'asc' }]
|
|
417
|
-
|
|
418
|
-
if (props.triParDepart == null) return undefined
|
|
419
|
-
|
|
420
|
-
const ordre = props.triDescDepart ? 'desc' : 'asc'
|
|
421
|
-
if (typeof props.triParDepart === 'string') return [{ key: props.triParDepart, order: ordre }]
|
|
422
|
-
|
|
423
|
-
const retour: SortItem[] = []
|
|
424
|
-
for (let i = 0; i < props.triParDepart.length; i += 1) {
|
|
425
|
-
const tri = props.triParDepart[i]!
|
|
426
|
-
if (typeof tri === 'string') retour.push({ key: tri, order: ordre })
|
|
427
|
-
else retour.push(tri)
|
|
428
|
-
}
|
|
429
|
-
return retour
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
const estMultiTriActif = computed(() => {
|
|
433
|
-
if (estEnModeModificateurOrdre.value) return false
|
|
434
|
-
if (props.triParDepart == null) return false
|
|
435
|
-
if (typeof props.triParDepart === 'string') return false
|
|
436
|
-
return true
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
// ============================================================================
|
|
440
|
-
// Computed - Export (filtrage local basé sur recherche)
|
|
441
|
-
// ============================================================================
|
|
442
|
-
const filteredItems = computed(() => {
|
|
443
|
-
if (!recherche.value) return props.liste
|
|
444
|
-
const q = recherche.value.toLowerCase()
|
|
445
|
-
return props.liste.filter(item => Object.values(item).some(val => String(val).toLowerCase().includes(q)))
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
const listeEffective = computed(() => (estEnModeModificateurOrdre.value ? listeInterne.value : props.liste))
|
|
449
|
-
|
|
450
|
-
// ============================================================================
|
|
451
|
-
// Drag & drop - Ordre manuel (Sortable.js)
|
|
452
|
-
// ============================================================================
|
|
453
|
-
let sortableInstance: Sortable | null = null
|
|
454
|
-
|
|
455
|
-
// Synchronise listeInterne quand le parent met à jour props.liste
|
|
456
|
-
watch(
|
|
457
|
-
() => props.liste,
|
|
458
|
-
val => {
|
|
459
|
-
listeInterne.value = [...val]
|
|
460
|
-
},
|
|
461
|
-
{ immediate: true, deep: true },
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
// Init/destroy Sortable si la prop change en cours de vie du composant
|
|
465
|
-
watch(estEnModeModificateurOrdre, actif => {
|
|
466
|
-
if (actif) nextTick(initSortable)
|
|
467
|
-
else destroySortable()
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
onMounted(() => {
|
|
471
|
-
if (estEnModeModificateurOrdre.value) nextTick(initSortable)
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
onBeforeUnmount(() => {
|
|
475
|
-
destroySortable()
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
function initSortable() {
|
|
479
|
-
const tableEl = datatable.value?.$el as HTMLElement | undefined
|
|
480
|
-
if (!tableEl) return
|
|
481
|
-
const tbody = tableEl.querySelector('tbody')
|
|
482
|
-
if (!tbody) return
|
|
483
|
-
|
|
484
|
-
destroySortable() // garantit une seule instance active
|
|
485
|
-
|
|
486
|
-
sortableInstance = Sortable.create(tbody, {
|
|
487
|
-
animation: 150,
|
|
488
|
-
handle: '.ancre-drag',
|
|
489
|
-
onEnd(event) {
|
|
490
|
-
const oldIndex = event.oldIndex
|
|
491
|
-
const newIndex = event.newIndex
|
|
492
|
-
if (oldIndex == null || newIndex == null || oldIndex === newIndex) return
|
|
493
|
-
|
|
494
|
-
const items = [...listeInterne.value]
|
|
495
|
-
const moved = items.splice(oldIndex, 1)[0]
|
|
496
|
-
items.splice(newIndex, 0, moved)
|
|
497
|
-
|
|
498
|
-
// Recalcule la valeur du champ ordre pour maintenir la cohérence
|
|
499
|
-
const champ = props.modificateurOrdreChamp!
|
|
500
|
-
items.forEach((item, idx) => {
|
|
501
|
-
item[champ] = idx + 1
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
listeInterne.value = items
|
|
505
|
-
emit('ordreRangeeModifie', [...items])
|
|
506
|
-
},
|
|
507
|
-
})
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function destroySortable() {
|
|
511
|
-
sortableInstance?.destroy()
|
|
512
|
-
sortableInstance = null
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// ============================================================================
|
|
516
|
-
// UI Actions - Recherche / Table events
|
|
517
|
-
// ============================================================================
|
|
518
|
-
function chargerRecherche(val: string) {
|
|
519
|
-
recherche.value = val
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function ajouter() {
|
|
523
|
-
emit('ajouter')
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function cliqueLigne(_e: Event, { item }: { item: any }) {
|
|
527
|
-
emit('cliqueLigne', item)
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function modifier(item: any) {
|
|
531
|
-
emit('modifier', item)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// ============================================================================
|
|
535
|
-
// Suppression - Texte + actions
|
|
536
|
-
// ============================================================================
|
|
537
|
-
const supprimerTexte = computed(() => {
|
|
538
|
-
if (itemSelectionne.value == null) return ''
|
|
539
|
-
|
|
540
|
-
if (props.modaleSupprimerTexte) return props.modaleSupprimerTexte
|
|
541
|
-
|
|
542
|
-
return t('csqc.message.supprimerMessage', {
|
|
543
|
-
nom: itemSelectionne?.value?.[props.modaleSupprimerChamp] ?? '',
|
|
544
|
-
})
|
|
545
|
-
})
|
|
546
|
-
|
|
547
|
-
const supprimerTitreTexte = computed(() => props.modaleSupprimerTitre || t('csqc.message.supprimerTitre'))
|
|
548
|
-
|
|
549
|
-
function ouvrirModaleSupprimer(item: any) {
|
|
550
|
-
itemSelectionne.value = item
|
|
551
|
-
emit('supprimer', itemSelectionne.value) // tu sembles gérer la modale ailleurs
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function supprimer() {
|
|
555
|
-
emit('supprimer', itemSelectionne.value)
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// ============================================================================
|
|
559
|
-
// UI - Panneau recherche avancée
|
|
560
|
-
// ============================================================================
|
|
561
|
-
function onPanelChange(val: boolean) {
|
|
562
|
-
emit('panneau:etat', val)
|
|
563
|
-
}
|
|
564
|
-
</script>
|
|
565
|
-
|
|
566
|
-
<style scoped lang="css">
|
|
567
|
-
.ControlesDatatable {
|
|
568
|
-
gap: 4px;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/* barre de recherche design QC*/
|
|
572
|
-
.BarreRecherche {
|
|
573
|
-
border-radius: 4px 0 0 4px !important;
|
|
574
|
-
min-height: 40px;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
.BarreRechercheBackIcone {
|
|
578
|
-
border-right: 1px solid #808a9d;
|
|
579
|
-
border-top: 1px solid #808a9d;
|
|
580
|
-
border-bottom: 1px solid #808a9d;
|
|
581
|
-
background-color: #095797 !important;
|
|
582
|
-
height: 40px !important;
|
|
583
|
-
width: 40px !important;
|
|
584
|
-
max-width: 40px !important;
|
|
585
|
-
min-width: 40px !important;
|
|
586
|
-
padding-right: 0 !important;
|
|
587
|
-
padding-left: 0 !important;
|
|
588
|
-
border-radius: 0 4px 4px 0 !important;
|
|
589
|
-
margin-left: -16px;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/* icone loupe */
|
|
593
|
-
.BarreRechercheIcone {
|
|
594
|
-
font-size: 34px !important;
|
|
595
|
-
margin-left: 1px !important;
|
|
596
|
-
margin-top: 2px !important;
|
|
597
|
-
color: white !important;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/* datatable contour */
|
|
601
|
-
.v-data-table {
|
|
602
|
-
border: 1px solid #d3d3d3;
|
|
603
|
-
border-radius: 5px;
|
|
604
|
-
overflow: hidden;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/* hover */
|
|
608
|
-
.v-data-table:hover {
|
|
609
|
-
cursor: pointer !important;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/* datatable row */
|
|
613
|
-
.v-data-table .v-table__wrapper > table > thead > tr > td,
|
|
614
|
-
.v-data-table .v-table__wrapper > table > thead > tr th,
|
|
615
|
-
.v-data-table .v-table__wrapper > table tbody > tr > td,
|
|
616
|
-
.v-data-table .v-table__wrapper > table tbody > tr th {
|
|
617
|
-
background-color: #d3d3d375 !important;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/* datatable header contour */
|
|
621
|
-
.v-data-table .v-table__wrapper > table > thead > tr > th,
|
|
622
|
-
.v-data-table .v-table__wrapper > table tbody > tr > th {
|
|
623
|
-
background-color: white !important;
|
|
624
|
-
border-bottom: 4px #223654 solid !important;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/* datatable header intérieur*/
|
|
628
|
-
.v-data-table-header__content {
|
|
629
|
-
background-color: white !important;
|
|
630
|
-
color: #223654 !important;
|
|
631
|
-
font-weight: 600;
|
|
632
|
-
font-size: 15px;
|
|
633
|
-
height: 46px;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/* datatable footer */
|
|
637
|
-
.v-data-table-footer {
|
|
638
|
-
background-color: white !important;
|
|
639
|
-
border-top: 4px #223654 solid !important;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/* datatable row hover */
|
|
643
|
-
.v-data-table .v-table__wrapper > table tbody > tr:hover > td,
|
|
644
|
-
.v-data-table .v-table__wrapper > table tbody > tr:hover {
|
|
645
|
-
background-color: #e0e0e0 !important;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
.iconeSupprimer:hover {
|
|
649
|
-
color: red;
|
|
650
|
-
}
|
|
651
|
-
.v-icon.v-icon.v-icon--link.iconeSupprimer:hover {
|
|
652
|
-
color: red !important;
|
|
653
|
-
}
|
|
654
|
-
.iconeEditer:hover {
|
|
655
|
-
color: #095797;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/* hover icone à droite du ajouter*/
|
|
659
|
-
.text-grisMoyen:hover {
|
|
660
|
-
color: #095797 !important;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/* icône d'ancre drag & drop */
|
|
664
|
-
.ancre-drag {
|
|
665
|
-
cursor: grab;
|
|
666
|
-
color: #808a9d;
|
|
667
|
-
}
|
|
668
|
-
.ancre-drag:active {
|
|
669
|
-
cursor: grabbing;
|
|
670
|
-
}
|
|
671
|
-
</style>
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-row v-if="barreHautAfficher">
|
|
4
|
+
<slot name="ligne" />
|
|
5
|
+
<!-- Affichage de la boite de recherche-->
|
|
6
|
+
<v-col>
|
|
7
|
+
<slot name="recherche"></slot>
|
|
8
|
+
<Recherche
|
|
9
|
+
v-bind="$attrs"
|
|
10
|
+
:afficher="rechercheAfficher"
|
|
11
|
+
:recherche-texte="rechercheTexte"
|
|
12
|
+
:chargement="chargementListe"
|
|
13
|
+
:recherche="recherche"
|
|
14
|
+
:recherche-avancee="rechercheAvancee"
|
|
15
|
+
:recherche-avancee-texte="rechercheAvanceeTexte"
|
|
16
|
+
:recherche-avancee-largeur="rechercheAvanceeLargeur"
|
|
17
|
+
:recherche-avancee-style="rechercheAvanceeStyle"
|
|
18
|
+
class="flex-grow-1"
|
|
19
|
+
@update:recherche="chargerRecherche"
|
|
20
|
+
@panneau:etat="onPanelChange"
|
|
21
|
+
>
|
|
22
|
+
<template #milieu>
|
|
23
|
+
<slot name="milieu" />
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<template #droite>
|
|
27
|
+
<slot name="droite" />
|
|
28
|
+
<v-btn
|
|
29
|
+
v-if="btnAjouter"
|
|
30
|
+
color="primary"
|
|
31
|
+
class="ml-1 float-right"
|
|
32
|
+
@click.stop="ajouter"
|
|
33
|
+
>
|
|
34
|
+
{{ props.btnAjouterTexte ? props.btnAjouterTexte : $t('csqc.bouton.ajouter') }}
|
|
35
|
+
</v-btn>
|
|
36
|
+
|
|
37
|
+
<exportExcelComponent
|
|
38
|
+
v-if="excel"
|
|
39
|
+
:liste="filteredItems"
|
|
40
|
+
:chargement-liste="chargementListe"
|
|
41
|
+
:nom-fichier="excelNomFichier"
|
|
42
|
+
class="mt-1 ml-1 float-right"
|
|
43
|
+
/>
|
|
44
|
+
<v-icon
|
|
45
|
+
v-if="permettreChoixColonnes"
|
|
46
|
+
color="grisMoyen"
|
|
47
|
+
class="mt-1 ml-1 float-right"
|
|
48
|
+
@click.stop="ouvrirChoixColonnes"
|
|
49
|
+
>
|
|
50
|
+
mdi-table-edit
|
|
51
|
+
</v-icon>
|
|
52
|
+
</template>
|
|
53
|
+
<template #rechercheAvanceeTitre>
|
|
54
|
+
<slot name="rechercheAvanceeTitre" />
|
|
55
|
+
</template>
|
|
56
|
+
<template #rechercheAvanceeApresTitre>
|
|
57
|
+
<slot name="rechercheAvanceeApresTitre" />
|
|
58
|
+
</template>
|
|
59
|
+
<template #rechercheAvancee>
|
|
60
|
+
<slot name="rechercheAvancee" />
|
|
61
|
+
</template>
|
|
62
|
+
</Recherche>
|
|
63
|
+
</v-col>
|
|
64
|
+
</v-row>
|
|
65
|
+
|
|
66
|
+
<!-- datatable-->
|
|
67
|
+
<v-row>
|
|
68
|
+
<v-col
|
|
69
|
+
cols="12"
|
|
70
|
+
class="d-flex ControlesDatatable flex-wrap"
|
|
71
|
+
>
|
|
72
|
+
<v-data-table
|
|
73
|
+
ref="datatable"
|
|
74
|
+
v-bind="$attrs"
|
|
75
|
+
:headers="colonnesAffichees"
|
|
76
|
+
:item-key="itemKey"
|
|
77
|
+
:items="listeEffective"
|
|
78
|
+
:search="recherche"
|
|
79
|
+
:loading="chargementListe"
|
|
80
|
+
:hover="hover"
|
|
81
|
+
:density="densite"
|
|
82
|
+
:items-per-page="itemsParPage"
|
|
83
|
+
:item-per-page-options="itemsParPageOptions"
|
|
84
|
+
:sort-by="triDepart"
|
|
85
|
+
:multi-sort="estMultiTriActif"
|
|
86
|
+
@click:row="cliqueLigne"
|
|
87
|
+
>
|
|
88
|
+
<!-- utilisation des slots via le component parent-->
|
|
89
|
+
<!-- eslint-disable-next-line -->
|
|
90
|
+
<template
|
|
91
|
+
v-for="(_, slot) of $slots"
|
|
92
|
+
:key="slot"
|
|
93
|
+
#[slot]="scope"
|
|
94
|
+
>
|
|
95
|
+
<slot
|
|
96
|
+
:name="slot"
|
|
97
|
+
v-bind="scope"
|
|
98
|
+
/>
|
|
99
|
+
</template>
|
|
100
|
+
|
|
101
|
+
<!-- Icône ancre (drag & drop) -->
|
|
102
|
+
<template
|
|
103
|
+
v-if="estEnModeModificateurOrdre"
|
|
104
|
+
#item.ancre
|
|
105
|
+
>
|
|
106
|
+
<v-icon class="ancre-drag">mdi-drag-vertical</v-icon>
|
|
107
|
+
</template>
|
|
108
|
+
|
|
109
|
+
<!-- boutons supprimer et modifier -->
|
|
110
|
+
<!-- eslint-disable-next-line -->
|
|
111
|
+
<template v-slot:item.action="{ item }">
|
|
112
|
+
<slot name="actionsCustom"></slot>
|
|
113
|
+
<v-icon
|
|
114
|
+
v-if="props.btnSupprimer"
|
|
115
|
+
size="large"
|
|
116
|
+
class="iconeSupprimer float-right"
|
|
117
|
+
@click.stop.prevent="ouvrirModaleSupprimer(item)"
|
|
118
|
+
>
|
|
119
|
+
mdi-delete
|
|
120
|
+
</v-icon>
|
|
121
|
+
|
|
122
|
+
<v-icon
|
|
123
|
+
v-if="props.btnModifier"
|
|
124
|
+
size="large"
|
|
125
|
+
class="iconeEditer float-right"
|
|
126
|
+
@click.stop.prevent="modifier(item)"
|
|
127
|
+
>
|
|
128
|
+
mdi-pencil
|
|
129
|
+
</v-icon>
|
|
130
|
+
</template>
|
|
131
|
+
</v-data-table>
|
|
132
|
+
|
|
133
|
+
<!-- Fenêtre de suppression -->
|
|
134
|
+
<confirmation
|
|
135
|
+
v-if="props.btnSupprimer"
|
|
136
|
+
ref="modaleSupprimer"
|
|
137
|
+
:texte="supprimerTexte"
|
|
138
|
+
:titre="supprimerTitreTexte"
|
|
139
|
+
:largeur="modaleSupprimerLargeur"
|
|
140
|
+
@confirmer="supprimer"
|
|
141
|
+
/>
|
|
142
|
+
<modale-choix
|
|
143
|
+
ref="modaleChoix"
|
|
144
|
+
v-if="permettreChoixColonnes"
|
|
145
|
+
:urlbase="urlbase"
|
|
146
|
+
:formulaire-id="formulaireId"
|
|
147
|
+
:identifiant="identifiant"
|
|
148
|
+
:colonnes="colonnesPourChoix"
|
|
149
|
+
:choix-origine="choixOrigineNormalise"
|
|
150
|
+
@selection="selectionChoix"
|
|
151
|
+
@sauvegarde="sauvegardeChoix"
|
|
152
|
+
/>
|
|
153
|
+
</v-col>
|
|
154
|
+
</v-row>
|
|
155
|
+
</div>
|
|
156
|
+
</template>
|
|
157
|
+
<script setup lang="ts">
|
|
158
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
159
|
+
import { computed, onBeforeUnmount, nextTick, onMounted, ref, watch, type PropType, type Slots } from 'vue'
|
|
160
|
+
import { useI18n } from 'vue-i18n'
|
|
161
|
+
import type { SortItem } from 'vuetify/lib/components/VDataTable/composables/sort.mjs'
|
|
162
|
+
|
|
163
|
+
import Recherche from '../csqcRecherche.vue'
|
|
164
|
+
import confirmation from '../csqcConfirmation.vue'
|
|
165
|
+
import exportExcelComponent from './csqcTableExportExcel.vue'
|
|
166
|
+
import ModaleChoix from './csqcTableModaleChoixColonnes.vue'
|
|
167
|
+
import axios from '../../outils/appAxios'
|
|
168
|
+
import Colonne from '../../modeles/composants/datatableColonne'
|
|
169
|
+
import Sortable from 'sortablejs'
|
|
170
|
+
|
|
171
|
+
type VueColonnes = {
|
|
172
|
+
nomVue: string
|
|
173
|
+
colonnes: string[]
|
|
174
|
+
defaut?: boolean
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const slots = defineSlots<Slots>()
|
|
178
|
+
const emit = defineEmits([
|
|
179
|
+
'ajouter',
|
|
180
|
+
'cliqueLigne',
|
|
181
|
+
'supprimer',
|
|
182
|
+
'modifier',
|
|
183
|
+
'donneesExportees',
|
|
184
|
+
'panneau:etat',
|
|
185
|
+
'ordreRangeeModifie',
|
|
186
|
+
])
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Props
|
|
190
|
+
// ============================================================================
|
|
191
|
+
const props = defineProps({
|
|
192
|
+
barreHautAfficher: { type: Boolean, default: true },
|
|
193
|
+
|
|
194
|
+
btnAjouter: { type: Boolean, default: true },
|
|
195
|
+
btnAjouterTexte: { type: String, default: '' },
|
|
196
|
+
btnModifier: { type: Boolean, default: true },
|
|
197
|
+
btnSupprimer: { type: Boolean, default: true },
|
|
198
|
+
|
|
199
|
+
chargementListe: { type: Boolean, default: false },
|
|
200
|
+
operationEnCours: { type: Boolean, default: false },
|
|
201
|
+
|
|
202
|
+
// Headers Vuetify
|
|
203
|
+
colonnes: {
|
|
204
|
+
type: Array as PropType<Colonne[]>,
|
|
205
|
+
default: () => [],
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
densite: { type: String as PropType<'default' | 'comfortable' | 'compact'>, default: 'default' },
|
|
209
|
+
|
|
210
|
+
excel: { type: Boolean, default: false },
|
|
211
|
+
excelNomFichier: { type: String, default: 'csqc' },
|
|
212
|
+
|
|
213
|
+
// Choix colonnes
|
|
214
|
+
formulaireId: { type: Number, default: -1 },
|
|
215
|
+
identifiant: { type: String, default: '' },
|
|
216
|
+
urlbase: { type: String, default: '' },
|
|
217
|
+
permettreChoixColonnes: { type: Boolean, default: false },
|
|
218
|
+
|
|
219
|
+
// Table
|
|
220
|
+
itemKey: { type: String, default: 'id' },
|
|
221
|
+
itemsParPage: { type: Number, default: 10 },
|
|
222
|
+
itemsParPageOptions: { type: Array, default: () => [10, 25, 30, -1] },
|
|
223
|
+
liste: {
|
|
224
|
+
type: Array as PropType<Record<string, any>[]>,
|
|
225
|
+
default: () => [],
|
|
226
|
+
},
|
|
227
|
+
hover: { type: Boolean, default: false },
|
|
228
|
+
|
|
229
|
+
// Recherche
|
|
230
|
+
rechercheTexte: { type: String, default: '' },
|
|
231
|
+
rechercheAfficher: { type: Boolean, default: true },
|
|
232
|
+
rechercheAvancee: { type: Boolean, default: false },
|
|
233
|
+
rechercheAvanceeTexte: { type: String, default: '' },
|
|
234
|
+
rechercheAvanceeLargeur: { type: Number, default: 12 },
|
|
235
|
+
rechercheAvanceeStyle: { type: String, default: '' },
|
|
236
|
+
|
|
237
|
+
// Modale suppression
|
|
238
|
+
modaleSupprimerChamp: { type: String, default: '' },
|
|
239
|
+
modaleSupprimerTexte: { type: String, default: '' },
|
|
240
|
+
modaleSupprimerTitre: { type: String, default: '' },
|
|
241
|
+
modaleSupprimerLargeur: { type: String, default: '525px' },
|
|
242
|
+
|
|
243
|
+
// Tri
|
|
244
|
+
triDescDepart: { type: Boolean, default: false },
|
|
245
|
+
triParDepart: {
|
|
246
|
+
type: [Array, String] as PropType<string | string[] | SortItem[] | undefined>,
|
|
247
|
+
default: undefined,
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
// Ordre manuel (drag & drop)
|
|
251
|
+
modificateurOrdreChamp: { type: String, default: undefined },
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const { t } = useI18n({ useScope: 'global' })
|
|
255
|
+
const recherche = ref<string>('')
|
|
256
|
+
|
|
257
|
+
// Modale suppression (on garde l’item sélectionné)
|
|
258
|
+
const itemSelectionne = ref<any>(null)
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// State - Choix de colonnes (vues sauvegardées) + sélection courante
|
|
262
|
+
// ============================================================================
|
|
263
|
+
const datatable = ref<any>(null)
|
|
264
|
+
const listeInterne = ref<Record<string, any>[]>([])
|
|
265
|
+
const modaleChoix = ref<InstanceType<typeof ModaleChoix> | null>(null)
|
|
266
|
+
const openChoixColonnes = ref(false)
|
|
267
|
+
const choix = ref<VueColonnes[]>([]) // la liste des vues disponibles
|
|
268
|
+
const vueActive = ref<VueColonnes | null>(null) // optionnel: la vue "appliquée"
|
|
269
|
+
const valeurSelectionChoixColonnes = ref<string | null>(null) // nomVue sélectionné dans la modale / UI
|
|
270
|
+
|
|
271
|
+
// ============================================================================
|
|
272
|
+
// Charger le choix de colonnes sauvegardé s'il y a lieu
|
|
273
|
+
// ============================================================================
|
|
274
|
+
onMounted(async () => {
|
|
275
|
+
if (!props.permettreChoixColonnes) return
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const res: any = await axios
|
|
279
|
+
.getAxios()
|
|
280
|
+
.get(`${props.urlbase}/api/ComposantUI/Colonnes/${props.formulaireId}/Identifiant/${props.identifiant}`)
|
|
281
|
+
|
|
282
|
+
const payload = res?.data ?? res
|
|
283
|
+
|
|
284
|
+
if (!payload) {
|
|
285
|
+
choix.value = []
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
choix.value = typeof payload === 'string' ? JSON.parse(payload) : payload
|
|
290
|
+
} catch (e) {
|
|
291
|
+
choix.value = []
|
|
292
|
+
// console.debug(e)
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// Computed - Normaliser pour la modale Choix colonne
|
|
298
|
+
// ============================================================================
|
|
299
|
+
const choixOrigineNormalise = computed(() =>
|
|
300
|
+
(choix.value ?? []).map(c => ({
|
|
301
|
+
_id: '',
|
|
302
|
+
nomVue: c.nomVue,
|
|
303
|
+
colonnes: c.colonnes ?? [],
|
|
304
|
+
defaut: c.defaut ?? false,
|
|
305
|
+
})),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
// ============================================================================
|
|
309
|
+
// Actions - Modale choix colonnes
|
|
310
|
+
// ============================================================================
|
|
311
|
+
function ouvrirChoixColonnes() {
|
|
312
|
+
modaleChoix.value?.ouvrir()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function appliquerVue(vue: VueColonnes) {
|
|
316
|
+
vueActive.value = vue
|
|
317
|
+
openChoixColonnes.value = false
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function selectionChoix(vue: VueColonnes) {
|
|
321
|
+
// on garde une trace du nom sélectionné (pour retrouver la vue plus tard)
|
|
322
|
+
valeurSelectionChoixColonnes.value = vue.nomVue
|
|
323
|
+
appliquerVue(vue)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function sauvegardeChoix(vues: VueColonnes[]) {
|
|
327
|
+
if (!props.permettreChoixColonnes) return
|
|
328
|
+
|
|
329
|
+
// si la sélection courante n’existe plus, on reset
|
|
330
|
+
if (
|
|
331
|
+
valeurSelectionChoixColonnes.value != null &&
|
|
332
|
+
vues.find(x => x.nomVue === valeurSelectionChoixColonnes.value) == null
|
|
333
|
+
) {
|
|
334
|
+
valeurSelectionChoixColonnes.value = null
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
choix.value = vues
|
|
338
|
+
|
|
339
|
+
// optionnel: appliquer automatiquement la vue par défaut
|
|
340
|
+
const defaut = vues.find(v => v.defaut)
|
|
341
|
+
if (defaut) appliquerVue(defaut)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// Computed - Mode ordre manuel
|
|
346
|
+
// ============================================================================
|
|
347
|
+
const estEnModeModificateurOrdre = computed(() => {
|
|
348
|
+
if (!props.modificateurOrdreChamp) return false
|
|
349
|
+
|
|
350
|
+
if (props.liste == null) return false
|
|
351
|
+
|
|
352
|
+
// Vérifie que le champ existe dans les données (pas nécessairement une colonne visible)
|
|
353
|
+
if (!props.liste.length) return false // liste vide, on attend sans avertissement
|
|
354
|
+
|
|
355
|
+
const premierItem = props.liste[0]
|
|
356
|
+
|
|
357
|
+
if (!(props.modificateurOrdreChamp in premierItem)) {
|
|
358
|
+
console.log(
|
|
359
|
+
`[csqcTable] modificateurOrdreChamp="${props.modificateurOrdreChamp}" n'existe pas dans les données. Le mode d'ordonnancement est désactivé.`,
|
|
360
|
+
)
|
|
361
|
+
return false
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return true
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
// ============================================================================
|
|
368
|
+
// Computed - Colonnes à envoyer à la modale (liste ordonnée + libellé)
|
|
369
|
+
// ============================================================================
|
|
370
|
+
const colonnesPourChoix = computed(() =>
|
|
371
|
+
props.colonnes
|
|
372
|
+
// on exclut l'action (souvent toujours visible) + colonnes "cachées"
|
|
373
|
+
.filter(c => c.key && c.key !== 'action' && (c as any).align !== 'd-none')
|
|
374
|
+
.map((c, idx) => ({
|
|
375
|
+
title: (c as any).title ?? (c as any).text ?? String((c as any).key ?? (c as any).value),
|
|
376
|
+
value: String((c as any).key ?? (c as any).value), // ← on force string
|
|
377
|
+
ordre: idx + 1,
|
|
378
|
+
})),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
// ============================================================================
|
|
382
|
+
// Computed - Headers réellement affichés par la table (selon le choix actif)
|
|
383
|
+
// ============================================================================
|
|
384
|
+
const colonnesAffichees = computed(() => {
|
|
385
|
+
// 1) colonnes "disponibles" (on enlève celles explicitement cachées)
|
|
386
|
+
const colonnesFiltre = props.colonnes.filter(c => (c as any).align !== 'd-none')
|
|
387
|
+
|
|
388
|
+
// 2) résoudre la liste selon les vues sauvegardées (choix colonnes)
|
|
389
|
+
let colonnesBase: Colonne[]
|
|
390
|
+
if (!props.permettreChoixColonnes || !choix.value?.length) {
|
|
391
|
+
colonnesBase = colonnesFiltre
|
|
392
|
+
} else {
|
|
393
|
+
// retrouver la vue sélectionnée (sinon la défaut)
|
|
394
|
+
const choixSelection = choix.value.find(
|
|
395
|
+
x => valeurSelectionChoixColonnes.value === x.nomVue || (!valeurSelectionChoixColonnes.value && x.defaut),
|
|
396
|
+
)
|
|
397
|
+
colonnesBase = choixSelection
|
|
398
|
+
? (choixSelection.colonnes.map(k => colonnesFiltre.find(c => String(c.key) === k)).filter(Boolean) as Colonne[])
|
|
399
|
+
: colonnesFiltre
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 3) ordre manuel actif : colonne ancre en tête + toutes les colonnes non triables
|
|
403
|
+
if (estEnModeModificateurOrdre.value) {
|
|
404
|
+
const colonneAncre = { key: 'ancre', title: '', sortable: false, width: '40px' } as Colonne
|
|
405
|
+
return [colonneAncre, ...colonnesBase.map(c => ({ ...c, sortable: false }))]
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return colonnesBase
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
// ============================================================================
|
|
412
|
+
// Computed - Tri initial (Vuetify sort-by)
|
|
413
|
+
// ============================================================================
|
|
414
|
+
const triDepart = computed<SortItem[] | undefined>(() => {
|
|
415
|
+
// ordre manuel : tri forcé sur le champ ordre, croissant
|
|
416
|
+
if (estEnModeModificateurOrdre.value) return [{ key: props.modificateurOrdreChamp!, order: 'asc' }]
|
|
417
|
+
|
|
418
|
+
if (props.triParDepart == null) return undefined
|
|
419
|
+
|
|
420
|
+
const ordre = props.triDescDepart ? 'desc' : 'asc'
|
|
421
|
+
if (typeof props.triParDepart === 'string') return [{ key: props.triParDepart, order: ordre }]
|
|
422
|
+
|
|
423
|
+
const retour: SortItem[] = []
|
|
424
|
+
for (let i = 0; i < props.triParDepart.length; i += 1) {
|
|
425
|
+
const tri = props.triParDepart[i]!
|
|
426
|
+
if (typeof tri === 'string') retour.push({ key: tri, order: ordre })
|
|
427
|
+
else retour.push(tri)
|
|
428
|
+
}
|
|
429
|
+
return retour
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
const estMultiTriActif = computed(() => {
|
|
433
|
+
if (estEnModeModificateurOrdre.value) return false
|
|
434
|
+
if (props.triParDepart == null) return false
|
|
435
|
+
if (typeof props.triParDepart === 'string') return false
|
|
436
|
+
return true
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
// ============================================================================
|
|
440
|
+
// Computed - Export (filtrage local basé sur recherche)
|
|
441
|
+
// ============================================================================
|
|
442
|
+
const filteredItems = computed(() => {
|
|
443
|
+
if (!recherche.value) return props.liste
|
|
444
|
+
const q = recherche.value.toLowerCase()
|
|
445
|
+
return props.liste.filter(item => Object.values(item).some(val => String(val).toLowerCase().includes(q)))
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
const listeEffective = computed(() => (estEnModeModificateurOrdre.value ? listeInterne.value : props.liste))
|
|
449
|
+
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// Drag & drop - Ordre manuel (Sortable.js)
|
|
452
|
+
// ============================================================================
|
|
453
|
+
let sortableInstance: Sortable | null = null
|
|
454
|
+
|
|
455
|
+
// Synchronise listeInterne quand le parent met à jour props.liste
|
|
456
|
+
watch(
|
|
457
|
+
() => props.liste,
|
|
458
|
+
val => {
|
|
459
|
+
listeInterne.value = [...val]
|
|
460
|
+
},
|
|
461
|
+
{ immediate: true, deep: true },
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
// Init/destroy Sortable si la prop change en cours de vie du composant
|
|
465
|
+
watch(estEnModeModificateurOrdre, actif => {
|
|
466
|
+
if (actif) nextTick(initSortable)
|
|
467
|
+
else destroySortable()
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
onMounted(() => {
|
|
471
|
+
if (estEnModeModificateurOrdre.value) nextTick(initSortable)
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
onBeforeUnmount(() => {
|
|
475
|
+
destroySortable()
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
function initSortable() {
|
|
479
|
+
const tableEl = datatable.value?.$el as HTMLElement | undefined
|
|
480
|
+
if (!tableEl) return
|
|
481
|
+
const tbody = tableEl.querySelector('tbody')
|
|
482
|
+
if (!tbody) return
|
|
483
|
+
|
|
484
|
+
destroySortable() // garantit une seule instance active
|
|
485
|
+
|
|
486
|
+
sortableInstance = Sortable.create(tbody, {
|
|
487
|
+
animation: 150,
|
|
488
|
+
handle: '.ancre-drag',
|
|
489
|
+
onEnd(event) {
|
|
490
|
+
const oldIndex = event.oldIndex
|
|
491
|
+
const newIndex = event.newIndex
|
|
492
|
+
if (oldIndex == null || newIndex == null || oldIndex === newIndex) return
|
|
493
|
+
|
|
494
|
+
const items = [...listeInterne.value]
|
|
495
|
+
const moved = items.splice(oldIndex, 1)[0]
|
|
496
|
+
items.splice(newIndex, 0, moved)
|
|
497
|
+
|
|
498
|
+
// Recalcule la valeur du champ ordre pour maintenir la cohérence
|
|
499
|
+
const champ = props.modificateurOrdreChamp!
|
|
500
|
+
items.forEach((item, idx) => {
|
|
501
|
+
item[champ] = idx + 1
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
listeInterne.value = items
|
|
505
|
+
emit('ordreRangeeModifie', [...items])
|
|
506
|
+
},
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function destroySortable() {
|
|
511
|
+
sortableInstance?.destroy()
|
|
512
|
+
sortableInstance = null
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ============================================================================
|
|
516
|
+
// UI Actions - Recherche / Table events
|
|
517
|
+
// ============================================================================
|
|
518
|
+
function chargerRecherche(val: string) {
|
|
519
|
+
recherche.value = val
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function ajouter() {
|
|
523
|
+
emit('ajouter')
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function cliqueLigne(_e: Event, { item }: { item: any }) {
|
|
527
|
+
emit('cliqueLigne', item)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function modifier(item: any) {
|
|
531
|
+
emit('modifier', item)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ============================================================================
|
|
535
|
+
// Suppression - Texte + actions
|
|
536
|
+
// ============================================================================
|
|
537
|
+
const supprimerTexte = computed(() => {
|
|
538
|
+
if (itemSelectionne.value == null) return ''
|
|
539
|
+
|
|
540
|
+
if (props.modaleSupprimerTexte) return props.modaleSupprimerTexte
|
|
541
|
+
|
|
542
|
+
return t('csqc.message.supprimerMessage', {
|
|
543
|
+
nom: itemSelectionne?.value?.[props.modaleSupprimerChamp] ?? '',
|
|
544
|
+
})
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
const supprimerTitreTexte = computed(() => props.modaleSupprimerTitre || t('csqc.message.supprimerTitre'))
|
|
548
|
+
|
|
549
|
+
function ouvrirModaleSupprimer(item: any) {
|
|
550
|
+
itemSelectionne.value = item
|
|
551
|
+
emit('supprimer', itemSelectionne.value) // tu sembles gérer la modale ailleurs
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function supprimer() {
|
|
555
|
+
emit('supprimer', itemSelectionne.value)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ============================================================================
|
|
559
|
+
// UI - Panneau recherche avancée
|
|
560
|
+
// ============================================================================
|
|
561
|
+
function onPanelChange(val: boolean) {
|
|
562
|
+
emit('panneau:etat', val)
|
|
563
|
+
}
|
|
564
|
+
</script>
|
|
565
|
+
|
|
566
|
+
<style scoped lang="css">
|
|
567
|
+
.ControlesDatatable {
|
|
568
|
+
gap: 4px;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* barre de recherche design QC*/
|
|
572
|
+
.BarreRecherche {
|
|
573
|
+
border-radius: 4px 0 0 4px !important;
|
|
574
|
+
min-height: 40px;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.BarreRechercheBackIcone {
|
|
578
|
+
border-right: 1px solid #808a9d;
|
|
579
|
+
border-top: 1px solid #808a9d;
|
|
580
|
+
border-bottom: 1px solid #808a9d;
|
|
581
|
+
background-color: #095797 !important;
|
|
582
|
+
height: 40px !important;
|
|
583
|
+
width: 40px !important;
|
|
584
|
+
max-width: 40px !important;
|
|
585
|
+
min-width: 40px !important;
|
|
586
|
+
padding-right: 0 !important;
|
|
587
|
+
padding-left: 0 !important;
|
|
588
|
+
border-radius: 0 4px 4px 0 !important;
|
|
589
|
+
margin-left: -16px;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/* icone loupe */
|
|
593
|
+
.BarreRechercheIcone {
|
|
594
|
+
font-size: 34px !important;
|
|
595
|
+
margin-left: 1px !important;
|
|
596
|
+
margin-top: 2px !important;
|
|
597
|
+
color: white !important;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/* datatable contour */
|
|
601
|
+
.v-data-table {
|
|
602
|
+
border: 1px solid #d3d3d3;
|
|
603
|
+
border-radius: 5px;
|
|
604
|
+
overflow: hidden;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* hover */
|
|
608
|
+
.v-data-table:hover {
|
|
609
|
+
cursor: pointer !important;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/* datatable row */
|
|
613
|
+
.v-data-table .v-table__wrapper > table > thead > tr > td,
|
|
614
|
+
.v-data-table .v-table__wrapper > table > thead > tr th,
|
|
615
|
+
.v-data-table .v-table__wrapper > table tbody > tr > td,
|
|
616
|
+
.v-data-table .v-table__wrapper > table tbody > tr th {
|
|
617
|
+
background-color: #d3d3d375 !important;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* datatable header contour */
|
|
621
|
+
.v-data-table .v-table__wrapper > table > thead > tr > th,
|
|
622
|
+
.v-data-table .v-table__wrapper > table tbody > tr > th {
|
|
623
|
+
background-color: white !important;
|
|
624
|
+
border-bottom: 4px #223654 solid !important;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/* datatable header intérieur*/
|
|
628
|
+
.v-data-table-header__content {
|
|
629
|
+
background-color: white !important;
|
|
630
|
+
color: #223654 !important;
|
|
631
|
+
font-weight: 600;
|
|
632
|
+
font-size: 15px;
|
|
633
|
+
height: 46px;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/* datatable footer */
|
|
637
|
+
.v-data-table-footer {
|
|
638
|
+
background-color: white !important;
|
|
639
|
+
border-top: 4px #223654 solid !important;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/* datatable row hover */
|
|
643
|
+
.v-data-table .v-table__wrapper > table tbody > tr:hover > td,
|
|
644
|
+
.v-data-table .v-table__wrapper > table tbody > tr:hover {
|
|
645
|
+
background-color: #e0e0e0 !important;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.iconeSupprimer:hover {
|
|
649
|
+
color: red;
|
|
650
|
+
}
|
|
651
|
+
.v-icon.v-icon.v-icon--link.iconeSupprimer:hover {
|
|
652
|
+
color: red !important;
|
|
653
|
+
}
|
|
654
|
+
.iconeEditer:hover {
|
|
655
|
+
color: #095797;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/* hover icone à droite du ajouter*/
|
|
659
|
+
.text-grisMoyen:hover {
|
|
660
|
+
color: #095797 !important;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* icône d'ancre drag & drop */
|
|
664
|
+
.ancre-drag {
|
|
665
|
+
cursor: grab;
|
|
666
|
+
color: #808a9d;
|
|
667
|
+
}
|
|
668
|
+
.ancre-drag:active {
|
|
669
|
+
cursor: grabbing;
|
|
670
|
+
}
|
|
671
|
+
</style>
|