codevdesign 2.0.5 → 2.0.6
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 +345 -345
- package/composants/csqcCodeBudgetaireGenerique.vue +340 -340
- package/composants/csqcRecherche.vue +219 -219
- package/composants/csqcSwitch.vue +220 -220
- package/composants/csqcTable/csqcTable.vue +847 -847
- package/composants/csqcTexteBilingue.vue +175 -175
- package/composants/csqcTiroir.vue +197 -197
- package/composants/gabarit/csqcMenu.vue +19 -14
- package/composants/gabarit/pivEntete.vue +206 -206
- package/composants/gabarit/pivPiedPage.vue +10 -2
- package/package.json +1 -1
|
@@ -1,847 +1,847 @@
|
|
|
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
|
-
:afficher="rechercheAfficher"
|
|
10
|
-
:recherche-texte="rechercheTexte"
|
|
11
|
-
:chargement="chargementListe"
|
|
12
|
-
:recherche="recherche"
|
|
13
|
-
:recherche-avancee="rechercheAvancee"
|
|
14
|
-
:recherche-avancee-texte="rechercheAvanceeTexte"
|
|
15
|
-
:recherche-avancee-largeur="rechercheAvanceeLargeur"
|
|
16
|
-
:recherche-avancee-style="rechercheAvanceeStyle"
|
|
17
|
-
class="flex-grow-1"
|
|
18
|
-
v-bind="$attrs"
|
|
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
|
-
:colonnes="colonnesAffichees"
|
|
43
|
-
class="mt-1 ml-1 float-right"
|
|
44
|
-
/>
|
|
45
|
-
<v-icon
|
|
46
|
-
v-if="permettreChoixColonnes"
|
|
47
|
-
color="grisMoyen"
|
|
48
|
-
class="mt-1 ml-1 float-right"
|
|
49
|
-
@click.stop="ouvrirChoixColonnes"
|
|
50
|
-
>
|
|
51
|
-
mdi-table-edit
|
|
52
|
-
</v-icon>
|
|
53
|
-
</template>
|
|
54
|
-
<template #rechercheAvanceeTitre>
|
|
55
|
-
<slot name="rechercheAvanceeTitre" />
|
|
56
|
-
</template>
|
|
57
|
-
<template #rechercheAvanceeApresTitre>
|
|
58
|
-
<slot name="rechercheAvanceeApresTitre" />
|
|
59
|
-
</template>
|
|
60
|
-
<template #rechercheAvancee>
|
|
61
|
-
<slot name="rechercheAvancee" />
|
|
62
|
-
</template>
|
|
63
|
-
</Recherche>
|
|
64
|
-
</v-col>
|
|
65
|
-
</v-row>
|
|
66
|
-
|
|
67
|
-
<!-- datatable-->
|
|
68
|
-
<v-row>
|
|
69
|
-
<v-col
|
|
70
|
-
cols="12"
|
|
71
|
-
class="d-flex ControlesDatatable flex-wrap"
|
|
72
|
-
>
|
|
73
|
-
<v-data-table
|
|
74
|
-
ref="datatable"
|
|
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="itemsParPageLocal"
|
|
83
|
-
:item-per-page-options="itemsParPageOptions"
|
|
84
|
-
:sort-by="triDepart"
|
|
85
|
-
:multi-sort="estMultiTriActif"
|
|
86
|
-
v-model:page="pageActuelle"
|
|
87
|
-
:striped="props.styleEntete !== 'simplifie' ? 'even' : undefined"
|
|
88
|
-
:class="classeEntete"
|
|
89
|
-
v-bind="$attrs"
|
|
90
|
-
@click:row="cliqueLigne"
|
|
91
|
-
>
|
|
92
|
-
<!-- utilisation des slots via le component parent-->
|
|
93
|
-
<!-- eslint-disable-next-line -->
|
|
94
|
-
<template
|
|
95
|
-
v-for="(_, slot) of $slots"
|
|
96
|
-
:key="slot"
|
|
97
|
-
#[slot]="scope"
|
|
98
|
-
>
|
|
99
|
-
<slot
|
|
100
|
-
:name="slot"
|
|
101
|
-
v-bind="scope"
|
|
102
|
-
/>
|
|
103
|
-
</template>
|
|
104
|
-
|
|
105
|
-
<!-- Icône ancre (drag & drop) -->
|
|
106
|
-
<template
|
|
107
|
-
v-if="estEnModeModificateurOrdre"
|
|
108
|
-
#item.ancre
|
|
109
|
-
>
|
|
110
|
-
<v-icon class="ancre-drag">mdi-drag-vertical</v-icon>
|
|
111
|
-
</template>
|
|
112
|
-
|
|
113
|
-
<!-- Footer : pagination design Québec -->
|
|
114
|
-
<template #bottom>
|
|
115
|
-
<div class="piv-pagination">
|
|
116
|
-
<div class="piv-par-page">
|
|
117
|
-
<span class="text-caption text-medium-emphasis">{{ $t('csqc.pagination.elementsParPage') }}</span>
|
|
118
|
-
<v-select
|
|
119
|
-
v-model="itemsParPageLocal"
|
|
120
|
-
:items="itemsParPageSelectOptions"
|
|
121
|
-
density="compact"
|
|
122
|
-
variant="outlined"
|
|
123
|
-
hide-details
|
|
124
|
-
style="max-width: 90px"
|
|
125
|
-
/>
|
|
126
|
-
</div>
|
|
127
|
-
<nav aria-label="Pagination">
|
|
128
|
-
<button
|
|
129
|
-
variant="text"
|
|
130
|
-
v-if="pageActuelle > 1"
|
|
131
|
-
class="piv-page-btn"
|
|
132
|
-
aria-label="Page précédente"
|
|
133
|
-
@click="pageActuelle--"
|
|
134
|
-
>
|
|
135
|
-
<v-icon size="24">mdi-chevron-left</v-icon>
|
|
136
|
-
</button>
|
|
137
|
-
|
|
138
|
-
<template
|
|
139
|
-
v-for="item in obtenirPages(pageCount)"
|
|
140
|
-
:key="`page-${item}`"
|
|
141
|
-
>
|
|
142
|
-
<span
|
|
143
|
-
v-if="item === '...'"
|
|
144
|
-
class="piv-ellipsis"
|
|
145
|
-
aria-hidden="true"
|
|
146
|
-
>…</span
|
|
147
|
-
>
|
|
148
|
-
<button
|
|
149
|
-
v-else
|
|
150
|
-
variant="text"
|
|
151
|
-
class="piv-page-btn"
|
|
152
|
-
:class="{ 'piv-page-active': item === pageActuelle }"
|
|
153
|
-
:aria-label="`Page ${item}`"
|
|
154
|
-
:aria-current="item === pageActuelle ? 'page' : undefined"
|
|
155
|
-
@click="pageActuelle = item as number"
|
|
156
|
-
>
|
|
157
|
-
{{ item }}
|
|
158
|
-
</button>
|
|
159
|
-
</template>
|
|
160
|
-
|
|
161
|
-
<button
|
|
162
|
-
v-if="pageActuelle < pageCount"
|
|
163
|
-
class="piv-page-btn"
|
|
164
|
-
aria-label="Page suivante"
|
|
165
|
-
@click="pageActuelle++"
|
|
166
|
-
>
|
|
167
|
-
<v-icon size="24">mdi-chevron-right</v-icon>
|
|
168
|
-
</button>
|
|
169
|
-
</nav>
|
|
170
|
-
<span class="piv-total text-caption text-medium-emphasis">
|
|
171
|
-
{{ filteredItems.length }} {{ $t('csqc.pagination.total') }}
|
|
172
|
-
</span>
|
|
173
|
-
</div>
|
|
174
|
-
</template>
|
|
175
|
-
|
|
176
|
-
<!-- boutons supprimer et modifier -->
|
|
177
|
-
<!-- eslint-disable-next-line -->
|
|
178
|
-
<template v-slot:item.action="{ item }">
|
|
179
|
-
<slot name="actionsCustom"></slot>
|
|
180
|
-
<v-icon
|
|
181
|
-
v-if="props.btnSupprimer"
|
|
182
|
-
size="large"
|
|
183
|
-
class="iconeSupprimer float-right"
|
|
184
|
-
@click.stop.prevent="ouvrirModaleSupprimer(item)"
|
|
185
|
-
>
|
|
186
|
-
mdi-delete
|
|
187
|
-
</v-icon>
|
|
188
|
-
|
|
189
|
-
<v-icon
|
|
190
|
-
v-if="props.btnModifier"
|
|
191
|
-
size="large"
|
|
192
|
-
class="iconeEditer float-right"
|
|
193
|
-
@click.stop.prevent="modifier(item)"
|
|
194
|
-
>
|
|
195
|
-
mdi-pencil
|
|
196
|
-
</v-icon>
|
|
197
|
-
</template>
|
|
198
|
-
</v-data-table>
|
|
199
|
-
|
|
200
|
-
<!-- Fenêtre de suppression -->
|
|
201
|
-
<confirmation
|
|
202
|
-
v-if="props.btnSupprimer"
|
|
203
|
-
ref="modaleSupprimer"
|
|
204
|
-
:texte="supprimerTexte"
|
|
205
|
-
:titre="supprimerTitreTexte"
|
|
206
|
-
:largeur="modaleSupprimerLargeur"
|
|
207
|
-
@confirmer="supprimer"
|
|
208
|
-
/>
|
|
209
|
-
<modale-choix
|
|
210
|
-
v-if="permettreChoixColonnes"
|
|
211
|
-
ref="modaleChoix"
|
|
212
|
-
:urlbase="urlbase"
|
|
213
|
-
:formulaire-id="formulaireId"
|
|
214
|
-
:identifiant="identifiant"
|
|
215
|
-
:colonnes="colonnesPourChoix"
|
|
216
|
-
:choix-origine="choixOrigineNormalise"
|
|
217
|
-
@selection="selectionChoix"
|
|
218
|
-
@sauvegarde="sauvegardeChoix"
|
|
219
|
-
/>
|
|
220
|
-
</v-col>
|
|
221
|
-
</v-row>
|
|
222
|
-
</div>
|
|
223
|
-
</template>
|
|
224
|
-
<script setup lang="ts">
|
|
225
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
226
|
-
import { computed, onBeforeUnmount, nextTick, onMounted, ref, watch, type PropType, type Slots } from 'vue'
|
|
227
|
-
import { useI18n } from 'vue-i18n'
|
|
228
|
-
import type { SortItem } from 'vuetify/lib/components/VDataTable/composables/sort.mjs'
|
|
229
|
-
|
|
230
|
-
import Recherche from '../csqcRecherche.vue'
|
|
231
|
-
import confirmation from '../csqcConfirmation.vue'
|
|
232
|
-
import exportExcelComponent from './csqcTableExportExcel.vue'
|
|
233
|
-
import ModaleChoix from './csqcTableModaleChoixColonnes.vue'
|
|
234
|
-
import axios from '../../outils/appAxios'
|
|
235
|
-
import Colonne from '../../modeles/composants/datatableColonne'
|
|
236
|
-
import Sortable from 'sortablejs'
|
|
237
|
-
|
|
238
|
-
type VueColonnes = {
|
|
239
|
-
nomVue: string
|
|
240
|
-
colonnes: string[]
|
|
241
|
-
defaut?: boolean
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const slots = defineSlots<Slots>()
|
|
245
|
-
const emit = defineEmits([
|
|
246
|
-
'ajouter',
|
|
247
|
-
'cliqueLigne',
|
|
248
|
-
'supprimer',
|
|
249
|
-
'modifier',
|
|
250
|
-
'donneesExportees',
|
|
251
|
-
'panneau:etat',
|
|
252
|
-
'ordreRangeeModifie',
|
|
253
|
-
])
|
|
254
|
-
|
|
255
|
-
// ============================================================================
|
|
256
|
-
// Props
|
|
257
|
-
// ============================================================================
|
|
258
|
-
const props = defineProps({
|
|
259
|
-
barreHautAfficher: { type: Boolean, default: true },
|
|
260
|
-
|
|
261
|
-
btnAjouter: { type: Boolean, default: true },
|
|
262
|
-
btnAjouterTexte: { type: String, default: '' },
|
|
263
|
-
btnModifier: { type: Boolean, default: true },
|
|
264
|
-
btnSupprimer: { type: Boolean, default: true },
|
|
265
|
-
|
|
266
|
-
chargementListe: { type: Boolean, default: false },
|
|
267
|
-
operationEnCours: { type: Boolean, default: false },
|
|
268
|
-
|
|
269
|
-
// Headers Vuetify
|
|
270
|
-
colonnes: {
|
|
271
|
-
type: Array as PropType<Colonne[]>,
|
|
272
|
-
default: (): Colonne[] => [],
|
|
273
|
-
},
|
|
274
|
-
|
|
275
|
-
densite: { type: String as PropType<'default' | 'comfortable' | 'compact'>, default: 'default' },
|
|
276
|
-
|
|
277
|
-
excel: { type: Boolean, default: false },
|
|
278
|
-
excelNomFichier: { type: String, default: 'csqc' },
|
|
279
|
-
|
|
280
|
-
// Choix colonnes
|
|
281
|
-
formulaireId: { type: Number, default: -1 },
|
|
282
|
-
identifiant: { type: String, default: '' },
|
|
283
|
-
urlbase: { type: String, default: '' },
|
|
284
|
-
permettreChoixColonnes: { type: Boolean, default: false },
|
|
285
|
-
|
|
286
|
-
// Table
|
|
287
|
-
itemKey: { type: String, default: 'id' },
|
|
288
|
-
itemsParPage: { type: Number, default:
|
|
289
|
-
itemsParPageOptions: { type: Array, default: () => [10, 25,
|
|
290
|
-
liste: {
|
|
291
|
-
type: Array as PropType<Record<string, any>[]>,
|
|
292
|
-
default: (): Record<string, any>[] => [],
|
|
293
|
-
},
|
|
294
|
-
hover: { type: Boolean, default: false },
|
|
295
|
-
|
|
296
|
-
// Recherche
|
|
297
|
-
rechercheTexte: { type: String, default: '' },
|
|
298
|
-
rechercheAfficher: { type: Boolean, default: true },
|
|
299
|
-
rechercheAvancee: { type: Boolean, default: false },
|
|
300
|
-
rechercheAvanceeTexte: { type: String, default: '' },
|
|
301
|
-
rechercheAvanceeLargeur: { type: Number, default: 12 },
|
|
302
|
-
rechercheAvanceeStyle: { type: String, default: '' },
|
|
303
|
-
|
|
304
|
-
// Style en-tête tableau (design Québec)
|
|
305
|
-
styleEntete: {
|
|
306
|
-
type: String as PropType<'standard' | 'standardGris' | 'simplifie'>,
|
|
307
|
-
default: 'standard',
|
|
308
|
-
},
|
|
309
|
-
|
|
310
|
-
// Modale suppression
|
|
311
|
-
modaleSupprimerChamp: { type: String, default: '' },
|
|
312
|
-
modaleSupprimerTexte: { type: String, default: '' },
|
|
313
|
-
modaleSupprimerTitre: { type: String, default: '' },
|
|
314
|
-
modaleSupprimerLargeur: { type: String, default: '525px' },
|
|
315
|
-
|
|
316
|
-
// Tri
|
|
317
|
-
triDescDepart: { type: Boolean, default: false },
|
|
318
|
-
triParDepart: {
|
|
319
|
-
type: [Array, String] as PropType<string | string[] | SortItem[] | undefined>,
|
|
320
|
-
default: undefined,
|
|
321
|
-
},
|
|
322
|
-
|
|
323
|
-
// Ordre manuel (drag & drop)
|
|
324
|
-
modificateurOrdreChamp: { type: String, default: undefined },
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
const { t } = useI18n({ useScope: 'global' })
|
|
328
|
-
const recherche = ref<string>('')
|
|
329
|
-
const pageActuelle = ref(1)
|
|
330
|
-
const itemsParPageLocal = ref(props.itemsParPage)
|
|
331
|
-
|
|
332
|
-
watch(
|
|
333
|
-
() => props.itemsParPage,
|
|
334
|
-
val => {
|
|
335
|
-
itemsParPageLocal.value = val
|
|
336
|
-
},
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
watch(itemsParPageLocal, () => {
|
|
340
|
-
pageActuelle.value = 1
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
const classeEntete = computed(() => ({
|
|
344
|
-
'entete-standard': props.styleEntete === 'standard',
|
|
345
|
-
'entete-standard-gris': props.styleEntete === 'standardGris',
|
|
346
|
-
'entete-simplifie': props.styleEntete === 'simplifie',
|
|
347
|
-
}))
|
|
348
|
-
|
|
349
|
-
const itemsParPageSelectOptions = computed(() =>
|
|
350
|
-
(props.itemsParPageOptions as number[]).map(val =>
|
|
351
|
-
val === -1 ? { title: t('csqc.pagination.tous'), value: -1 } : { title: String(val), value: val },
|
|
352
|
-
),
|
|
353
|
-
)
|
|
354
|
-
|
|
355
|
-
const pageCount = computed(() => {
|
|
356
|
-
const total = filteredItems.value.length
|
|
357
|
-
if (!itemsParPageLocal.value || itemsParPageLocal.value <= 0) return 1
|
|
358
|
-
return Math.max(1, Math.ceil(total / itemsParPageLocal.value))
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
function obtenirPages(totalPages: number): (number | '...')[] {
|
|
362
|
-
if (totalPages <= 5) return Array.from({ length: totalPages }, (_, i) => i + 1)
|
|
363
|
-
const current = pageActuelle.value
|
|
364
|
-
const pages: (number | '...')[] = []
|
|
365
|
-
const left = Math.max(2, current - 1)
|
|
366
|
-
const right = Math.min(totalPages - 1, current + 1)
|
|
367
|
-
pages.push(1)
|
|
368
|
-
if (left > 2) pages.push('...')
|
|
369
|
-
for (let i = left; i <= right; i++) pages.push(i)
|
|
370
|
-
if (right < totalPages - 1) pages.push('...')
|
|
371
|
-
pages.push(totalPages)
|
|
372
|
-
return pages
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Modale suppression (on garde l’item sélectionné)
|
|
376
|
-
const itemSelectionne = ref<any>(null)
|
|
377
|
-
|
|
378
|
-
// ============================================================================
|
|
379
|
-
// State - Choix de colonnes (vues sauvegardées) + sélection courante
|
|
380
|
-
// ============================================================================
|
|
381
|
-
const datatable = ref<any>(null)
|
|
382
|
-
const listeInterne = ref<Record<string, any>[]>([])
|
|
383
|
-
const modaleChoix = ref<InstanceType<typeof ModaleChoix> | null>(null)
|
|
384
|
-
const openChoixColonnes = ref(false)
|
|
385
|
-
const choix = ref<VueColonnes[]>([]) // la liste des vues disponibles
|
|
386
|
-
const vueActive = ref<VueColonnes | null>(null) // optionnel: la vue "appliquée"
|
|
387
|
-
const valeurSelectionChoixColonnes = ref<string | null>(null) // nomVue sélectionné dans la modale / UI
|
|
388
|
-
|
|
389
|
-
// ============================================================================
|
|
390
|
-
// Charger le choix de colonnes sauvegardé s'il y a lieu
|
|
391
|
-
// ============================================================================
|
|
392
|
-
onMounted(async () => {
|
|
393
|
-
if (!props.permettreChoixColonnes) return
|
|
394
|
-
|
|
395
|
-
try {
|
|
396
|
-
const res: any = await axios
|
|
397
|
-
.getAxios()
|
|
398
|
-
.get(`${props.urlbase}/api/ComposantUI/Colonnes/${props.formulaireId}/Identifiant/${props.identifiant}`)
|
|
399
|
-
|
|
400
|
-
const payload = res?.data ?? res
|
|
401
|
-
|
|
402
|
-
if (!payload) {
|
|
403
|
-
choix.value = []
|
|
404
|
-
return
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
choix.value = typeof payload === 'string' ? JSON.parse(payload) : payload
|
|
408
|
-
} catch (e) {
|
|
409
|
-
choix.value = []
|
|
410
|
-
// console.debug(e)
|
|
411
|
-
}
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
// ============================================================================
|
|
415
|
-
// Computed - Normaliser pour la modale Choix colonne
|
|
416
|
-
// ============================================================================
|
|
417
|
-
const choixOrigineNormalise = computed(() =>
|
|
418
|
-
(choix.value ?? []).map(c => ({
|
|
419
|
-
_id: '',
|
|
420
|
-
nomVue: c.nomVue,
|
|
421
|
-
colonnes: c.colonnes ?? [],
|
|
422
|
-
defaut: c.defaut ?? false,
|
|
423
|
-
})),
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
// ============================================================================
|
|
427
|
-
// Actions - Modale choix colonnes
|
|
428
|
-
// ============================================================================
|
|
429
|
-
function ouvrirChoixColonnes() {
|
|
430
|
-
modaleChoix.value?.ouvrir()
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function appliquerVue(vue: VueColonnes) {
|
|
434
|
-
vueActive.value = vue
|
|
435
|
-
openChoixColonnes.value = false
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function selectionChoix(vue: VueColonnes) {
|
|
439
|
-
// on garde une trace du nom sélectionné (pour retrouver la vue plus tard)
|
|
440
|
-
valeurSelectionChoixColonnes.value = vue.nomVue
|
|
441
|
-
appliquerVue(vue)
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function sauvegardeChoix(vues: VueColonnes[]) {
|
|
445
|
-
if (!props.permettreChoixColonnes) return
|
|
446
|
-
|
|
447
|
-
// si la sélection courante n’existe plus, on reset
|
|
448
|
-
if (
|
|
449
|
-
valeurSelectionChoixColonnes.value != null &&
|
|
450
|
-
vues.find(x => x.nomVue === valeurSelectionChoixColonnes.value) == null
|
|
451
|
-
) {
|
|
452
|
-
valeurSelectionChoixColonnes.value = null
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
choix.value = vues
|
|
456
|
-
|
|
457
|
-
// optionnel: appliquer automatiquement la vue par défaut
|
|
458
|
-
const defaut = vues.find(v => v.defaut)
|
|
459
|
-
if (defaut) appliquerVue(defaut)
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// ============================================================================
|
|
463
|
-
// Computed - Mode ordre manuel
|
|
464
|
-
// ============================================================================
|
|
465
|
-
const estEnModeModificateurOrdre = computed(() => {
|
|
466
|
-
if (!props.modificateurOrdreChamp) return false
|
|
467
|
-
|
|
468
|
-
if (props.liste == null) return false
|
|
469
|
-
|
|
470
|
-
// Vérifie que le champ existe dans les données (pas nécessairement une colonne visible)
|
|
471
|
-
if (!props.liste.length) return false // liste vide, on attend sans avertissement
|
|
472
|
-
|
|
473
|
-
const premierItem = props.liste[0]
|
|
474
|
-
|
|
475
|
-
if (!(props.modificateurOrdreChamp in premierItem)) {
|
|
476
|
-
console.log(
|
|
477
|
-
`[csqcTable] modificateurOrdreChamp="${props.modificateurOrdreChamp}" n'existe pas dans les données. Le mode d'ordonnancement est désactivé.`,
|
|
478
|
-
)
|
|
479
|
-
return false
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return true
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
// ============================================================================
|
|
486
|
-
// Computed - Colonnes à envoyer à la modale (liste ordonnée + libellé)
|
|
487
|
-
// ============================================================================
|
|
488
|
-
const colonnesPourChoix = computed(() =>
|
|
489
|
-
props.colonnes
|
|
490
|
-
// on exclut l'action (souvent toujours visible) + colonnes "cachées"
|
|
491
|
-
.filter(c => c.key && c.key !== 'action' && (c as any).align !== 'd-none')
|
|
492
|
-
.map((c, idx) => ({
|
|
493
|
-
title: (c as any).title ?? (c as any).text ?? String((c as any).key ?? (c as any).value),
|
|
494
|
-
value: String((c as any).key ?? (c as any).value), // ← on force string
|
|
495
|
-
ordre: idx + 1,
|
|
496
|
-
})),
|
|
497
|
-
)
|
|
498
|
-
|
|
499
|
-
// ============================================================================
|
|
500
|
-
// Computed - Headers réellement affichés par la table (selon le choix actif)
|
|
501
|
-
// ============================================================================
|
|
502
|
-
const colonnesAffichees = computed(() => {
|
|
503
|
-
// 1) colonnes "disponibles" (on enlève celles explicitement cachées)
|
|
504
|
-
const colonnesFiltre = props.colonnes.filter(c => (c as any).align !== 'd-none')
|
|
505
|
-
|
|
506
|
-
// 2) résoudre la liste selon les vues sauvegardées (choix colonnes)
|
|
507
|
-
let colonnesBase: Colonne[]
|
|
508
|
-
if (!props.permettreChoixColonnes || !choix.value?.length) {
|
|
509
|
-
colonnesBase = colonnesFiltre
|
|
510
|
-
} else {
|
|
511
|
-
// retrouver la vue sélectionnée (sinon la défaut)
|
|
512
|
-
const choixSelection = choix.value.find(
|
|
513
|
-
x => valeurSelectionChoixColonnes.value === x.nomVue || (!valeurSelectionChoixColonnes.value && x.defaut),
|
|
514
|
-
)
|
|
515
|
-
colonnesBase = choixSelection
|
|
516
|
-
? (choixSelection.colonnes.map(k => colonnesFiltre.find(c => String(c.key) === k)).filter(Boolean) as Colonne[])
|
|
517
|
-
: colonnesFiltre
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// 3) ordre manuel actif : colonne ancre en tête + toutes les colonnes non triables
|
|
521
|
-
if (estEnModeModificateurOrdre.value) {
|
|
522
|
-
const colonneAncre = { key: 'ancre', title: '', sortable: false, width: '40px' } as Colonne
|
|
523
|
-
return [colonneAncre, ...colonnesBase.map(c => ({ ...c, sortable: false }))]
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
return colonnesBase
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
// ============================================================================
|
|
530
|
-
// Computed - Tri initial (Vuetify sort-by)
|
|
531
|
-
// ============================================================================
|
|
532
|
-
const triDepart = computed<SortItem[] | undefined>(() => {
|
|
533
|
-
// ordre manuel : tri forcé sur le champ ordre, croissant
|
|
534
|
-
if (estEnModeModificateurOrdre.value) return [{ key: props.modificateurOrdreChamp!, order: 'asc' }]
|
|
535
|
-
|
|
536
|
-
if (props.triParDepart == null) return undefined
|
|
537
|
-
|
|
538
|
-
const ordre = props.triDescDepart ? 'desc' : 'asc'
|
|
539
|
-
if (typeof props.triParDepart === 'string') return [{ key: props.triParDepart, order: ordre }]
|
|
540
|
-
|
|
541
|
-
const retour: SortItem[] = []
|
|
542
|
-
for (let i = 0; i < props.triParDepart.length; i += 1) {
|
|
543
|
-
const tri = props.triParDepart[i]!
|
|
544
|
-
if (typeof tri === 'string') retour.push({ key: tri, order: ordre })
|
|
545
|
-
else retour.push(tri)
|
|
546
|
-
}
|
|
547
|
-
return retour
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
const estMultiTriActif = computed(() => {
|
|
551
|
-
if (estEnModeModificateurOrdre.value) return false
|
|
552
|
-
if (props.triParDepart == null) return false
|
|
553
|
-
if (typeof props.triParDepart === 'string') return false
|
|
554
|
-
return true
|
|
555
|
-
})
|
|
556
|
-
|
|
557
|
-
// ============================================================================
|
|
558
|
-
// Computed - Export (filtrage local basé sur recherche)
|
|
559
|
-
// ============================================================================
|
|
560
|
-
const filteredItems = computed(() => {
|
|
561
|
-
if (!recherche.value) return props.liste
|
|
562
|
-
const q = recherche.value.toLowerCase()
|
|
563
|
-
return props.liste.filter(item => Object.values(item).some(val => String(val).toLowerCase().includes(q)))
|
|
564
|
-
})
|
|
565
|
-
|
|
566
|
-
const listeEffective = computed(() => (estEnModeModificateurOrdre.value ? listeInterne.value : props.liste))
|
|
567
|
-
|
|
568
|
-
// ============================================================================
|
|
569
|
-
// Drag & drop - Ordre manuel (Sortable.js)
|
|
570
|
-
// ============================================================================
|
|
571
|
-
let sortableInstance: Sortable | null = null
|
|
572
|
-
|
|
573
|
-
// Synchronise listeInterne quand le parent met à jour props.liste
|
|
574
|
-
watch(
|
|
575
|
-
() => props.liste,
|
|
576
|
-
val => {
|
|
577
|
-
listeInterne.value = [...val]
|
|
578
|
-
},
|
|
579
|
-
{ immediate: true, deep: true },
|
|
580
|
-
)
|
|
581
|
-
|
|
582
|
-
// Init/destroy Sortable si la prop change en cours de vie du composant
|
|
583
|
-
watch(estEnModeModificateurOrdre, actif => {
|
|
584
|
-
if (actif) nextTick(initSortable)
|
|
585
|
-
else destroySortable()
|
|
586
|
-
})
|
|
587
|
-
|
|
588
|
-
onMounted(() => {
|
|
589
|
-
if (estEnModeModificateurOrdre.value) nextTick(initSortable)
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
onBeforeUnmount(() => {
|
|
593
|
-
destroySortable()
|
|
594
|
-
})
|
|
595
|
-
|
|
596
|
-
function initSortable() {
|
|
597
|
-
const tableEl = datatable.value?.$el as HTMLElement | undefined
|
|
598
|
-
if (!tableEl) return
|
|
599
|
-
const tbody = tableEl.querySelector('tbody')
|
|
600
|
-
if (!tbody) return
|
|
601
|
-
|
|
602
|
-
destroySortable() // garantit une seule instance active
|
|
603
|
-
|
|
604
|
-
sortableInstance = Sortable.create(tbody, {
|
|
605
|
-
animation: 150,
|
|
606
|
-
handle: '.ancre-drag',
|
|
607
|
-
onEnd(event) {
|
|
608
|
-
const oldIndex = event.oldIndex
|
|
609
|
-
const newIndex = event.newIndex
|
|
610
|
-
if (oldIndex == null || newIndex == null || oldIndex === newIndex) return
|
|
611
|
-
|
|
612
|
-
const items = [...listeInterne.value]
|
|
613
|
-
const moved = items.splice(oldIndex, 1)[0]
|
|
614
|
-
items.splice(newIndex, 0, moved)
|
|
615
|
-
|
|
616
|
-
// Recalcule la valeur du champ ordre pour maintenir la cohérence
|
|
617
|
-
const champ = props.modificateurOrdreChamp!
|
|
618
|
-
items.forEach((item, idx) => {
|
|
619
|
-
item[champ] = idx + 1
|
|
620
|
-
})
|
|
621
|
-
|
|
622
|
-
listeInterne.value = items
|
|
623
|
-
emit('ordreRangeeModifie', [...items])
|
|
624
|
-
},
|
|
625
|
-
})
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function destroySortable() {
|
|
629
|
-
sortableInstance?.destroy()
|
|
630
|
-
sortableInstance = null
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// ============================================================================
|
|
634
|
-
// UI Actions - Recherche / Table events
|
|
635
|
-
// ============================================================================
|
|
636
|
-
function chargerRecherche(val: string) {
|
|
637
|
-
recherche.value = val
|
|
638
|
-
pageActuelle.value = 1
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function ajouter() {
|
|
642
|
-
emit('ajouter')
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function cliqueLigne(_e: Event, { item }: { item: any }) {
|
|
646
|
-
emit('cliqueLigne', item)
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function modifier(item: any) {
|
|
650
|
-
emit('modifier', item)
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// ============================================================================
|
|
654
|
-
// Suppression - Texte + actions
|
|
655
|
-
// ============================================================================
|
|
656
|
-
const supprimerTexte = computed(() => {
|
|
657
|
-
if (itemSelectionne.value == null) return ''
|
|
658
|
-
|
|
659
|
-
if (props.modaleSupprimerTexte) return props.modaleSupprimerTexte
|
|
660
|
-
|
|
661
|
-
return t('csqc.message.supprimerMessage', {
|
|
662
|
-
nom: itemSelectionne?.value?.[props.modaleSupprimerChamp] ?? '',
|
|
663
|
-
})
|
|
664
|
-
})
|
|
665
|
-
|
|
666
|
-
const supprimerTitreTexte = computed(() => props.modaleSupprimerTitre || t('csqc.message.supprimerTitre'))
|
|
667
|
-
|
|
668
|
-
function ouvrirModaleSupprimer(item: any) {
|
|
669
|
-
itemSelectionne.value = item
|
|
670
|
-
emit('supprimer', itemSelectionne.value) // tu sembles gérer la modale ailleurs
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function supprimer() {
|
|
674
|
-
emit('supprimer', itemSelectionne.value)
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// ============================================================================
|
|
678
|
-
// UI - Panneau recherche avancée
|
|
679
|
-
// ============================================================================
|
|
680
|
-
function onPanelChange(val: boolean) {
|
|
681
|
-
emit('panneau:etat', val)
|
|
682
|
-
}
|
|
683
|
-
</script>
|
|
684
|
-
|
|
685
|
-
<style scoped lang="css">
|
|
686
|
-
.ControlesDatatable {
|
|
687
|
-
gap: 4px;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/* datatable contour */
|
|
691
|
-
.v-data-table {
|
|
692
|
-
border: 1px solid #d3d3d3;
|
|
693
|
-
border-radius: 5px;
|
|
694
|
-
overflow: hidden;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/* hover */
|
|
698
|
-
.v-data-table:hover {
|
|
699
|
-
cursor: pointer !important;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/* datatable row */
|
|
703
|
-
.v-data-table .v-table__wrapper > table > thead > tr > td,
|
|
704
|
-
.v-data-table .v-table__wrapper > table > thead > tr th,
|
|
705
|
-
.v-data-table .v-table__wrapper > table tbody > tr > td,
|
|
706
|
-
.v-data-table .v-table__wrapper > table tbody > tr th {
|
|
707
|
-
background-color: #d3d3d375 !important;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
/* datatable header contour */
|
|
711
|
-
.v-data-table .v-table__wrapper > table > thead > tr > th,
|
|
712
|
-
.v-data-table .v-table__wrapper > table tbody > tr > th {
|
|
713
|
-
background-color: white !important;
|
|
714
|
-
border-bottom: 4px #223654 solid !important;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/* datatable header commun */
|
|
718
|
-
:deep(.v-data-table-header__content) {
|
|
719
|
-
font-size: 15px;
|
|
720
|
-
font-weight: 600;
|
|
721
|
-
height: 46px;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/* standard : en-tête bleu moyen */
|
|
725
|
-
:deep(.entete-standard .v-table__wrapper > table > thead > tr > th),
|
|
726
|
-
:deep(.entete-standard .v-data-table-header__content) {
|
|
727
|
-
background-color: rgb(var(--v-theme-bleuMoyen)) !important;
|
|
728
|
-
color: rgb(var(--v-theme-blanc)) !important;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
/* standardGris : en-tête gris pâle */
|
|
732
|
-
:deep(.entete-standard-gris .v-table__wrapper > table > thead > tr > th),
|
|
733
|
-
:deep(.entete-standard-gris .v-data-table-header__content) {
|
|
734
|
-
background-color: rgb(var(--v-theme-grisPale)) !important;
|
|
735
|
-
color: rgb(var(--v-theme-bleuFonce)) !important;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
/* simplifié : en-tête blanc + filet bleuFonce */
|
|
739
|
-
:deep(.entete-simplifie .v-table__wrapper > table > thead > tr > th) {
|
|
740
|
-
background-color: rgb(var(--v-theme-blanc)) !important;
|
|
741
|
-
border-bottom: 4px rgb(var(--v-theme-bleuFonce)) solid !important;
|
|
742
|
-
}
|
|
743
|
-
:deep(.entete-simplifie .v-data-table-header__content) {
|
|
744
|
-
background-color: rgb(var(--v-theme-blanc)) !important;
|
|
745
|
-
color: rgb(var(--v-theme-bleuFonce)) !important;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
/* datatable footer - pagination design Québec */
|
|
749
|
-
.piv-pagination {
|
|
750
|
-
display: grid;
|
|
751
|
-
grid-template-columns: 1fr auto 1fr;
|
|
752
|
-
align-items: center;
|
|
753
|
-
padding: 12px 16px;
|
|
754
|
-
background-color: rgb(var(--v-theme-blanc));
|
|
755
|
-
border-top: 4px rgb(var(--v-theme-bleuFonce)) solid;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
.piv-par-page {
|
|
759
|
-
display: flex;
|
|
760
|
-
align-items: center;
|
|
761
|
-
gap: 8px;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
.piv-total {
|
|
765
|
-
text-align: right;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
.piv-pagination nav {
|
|
769
|
-
display: flex;
|
|
770
|
-
align-items: center;
|
|
771
|
-
gap: 4px;
|
|
772
|
-
margin: 0;
|
|
773
|
-
padding: 0;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
.piv-page-btn {
|
|
777
|
-
min-width: 40px;
|
|
778
|
-
height: 40px;
|
|
779
|
-
padding: 0 10px;
|
|
780
|
-
border: 0px solid rgb(var(--v-theme-grisMoyen));
|
|
781
|
-
background: rgb(var(--v-theme-blanc));
|
|
782
|
-
color: rgb(var(--v-theme-bleuPiv));
|
|
783
|
-
font-size: 16px;
|
|
784
|
-
font-weight: 600;
|
|
785
|
-
cursor: pointer;
|
|
786
|
-
border-radius: 4px;
|
|
787
|
-
display: inline-flex;
|
|
788
|
-
align-items: center;
|
|
789
|
-
justify-content: center;
|
|
790
|
-
transition:
|
|
791
|
-
background-color 0.2s,
|
|
792
|
-
color 0.2s;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
.piv-page-btn:hover:not(.piv-page-active) {
|
|
796
|
-
background-color: rgb(var(--v-theme-bleuPale));
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
.piv-page-btn:focus-visible {
|
|
800
|
-
outline: 3px solid rgb(var(--v-theme-bleuPiv));
|
|
801
|
-
outline-offset: 2px;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
.piv-page-active {
|
|
805
|
-
color: rgb(var(--v-theme-noir));
|
|
806
|
-
font-weight: bolder;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
.piv-ellipsis {
|
|
810
|
-
padding: 0 6px;
|
|
811
|
-
color: rgb(var(--v-theme-bleuFonce));
|
|
812
|
-
font-size: 16px;
|
|
813
|
-
font-weight: 600;
|
|
814
|
-
display: inline-flex;
|
|
815
|
-
align-items: center;
|
|
816
|
-
height: 40px;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/* datatable row hover */
|
|
820
|
-
:deep(.v-table__wrapper > table tbody > tr:hover > td) {
|
|
821
|
-
background-color: rgb(var(--v-theme-bleuPale)) !important;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
.iconeSupprimer:hover {
|
|
825
|
-
color: red;
|
|
826
|
-
}
|
|
827
|
-
.v-icon.v-icon.v-icon--link.iconeSupprimer:hover {
|
|
828
|
-
color: red !important;
|
|
829
|
-
}
|
|
830
|
-
.iconeEditer:hover {
|
|
831
|
-
color: #095797;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
/* hover icone à droite du ajouter*/
|
|
835
|
-
.text-grisMoyen:hover {
|
|
836
|
-
color: #095797 !important;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
/* icône d'ancre drag & drop */
|
|
840
|
-
.ancre-drag {
|
|
841
|
-
color: #808a9d;
|
|
842
|
-
cursor: grab;
|
|
843
|
-
}
|
|
844
|
-
.ancre-drag:active {
|
|
845
|
-
cursor: grabbing;
|
|
846
|
-
}
|
|
847
|
-
</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
|
+
:afficher="rechercheAfficher"
|
|
10
|
+
:recherche-texte="rechercheTexte"
|
|
11
|
+
:chargement="chargementListe"
|
|
12
|
+
:recherche="recherche"
|
|
13
|
+
:recherche-avancee="rechercheAvancee"
|
|
14
|
+
:recherche-avancee-texte="rechercheAvanceeTexte"
|
|
15
|
+
:recherche-avancee-largeur="rechercheAvanceeLargeur"
|
|
16
|
+
:recherche-avancee-style="rechercheAvanceeStyle"
|
|
17
|
+
class="flex-grow-1"
|
|
18
|
+
v-bind="$attrs"
|
|
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
|
+
:colonnes="colonnesAffichees"
|
|
43
|
+
class="mt-1 ml-1 float-right"
|
|
44
|
+
/>
|
|
45
|
+
<v-icon
|
|
46
|
+
v-if="permettreChoixColonnes"
|
|
47
|
+
color="grisMoyen"
|
|
48
|
+
class="mt-1 ml-1 float-right"
|
|
49
|
+
@click.stop="ouvrirChoixColonnes"
|
|
50
|
+
>
|
|
51
|
+
mdi-table-edit
|
|
52
|
+
</v-icon>
|
|
53
|
+
</template>
|
|
54
|
+
<template #rechercheAvanceeTitre>
|
|
55
|
+
<slot name="rechercheAvanceeTitre" />
|
|
56
|
+
</template>
|
|
57
|
+
<template #rechercheAvanceeApresTitre>
|
|
58
|
+
<slot name="rechercheAvanceeApresTitre" />
|
|
59
|
+
</template>
|
|
60
|
+
<template #rechercheAvancee>
|
|
61
|
+
<slot name="rechercheAvancee" />
|
|
62
|
+
</template>
|
|
63
|
+
</Recherche>
|
|
64
|
+
</v-col>
|
|
65
|
+
</v-row>
|
|
66
|
+
|
|
67
|
+
<!-- datatable-->
|
|
68
|
+
<v-row>
|
|
69
|
+
<v-col
|
|
70
|
+
cols="12"
|
|
71
|
+
class="d-flex ControlesDatatable flex-wrap"
|
|
72
|
+
>
|
|
73
|
+
<v-data-table
|
|
74
|
+
ref="datatable"
|
|
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="itemsParPageLocal"
|
|
83
|
+
:item-per-page-options="itemsParPageOptions"
|
|
84
|
+
:sort-by="triDepart"
|
|
85
|
+
:multi-sort="estMultiTriActif"
|
|
86
|
+
v-model:page="pageActuelle"
|
|
87
|
+
:striped="props.styleEntete !== 'simplifie' ? 'even' : undefined"
|
|
88
|
+
:class="classeEntete"
|
|
89
|
+
v-bind="$attrs"
|
|
90
|
+
@click:row="cliqueLigne"
|
|
91
|
+
>
|
|
92
|
+
<!-- utilisation des slots via le component parent-->
|
|
93
|
+
<!-- eslint-disable-next-line -->
|
|
94
|
+
<template
|
|
95
|
+
v-for="(_, slot) of $slots"
|
|
96
|
+
:key="slot"
|
|
97
|
+
#[slot]="scope"
|
|
98
|
+
>
|
|
99
|
+
<slot
|
|
100
|
+
:name="slot"
|
|
101
|
+
v-bind="scope"
|
|
102
|
+
/>
|
|
103
|
+
</template>
|
|
104
|
+
|
|
105
|
+
<!-- Icône ancre (drag & drop) -->
|
|
106
|
+
<template
|
|
107
|
+
v-if="estEnModeModificateurOrdre"
|
|
108
|
+
#item.ancre
|
|
109
|
+
>
|
|
110
|
+
<v-icon class="ancre-drag">mdi-drag-vertical</v-icon>
|
|
111
|
+
</template>
|
|
112
|
+
|
|
113
|
+
<!-- Footer : pagination design Québec -->
|
|
114
|
+
<template #bottom>
|
|
115
|
+
<div class="piv-pagination">
|
|
116
|
+
<div class="piv-par-page">
|
|
117
|
+
<span class="text-caption text-medium-emphasis">{{ $t('csqc.pagination.elementsParPage') }}</span>
|
|
118
|
+
<v-select
|
|
119
|
+
v-model="itemsParPageLocal"
|
|
120
|
+
:items="itemsParPageSelectOptions"
|
|
121
|
+
density="compact"
|
|
122
|
+
variant="outlined"
|
|
123
|
+
hide-details
|
|
124
|
+
style="max-width: 90px"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
<nav aria-label="Pagination">
|
|
128
|
+
<button
|
|
129
|
+
variant="text"
|
|
130
|
+
v-if="pageActuelle > 1"
|
|
131
|
+
class="piv-page-btn"
|
|
132
|
+
aria-label="Page précédente"
|
|
133
|
+
@click="pageActuelle--"
|
|
134
|
+
>
|
|
135
|
+
<v-icon size="24">mdi-chevron-left</v-icon>
|
|
136
|
+
</button>
|
|
137
|
+
|
|
138
|
+
<template
|
|
139
|
+
v-for="item in obtenirPages(pageCount)"
|
|
140
|
+
:key="`page-${item}`"
|
|
141
|
+
>
|
|
142
|
+
<span
|
|
143
|
+
v-if="item === '...'"
|
|
144
|
+
class="piv-ellipsis"
|
|
145
|
+
aria-hidden="true"
|
|
146
|
+
>…</span
|
|
147
|
+
>
|
|
148
|
+
<button
|
|
149
|
+
v-else
|
|
150
|
+
variant="text"
|
|
151
|
+
class="piv-page-btn"
|
|
152
|
+
:class="{ 'piv-page-active': item === pageActuelle }"
|
|
153
|
+
:aria-label="`Page ${item}`"
|
|
154
|
+
:aria-current="item === pageActuelle ? 'page' : undefined"
|
|
155
|
+
@click="pageActuelle = item as number"
|
|
156
|
+
>
|
|
157
|
+
{{ item }}
|
|
158
|
+
</button>
|
|
159
|
+
</template>
|
|
160
|
+
|
|
161
|
+
<button
|
|
162
|
+
v-if="pageActuelle < pageCount"
|
|
163
|
+
class="piv-page-btn"
|
|
164
|
+
aria-label="Page suivante"
|
|
165
|
+
@click="pageActuelle++"
|
|
166
|
+
>
|
|
167
|
+
<v-icon size="24">mdi-chevron-right</v-icon>
|
|
168
|
+
</button>
|
|
169
|
+
</nav>
|
|
170
|
+
<span class="piv-total text-caption text-medium-emphasis">
|
|
171
|
+
{{ filteredItems.length }} {{ $t('csqc.pagination.total') }}
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
</template>
|
|
175
|
+
|
|
176
|
+
<!-- boutons supprimer et modifier -->
|
|
177
|
+
<!-- eslint-disable-next-line -->
|
|
178
|
+
<template v-slot:item.action="{ item }">
|
|
179
|
+
<slot name="actionsCustom"></slot>
|
|
180
|
+
<v-icon
|
|
181
|
+
v-if="props.btnSupprimer"
|
|
182
|
+
size="large"
|
|
183
|
+
class="iconeSupprimer float-right"
|
|
184
|
+
@click.stop.prevent="ouvrirModaleSupprimer(item)"
|
|
185
|
+
>
|
|
186
|
+
mdi-delete
|
|
187
|
+
</v-icon>
|
|
188
|
+
|
|
189
|
+
<v-icon
|
|
190
|
+
v-if="props.btnModifier"
|
|
191
|
+
size="large"
|
|
192
|
+
class="iconeEditer float-right"
|
|
193
|
+
@click.stop.prevent="modifier(item)"
|
|
194
|
+
>
|
|
195
|
+
mdi-pencil
|
|
196
|
+
</v-icon>
|
|
197
|
+
</template>
|
|
198
|
+
</v-data-table>
|
|
199
|
+
|
|
200
|
+
<!-- Fenêtre de suppression -->
|
|
201
|
+
<confirmation
|
|
202
|
+
v-if="props.btnSupprimer"
|
|
203
|
+
ref="modaleSupprimer"
|
|
204
|
+
:texte="supprimerTexte"
|
|
205
|
+
:titre="supprimerTitreTexte"
|
|
206
|
+
:largeur="modaleSupprimerLargeur"
|
|
207
|
+
@confirmer="supprimer"
|
|
208
|
+
/>
|
|
209
|
+
<modale-choix
|
|
210
|
+
v-if="permettreChoixColonnes"
|
|
211
|
+
ref="modaleChoix"
|
|
212
|
+
:urlbase="urlbase"
|
|
213
|
+
:formulaire-id="formulaireId"
|
|
214
|
+
:identifiant="identifiant"
|
|
215
|
+
:colonnes="colonnesPourChoix"
|
|
216
|
+
:choix-origine="choixOrigineNormalise"
|
|
217
|
+
@selection="selectionChoix"
|
|
218
|
+
@sauvegarde="sauvegardeChoix"
|
|
219
|
+
/>
|
|
220
|
+
</v-col>
|
|
221
|
+
</v-row>
|
|
222
|
+
</div>
|
|
223
|
+
</template>
|
|
224
|
+
<script setup lang="ts">
|
|
225
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
226
|
+
import { computed, onBeforeUnmount, nextTick, onMounted, ref, watch, type PropType, type Slots } from 'vue'
|
|
227
|
+
import { useI18n } from 'vue-i18n'
|
|
228
|
+
import type { SortItem } from 'vuetify/lib/components/VDataTable/composables/sort.mjs'
|
|
229
|
+
|
|
230
|
+
import Recherche from '../csqcRecherche.vue'
|
|
231
|
+
import confirmation from '../csqcConfirmation.vue'
|
|
232
|
+
import exportExcelComponent from './csqcTableExportExcel.vue'
|
|
233
|
+
import ModaleChoix from './csqcTableModaleChoixColonnes.vue'
|
|
234
|
+
import axios from '../../outils/appAxios'
|
|
235
|
+
import Colonne from '../../modeles/composants/datatableColonne'
|
|
236
|
+
import Sortable from 'sortablejs'
|
|
237
|
+
|
|
238
|
+
type VueColonnes = {
|
|
239
|
+
nomVue: string
|
|
240
|
+
colonnes: string[]
|
|
241
|
+
defaut?: boolean
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const slots = defineSlots<Slots>()
|
|
245
|
+
const emit = defineEmits([
|
|
246
|
+
'ajouter',
|
|
247
|
+
'cliqueLigne',
|
|
248
|
+
'supprimer',
|
|
249
|
+
'modifier',
|
|
250
|
+
'donneesExportees',
|
|
251
|
+
'panneau:etat',
|
|
252
|
+
'ordreRangeeModifie',
|
|
253
|
+
])
|
|
254
|
+
|
|
255
|
+
// ============================================================================
|
|
256
|
+
// Props
|
|
257
|
+
// ============================================================================
|
|
258
|
+
const props = defineProps({
|
|
259
|
+
barreHautAfficher: { type: Boolean, default: true },
|
|
260
|
+
|
|
261
|
+
btnAjouter: { type: Boolean, default: true },
|
|
262
|
+
btnAjouterTexte: { type: String, default: '' },
|
|
263
|
+
btnModifier: { type: Boolean, default: true },
|
|
264
|
+
btnSupprimer: { type: Boolean, default: true },
|
|
265
|
+
|
|
266
|
+
chargementListe: { type: Boolean, default: false },
|
|
267
|
+
operationEnCours: { type: Boolean, default: false },
|
|
268
|
+
|
|
269
|
+
// Headers Vuetify
|
|
270
|
+
colonnes: {
|
|
271
|
+
type: Array as PropType<Colonne[]>,
|
|
272
|
+
default: (): Colonne[] => [],
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
densite: { type: String as PropType<'default' | 'comfortable' | 'compact'>, default: 'default' },
|
|
276
|
+
|
|
277
|
+
excel: { type: Boolean, default: false },
|
|
278
|
+
excelNomFichier: { type: String, default: 'csqc' },
|
|
279
|
+
|
|
280
|
+
// Choix colonnes
|
|
281
|
+
formulaireId: { type: Number, default: -1 },
|
|
282
|
+
identifiant: { type: String, default: '' },
|
|
283
|
+
urlbase: { type: String, default: '' },
|
|
284
|
+
permettreChoixColonnes: { type: Boolean, default: false },
|
|
285
|
+
|
|
286
|
+
// Table
|
|
287
|
+
itemKey: { type: String, default: 'id' },
|
|
288
|
+
itemsParPage: { type: Number, default: 25 },
|
|
289
|
+
itemsParPageOptions: { type: Array, default: () => [10, 25, 50, 100, -1] },
|
|
290
|
+
liste: {
|
|
291
|
+
type: Array as PropType<Record<string, any>[]>,
|
|
292
|
+
default: (): Record<string, any>[] => [],
|
|
293
|
+
},
|
|
294
|
+
hover: { type: Boolean, default: false },
|
|
295
|
+
|
|
296
|
+
// Recherche
|
|
297
|
+
rechercheTexte: { type: String, default: '' },
|
|
298
|
+
rechercheAfficher: { type: Boolean, default: true },
|
|
299
|
+
rechercheAvancee: { type: Boolean, default: false },
|
|
300
|
+
rechercheAvanceeTexte: { type: String, default: '' },
|
|
301
|
+
rechercheAvanceeLargeur: { type: Number, default: 12 },
|
|
302
|
+
rechercheAvanceeStyle: { type: String, default: '' },
|
|
303
|
+
|
|
304
|
+
// Style en-tête tableau (design Québec)
|
|
305
|
+
styleEntete: {
|
|
306
|
+
type: String as PropType<'standard' | 'standardGris' | 'simplifie'>,
|
|
307
|
+
default: 'standard',
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
// Modale suppression
|
|
311
|
+
modaleSupprimerChamp: { type: String, default: '' },
|
|
312
|
+
modaleSupprimerTexte: { type: String, default: '' },
|
|
313
|
+
modaleSupprimerTitre: { type: String, default: '' },
|
|
314
|
+
modaleSupprimerLargeur: { type: String, default: '525px' },
|
|
315
|
+
|
|
316
|
+
// Tri
|
|
317
|
+
triDescDepart: { type: Boolean, default: false },
|
|
318
|
+
triParDepart: {
|
|
319
|
+
type: [Array, String] as PropType<string | string[] | SortItem[] | undefined>,
|
|
320
|
+
default: undefined,
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
// Ordre manuel (drag & drop)
|
|
324
|
+
modificateurOrdreChamp: { type: String, default: undefined },
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
const { t } = useI18n({ useScope: 'global' })
|
|
328
|
+
const recherche = ref<string>('')
|
|
329
|
+
const pageActuelle = ref(1)
|
|
330
|
+
const itemsParPageLocal = ref(props.itemsParPage)
|
|
331
|
+
|
|
332
|
+
watch(
|
|
333
|
+
() => props.itemsParPage,
|
|
334
|
+
val => {
|
|
335
|
+
itemsParPageLocal.value = val
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
watch(itemsParPageLocal, () => {
|
|
340
|
+
pageActuelle.value = 1
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
const classeEntete = computed(() => ({
|
|
344
|
+
'entete-standard': props.styleEntete === 'standard',
|
|
345
|
+
'entete-standard-gris': props.styleEntete === 'standardGris',
|
|
346
|
+
'entete-simplifie': props.styleEntete === 'simplifie',
|
|
347
|
+
}))
|
|
348
|
+
|
|
349
|
+
const itemsParPageSelectOptions = computed(() =>
|
|
350
|
+
(props.itemsParPageOptions as number[]).map(val =>
|
|
351
|
+
val === -1 ? { title: t('csqc.pagination.tous'), value: -1 } : { title: String(val), value: val },
|
|
352
|
+
),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
const pageCount = computed(() => {
|
|
356
|
+
const total = filteredItems.value.length
|
|
357
|
+
if (!itemsParPageLocal.value || itemsParPageLocal.value <= 0) return 1
|
|
358
|
+
return Math.max(1, Math.ceil(total / itemsParPageLocal.value))
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
function obtenirPages(totalPages: number): (number | '...')[] {
|
|
362
|
+
if (totalPages <= 5) return Array.from({ length: totalPages }, (_, i) => i + 1)
|
|
363
|
+
const current = pageActuelle.value
|
|
364
|
+
const pages: (number | '...')[] = []
|
|
365
|
+
const left = Math.max(2, current - 1)
|
|
366
|
+
const right = Math.min(totalPages - 1, current + 1)
|
|
367
|
+
pages.push(1)
|
|
368
|
+
if (left > 2) pages.push('...')
|
|
369
|
+
for (let i = left; i <= right; i++) pages.push(i)
|
|
370
|
+
if (right < totalPages - 1) pages.push('...')
|
|
371
|
+
pages.push(totalPages)
|
|
372
|
+
return pages
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Modale suppression (on garde l’item sélectionné)
|
|
376
|
+
const itemSelectionne = ref<any>(null)
|
|
377
|
+
|
|
378
|
+
// ============================================================================
|
|
379
|
+
// State - Choix de colonnes (vues sauvegardées) + sélection courante
|
|
380
|
+
// ============================================================================
|
|
381
|
+
const datatable = ref<any>(null)
|
|
382
|
+
const listeInterne = ref<Record<string, any>[]>([])
|
|
383
|
+
const modaleChoix = ref<InstanceType<typeof ModaleChoix> | null>(null)
|
|
384
|
+
const openChoixColonnes = ref(false)
|
|
385
|
+
const choix = ref<VueColonnes[]>([]) // la liste des vues disponibles
|
|
386
|
+
const vueActive = ref<VueColonnes | null>(null) // optionnel: la vue "appliquée"
|
|
387
|
+
const valeurSelectionChoixColonnes = ref<string | null>(null) // nomVue sélectionné dans la modale / UI
|
|
388
|
+
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// Charger le choix de colonnes sauvegardé s'il y a lieu
|
|
391
|
+
// ============================================================================
|
|
392
|
+
onMounted(async () => {
|
|
393
|
+
if (!props.permettreChoixColonnes) return
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const res: any = await axios
|
|
397
|
+
.getAxios()
|
|
398
|
+
.get(`${props.urlbase}/api/ComposantUI/Colonnes/${props.formulaireId}/Identifiant/${props.identifiant}`)
|
|
399
|
+
|
|
400
|
+
const payload = res?.data ?? res
|
|
401
|
+
|
|
402
|
+
if (!payload) {
|
|
403
|
+
choix.value = []
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
choix.value = typeof payload === 'string' ? JSON.parse(payload) : payload
|
|
408
|
+
} catch (e) {
|
|
409
|
+
choix.value = []
|
|
410
|
+
// console.debug(e)
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
// ============================================================================
|
|
415
|
+
// Computed - Normaliser pour la modale Choix colonne
|
|
416
|
+
// ============================================================================
|
|
417
|
+
const choixOrigineNormalise = computed(() =>
|
|
418
|
+
(choix.value ?? []).map(c => ({
|
|
419
|
+
_id: '',
|
|
420
|
+
nomVue: c.nomVue,
|
|
421
|
+
colonnes: c.colonnes ?? [],
|
|
422
|
+
defaut: c.defaut ?? false,
|
|
423
|
+
})),
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
// ============================================================================
|
|
427
|
+
// Actions - Modale choix colonnes
|
|
428
|
+
// ============================================================================
|
|
429
|
+
function ouvrirChoixColonnes() {
|
|
430
|
+
modaleChoix.value?.ouvrir()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function appliquerVue(vue: VueColonnes) {
|
|
434
|
+
vueActive.value = vue
|
|
435
|
+
openChoixColonnes.value = false
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function selectionChoix(vue: VueColonnes) {
|
|
439
|
+
// on garde une trace du nom sélectionné (pour retrouver la vue plus tard)
|
|
440
|
+
valeurSelectionChoixColonnes.value = vue.nomVue
|
|
441
|
+
appliquerVue(vue)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function sauvegardeChoix(vues: VueColonnes[]) {
|
|
445
|
+
if (!props.permettreChoixColonnes) return
|
|
446
|
+
|
|
447
|
+
// si la sélection courante n’existe plus, on reset
|
|
448
|
+
if (
|
|
449
|
+
valeurSelectionChoixColonnes.value != null &&
|
|
450
|
+
vues.find(x => x.nomVue === valeurSelectionChoixColonnes.value) == null
|
|
451
|
+
) {
|
|
452
|
+
valeurSelectionChoixColonnes.value = null
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
choix.value = vues
|
|
456
|
+
|
|
457
|
+
// optionnel: appliquer automatiquement la vue par défaut
|
|
458
|
+
const defaut = vues.find(v => v.defaut)
|
|
459
|
+
if (defaut) appliquerVue(defaut)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ============================================================================
|
|
463
|
+
// Computed - Mode ordre manuel
|
|
464
|
+
// ============================================================================
|
|
465
|
+
const estEnModeModificateurOrdre = computed(() => {
|
|
466
|
+
if (!props.modificateurOrdreChamp) return false
|
|
467
|
+
|
|
468
|
+
if (props.liste == null) return false
|
|
469
|
+
|
|
470
|
+
// Vérifie que le champ existe dans les données (pas nécessairement une colonne visible)
|
|
471
|
+
if (!props.liste.length) return false // liste vide, on attend sans avertissement
|
|
472
|
+
|
|
473
|
+
const premierItem = props.liste[0]
|
|
474
|
+
|
|
475
|
+
if (!(props.modificateurOrdreChamp in premierItem)) {
|
|
476
|
+
console.log(
|
|
477
|
+
`[csqcTable] modificateurOrdreChamp="${props.modificateurOrdreChamp}" n'existe pas dans les données. Le mode d'ordonnancement est désactivé.`,
|
|
478
|
+
)
|
|
479
|
+
return false
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return true
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
// ============================================================================
|
|
486
|
+
// Computed - Colonnes à envoyer à la modale (liste ordonnée + libellé)
|
|
487
|
+
// ============================================================================
|
|
488
|
+
const colonnesPourChoix = computed(() =>
|
|
489
|
+
props.colonnes
|
|
490
|
+
// on exclut l'action (souvent toujours visible) + colonnes "cachées"
|
|
491
|
+
.filter(c => c.key && c.key !== 'action' && (c as any).align !== 'd-none')
|
|
492
|
+
.map((c, idx) => ({
|
|
493
|
+
title: (c as any).title ?? (c as any).text ?? String((c as any).key ?? (c as any).value),
|
|
494
|
+
value: String((c as any).key ?? (c as any).value), // ← on force string
|
|
495
|
+
ordre: idx + 1,
|
|
496
|
+
})),
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
// ============================================================================
|
|
500
|
+
// Computed - Headers réellement affichés par la table (selon le choix actif)
|
|
501
|
+
// ============================================================================
|
|
502
|
+
const colonnesAffichees = computed(() => {
|
|
503
|
+
// 1) colonnes "disponibles" (on enlève celles explicitement cachées)
|
|
504
|
+
const colonnesFiltre = props.colonnes.filter(c => (c as any).align !== 'd-none')
|
|
505
|
+
|
|
506
|
+
// 2) résoudre la liste selon les vues sauvegardées (choix colonnes)
|
|
507
|
+
let colonnesBase: Colonne[]
|
|
508
|
+
if (!props.permettreChoixColonnes || !choix.value?.length) {
|
|
509
|
+
colonnesBase = colonnesFiltre
|
|
510
|
+
} else {
|
|
511
|
+
// retrouver la vue sélectionnée (sinon la défaut)
|
|
512
|
+
const choixSelection = choix.value.find(
|
|
513
|
+
x => valeurSelectionChoixColonnes.value === x.nomVue || (!valeurSelectionChoixColonnes.value && x.defaut),
|
|
514
|
+
)
|
|
515
|
+
colonnesBase = choixSelection
|
|
516
|
+
? (choixSelection.colonnes.map(k => colonnesFiltre.find(c => String(c.key) === k)).filter(Boolean) as Colonne[])
|
|
517
|
+
: colonnesFiltre
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 3) ordre manuel actif : colonne ancre en tête + toutes les colonnes non triables
|
|
521
|
+
if (estEnModeModificateurOrdre.value) {
|
|
522
|
+
const colonneAncre = { key: 'ancre', title: '', sortable: false, width: '40px' } as Colonne
|
|
523
|
+
return [colonneAncre, ...colonnesBase.map(c => ({ ...c, sortable: false }))]
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return colonnesBase
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
// ============================================================================
|
|
530
|
+
// Computed - Tri initial (Vuetify sort-by)
|
|
531
|
+
// ============================================================================
|
|
532
|
+
const triDepart = computed<SortItem[] | undefined>(() => {
|
|
533
|
+
// ordre manuel : tri forcé sur le champ ordre, croissant
|
|
534
|
+
if (estEnModeModificateurOrdre.value) return [{ key: props.modificateurOrdreChamp!, order: 'asc' }]
|
|
535
|
+
|
|
536
|
+
if (props.triParDepart == null) return undefined
|
|
537
|
+
|
|
538
|
+
const ordre = props.triDescDepart ? 'desc' : 'asc'
|
|
539
|
+
if (typeof props.triParDepart === 'string') return [{ key: props.triParDepart, order: ordre }]
|
|
540
|
+
|
|
541
|
+
const retour: SortItem[] = []
|
|
542
|
+
for (let i = 0; i < props.triParDepart.length; i += 1) {
|
|
543
|
+
const tri = props.triParDepart[i]!
|
|
544
|
+
if (typeof tri === 'string') retour.push({ key: tri, order: ordre })
|
|
545
|
+
else retour.push(tri)
|
|
546
|
+
}
|
|
547
|
+
return retour
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
const estMultiTriActif = computed(() => {
|
|
551
|
+
if (estEnModeModificateurOrdre.value) return false
|
|
552
|
+
if (props.triParDepart == null) return false
|
|
553
|
+
if (typeof props.triParDepart === 'string') return false
|
|
554
|
+
return true
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
// ============================================================================
|
|
558
|
+
// Computed - Export (filtrage local basé sur recherche)
|
|
559
|
+
// ============================================================================
|
|
560
|
+
const filteredItems = computed(() => {
|
|
561
|
+
if (!recherche.value) return props.liste
|
|
562
|
+
const q = recherche.value.toLowerCase()
|
|
563
|
+
return props.liste.filter(item => Object.values(item).some(val => String(val).toLowerCase().includes(q)))
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
const listeEffective = computed(() => (estEnModeModificateurOrdre.value ? listeInterne.value : props.liste))
|
|
567
|
+
|
|
568
|
+
// ============================================================================
|
|
569
|
+
// Drag & drop - Ordre manuel (Sortable.js)
|
|
570
|
+
// ============================================================================
|
|
571
|
+
let sortableInstance: Sortable | null = null
|
|
572
|
+
|
|
573
|
+
// Synchronise listeInterne quand le parent met à jour props.liste
|
|
574
|
+
watch(
|
|
575
|
+
() => props.liste,
|
|
576
|
+
val => {
|
|
577
|
+
listeInterne.value = [...val]
|
|
578
|
+
},
|
|
579
|
+
{ immediate: true, deep: true },
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
// Init/destroy Sortable si la prop change en cours de vie du composant
|
|
583
|
+
watch(estEnModeModificateurOrdre, actif => {
|
|
584
|
+
if (actif) nextTick(initSortable)
|
|
585
|
+
else destroySortable()
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
onMounted(() => {
|
|
589
|
+
if (estEnModeModificateurOrdre.value) nextTick(initSortable)
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
onBeforeUnmount(() => {
|
|
593
|
+
destroySortable()
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
function initSortable() {
|
|
597
|
+
const tableEl = datatable.value?.$el as HTMLElement | undefined
|
|
598
|
+
if (!tableEl) return
|
|
599
|
+
const tbody = tableEl.querySelector('tbody')
|
|
600
|
+
if (!tbody) return
|
|
601
|
+
|
|
602
|
+
destroySortable() // garantit une seule instance active
|
|
603
|
+
|
|
604
|
+
sortableInstance = Sortable.create(tbody, {
|
|
605
|
+
animation: 150,
|
|
606
|
+
handle: '.ancre-drag',
|
|
607
|
+
onEnd(event) {
|
|
608
|
+
const oldIndex = event.oldIndex
|
|
609
|
+
const newIndex = event.newIndex
|
|
610
|
+
if (oldIndex == null || newIndex == null || oldIndex === newIndex) return
|
|
611
|
+
|
|
612
|
+
const items = [...listeInterne.value]
|
|
613
|
+
const moved = items.splice(oldIndex, 1)[0]
|
|
614
|
+
items.splice(newIndex, 0, moved)
|
|
615
|
+
|
|
616
|
+
// Recalcule la valeur du champ ordre pour maintenir la cohérence
|
|
617
|
+
const champ = props.modificateurOrdreChamp!
|
|
618
|
+
items.forEach((item, idx) => {
|
|
619
|
+
item[champ] = idx + 1
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
listeInterne.value = items
|
|
623
|
+
emit('ordreRangeeModifie', [...items])
|
|
624
|
+
},
|
|
625
|
+
})
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function destroySortable() {
|
|
629
|
+
sortableInstance?.destroy()
|
|
630
|
+
sortableInstance = null
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ============================================================================
|
|
634
|
+
// UI Actions - Recherche / Table events
|
|
635
|
+
// ============================================================================
|
|
636
|
+
function chargerRecherche(val: string) {
|
|
637
|
+
recherche.value = val
|
|
638
|
+
pageActuelle.value = 1
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function ajouter() {
|
|
642
|
+
emit('ajouter')
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function cliqueLigne(_e: Event, { item }: { item: any }) {
|
|
646
|
+
emit('cliqueLigne', item)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function modifier(item: any) {
|
|
650
|
+
emit('modifier', item)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ============================================================================
|
|
654
|
+
// Suppression - Texte + actions
|
|
655
|
+
// ============================================================================
|
|
656
|
+
const supprimerTexte = computed(() => {
|
|
657
|
+
if (itemSelectionne.value == null) return ''
|
|
658
|
+
|
|
659
|
+
if (props.modaleSupprimerTexte) return props.modaleSupprimerTexte
|
|
660
|
+
|
|
661
|
+
return t('csqc.message.supprimerMessage', {
|
|
662
|
+
nom: itemSelectionne?.value?.[props.modaleSupprimerChamp] ?? '',
|
|
663
|
+
})
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
const supprimerTitreTexte = computed(() => props.modaleSupprimerTitre || t('csqc.message.supprimerTitre'))
|
|
667
|
+
|
|
668
|
+
function ouvrirModaleSupprimer(item: any) {
|
|
669
|
+
itemSelectionne.value = item
|
|
670
|
+
emit('supprimer', itemSelectionne.value) // tu sembles gérer la modale ailleurs
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function supprimer() {
|
|
674
|
+
emit('supprimer', itemSelectionne.value)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ============================================================================
|
|
678
|
+
// UI - Panneau recherche avancée
|
|
679
|
+
// ============================================================================
|
|
680
|
+
function onPanelChange(val: boolean) {
|
|
681
|
+
emit('panneau:etat', val)
|
|
682
|
+
}
|
|
683
|
+
</script>
|
|
684
|
+
|
|
685
|
+
<style scoped lang="css">
|
|
686
|
+
.ControlesDatatable {
|
|
687
|
+
gap: 4px;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* datatable contour */
|
|
691
|
+
.v-data-table {
|
|
692
|
+
border: 1px solid #d3d3d3;
|
|
693
|
+
border-radius: 5px;
|
|
694
|
+
overflow: hidden;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/* hover */
|
|
698
|
+
.v-data-table:hover {
|
|
699
|
+
cursor: pointer !important;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/* datatable row */
|
|
703
|
+
.v-data-table .v-table__wrapper > table > thead > tr > td,
|
|
704
|
+
.v-data-table .v-table__wrapper > table > thead > tr th,
|
|
705
|
+
.v-data-table .v-table__wrapper > table tbody > tr > td,
|
|
706
|
+
.v-data-table .v-table__wrapper > table tbody > tr th {
|
|
707
|
+
background-color: #d3d3d375 !important;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/* datatable header contour */
|
|
711
|
+
.v-data-table .v-table__wrapper > table > thead > tr > th,
|
|
712
|
+
.v-data-table .v-table__wrapper > table tbody > tr > th {
|
|
713
|
+
background-color: white !important;
|
|
714
|
+
border-bottom: 4px #223654 solid !important;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/* datatable header commun */
|
|
718
|
+
:deep(.v-data-table-header__content) {
|
|
719
|
+
font-size: 15px;
|
|
720
|
+
font-weight: 600;
|
|
721
|
+
height: 46px;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/* standard : en-tête bleu moyen */
|
|
725
|
+
:deep(.entete-standard .v-table__wrapper > table > thead > tr > th),
|
|
726
|
+
:deep(.entete-standard .v-data-table-header__content) {
|
|
727
|
+
background-color: rgb(var(--v-theme-bleuMoyen)) !important;
|
|
728
|
+
color: rgb(var(--v-theme-blanc)) !important;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/* standardGris : en-tête gris pâle */
|
|
732
|
+
:deep(.entete-standard-gris .v-table__wrapper > table > thead > tr > th),
|
|
733
|
+
:deep(.entete-standard-gris .v-data-table-header__content) {
|
|
734
|
+
background-color: rgb(var(--v-theme-grisPale)) !important;
|
|
735
|
+
color: rgb(var(--v-theme-bleuFonce)) !important;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/* simplifié : en-tête blanc + filet bleuFonce */
|
|
739
|
+
:deep(.entete-simplifie .v-table__wrapper > table > thead > tr > th) {
|
|
740
|
+
background-color: rgb(var(--v-theme-blanc)) !important;
|
|
741
|
+
border-bottom: 4px rgb(var(--v-theme-bleuFonce)) solid !important;
|
|
742
|
+
}
|
|
743
|
+
:deep(.entete-simplifie .v-data-table-header__content) {
|
|
744
|
+
background-color: rgb(var(--v-theme-blanc)) !important;
|
|
745
|
+
color: rgb(var(--v-theme-bleuFonce)) !important;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/* datatable footer - pagination design Québec */
|
|
749
|
+
.piv-pagination {
|
|
750
|
+
display: grid;
|
|
751
|
+
grid-template-columns: 1fr auto 1fr;
|
|
752
|
+
align-items: center;
|
|
753
|
+
padding: 12px 16px;
|
|
754
|
+
background-color: rgb(var(--v-theme-blanc));
|
|
755
|
+
border-top: 4px rgb(var(--v-theme-bleuFonce)) solid;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.piv-par-page {
|
|
759
|
+
display: flex;
|
|
760
|
+
align-items: center;
|
|
761
|
+
gap: 8px;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.piv-total {
|
|
765
|
+
text-align: right;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.piv-pagination nav {
|
|
769
|
+
display: flex;
|
|
770
|
+
align-items: center;
|
|
771
|
+
gap: 4px;
|
|
772
|
+
margin: 0;
|
|
773
|
+
padding: 0;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.piv-page-btn {
|
|
777
|
+
min-width: 40px;
|
|
778
|
+
height: 40px;
|
|
779
|
+
padding: 0 10px;
|
|
780
|
+
border: 0px solid rgb(var(--v-theme-grisMoyen));
|
|
781
|
+
background: rgb(var(--v-theme-blanc));
|
|
782
|
+
color: rgb(var(--v-theme-bleuPiv));
|
|
783
|
+
font-size: 16px;
|
|
784
|
+
font-weight: 600;
|
|
785
|
+
cursor: pointer;
|
|
786
|
+
border-radius: 4px;
|
|
787
|
+
display: inline-flex;
|
|
788
|
+
align-items: center;
|
|
789
|
+
justify-content: center;
|
|
790
|
+
transition:
|
|
791
|
+
background-color 0.2s,
|
|
792
|
+
color 0.2s;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.piv-page-btn:hover:not(.piv-page-active) {
|
|
796
|
+
background-color: rgb(var(--v-theme-bleuPale));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.piv-page-btn:focus-visible {
|
|
800
|
+
outline: 3px solid rgb(var(--v-theme-bleuPiv));
|
|
801
|
+
outline-offset: 2px;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.piv-page-active {
|
|
805
|
+
color: rgb(var(--v-theme-noir));
|
|
806
|
+
font-weight: bolder;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.piv-ellipsis {
|
|
810
|
+
padding: 0 6px;
|
|
811
|
+
color: rgb(var(--v-theme-bleuFonce));
|
|
812
|
+
font-size: 16px;
|
|
813
|
+
font-weight: 600;
|
|
814
|
+
display: inline-flex;
|
|
815
|
+
align-items: center;
|
|
816
|
+
height: 40px;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/* datatable row hover */
|
|
820
|
+
:deep(.v-table__wrapper > table tbody > tr:hover > td) {
|
|
821
|
+
background-color: rgb(var(--v-theme-bleuPale)) !important;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.iconeSupprimer:hover {
|
|
825
|
+
color: red;
|
|
826
|
+
}
|
|
827
|
+
.v-icon.v-icon.v-icon--link.iconeSupprimer:hover {
|
|
828
|
+
color: red !important;
|
|
829
|
+
}
|
|
830
|
+
.iconeEditer:hover {
|
|
831
|
+
color: #095797;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/* hover icone à droite du ajouter*/
|
|
835
|
+
.text-grisMoyen:hover {
|
|
836
|
+
color: #095797 !important;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/* icône d'ancre drag & drop */
|
|
840
|
+
.ancre-drag {
|
|
841
|
+
color: #808a9d;
|
|
842
|
+
cursor: grab;
|
|
843
|
+
}
|
|
844
|
+
.ancre-drag:active {
|
|
845
|
+
cursor: grabbing;
|
|
846
|
+
}
|
|
847
|
+
</style>
|