codevdesign 1.0.63 → 1.0.65

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.
@@ -74,7 +74,7 @@
74
74
  v-bind="$attrs"
75
75
  :headers="colonnesAffichees"
76
76
  :item-key="itemKey"
77
- :items="liste"
77
+ :items="listeEffective"
78
78
  :search="recherche"
79
79
  :loading="chargementListe"
80
80
  :hover="hover"
@@ -98,6 +98,14 @@
98
98
  />
99
99
  </template>
100
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
+
101
109
  <!-- boutons supprimer et modifier -->
102
110
  <!-- eslint-disable-next-line -->
103
111
  <template v-slot:item.action="{ item }">
@@ -148,7 +156,7 @@
148
156
  </template>
149
157
  <script setup lang="ts">
150
158
  /* eslint-disable @typescript-eslint/no-explicit-any */
151
- import { computed, onMounted, ref, type PropType, type Slots } from 'vue'
159
+ import { computed, onBeforeUnmount, nextTick, onMounted, ref, watch, type PropType, type Slots } from 'vue'
152
160
  import { useI18n } from 'vue-i18n'
153
161
  import type { SortItem } from 'vuetify/lib/components/VDataTable/composables/sort.mjs'
154
162
 
@@ -158,6 +166,7 @@
158
166
  import ModaleChoix from './csqcTableModaleChoixColonnes.vue'
159
167
  import axios from '../../outils/appAxios'
160
168
  import Colonne from '../../modeles/composants/datatableColonne'
169
+ import Sortable from 'sortablejs'
161
170
 
162
171
  type VueColonnes = {
163
172
  nomVue: string
@@ -166,7 +175,15 @@
166
175
  }
167
176
 
168
177
  const slots = defineSlots<Slots>()
169
- const emit = defineEmits(['ajouter', 'cliqueLigne', 'supprimer', 'modifier', 'donneesExportees', 'panneau:etat'])
178
+ const emit = defineEmits([
179
+ 'ajouter',
180
+ 'cliqueLigne',
181
+ 'supprimer',
182
+ 'modifier',
183
+ 'donneesExportees',
184
+ 'panneau:etat',
185
+ 'ordreRangeeModifie',
186
+ ])
170
187
 
171
188
  // ============================================================================
172
189
  // Props
@@ -229,6 +246,9 @@
229
246
  type: [Array, String] as PropType<string | string[] | SortItem[] | undefined>,
230
247
  default: undefined,
231
248
  },
249
+
250
+ // Ordre manuel (drag & drop)
251
+ modificateurOrdreChamp: { type: String, default: undefined },
232
252
  })
233
253
 
234
254
  const { t } = useI18n({ useScope: 'global' })
@@ -240,6 +260,8 @@
240
260
  // ============================================================================
241
261
  // State - Choix de colonnes (vues sauvegardées) + sélection courante
242
262
  // ============================================================================
263
+ const datatable = ref<any>(null)
264
+ const listeInterne = ref<Record<string, any>[]>([])
243
265
  const modaleChoix = ref<InstanceType<typeof ModaleChoix> | null>(null)
244
266
  const openChoixColonnes = ref(false)
245
267
  const choix = ref<VueColonnes[]>([]) // la liste des vues disponibles
@@ -319,6 +341,29 @@
319
341
  if (defaut) appliquerVue(defaut)
320
342
  }
321
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
+
322
367
  // ============================================================================
323
368
  // Computed - Colonnes à envoyer à la modale (liste ordonnée + libellé)
324
369
  // ============================================================================
@@ -340,24 +385,36 @@
340
385
  // 1) colonnes "disponibles" (on enlève celles explicitement cachées)
341
386
  const colonnesFiltre = props.colonnes.filter(c => (c as any).align !== 'd-none')
342
387
 
343
- // 2) si feature OFF ou pas de vues sauvegardées => on retourne tout
344
- if (!props.permettreChoixColonnes) return colonnesFiltre
345
- if (!choix.value?.length) return colonnesFiltre
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
+ }
346
401
 
347
- // 3) retrouver la vue sélectionnée (sinon la défaut)
348
- const choixSelection = choix.value.find(
349
- x => valeurSelectionChoixColonnes.value === x.nomVue || (!valeurSelectionChoixColonnes.value && x.defaut),
350
- )
351
- if (!choixSelection) return colonnesFiltre
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
+ }
352
407
 
353
- // 4) reconstruire la liste de headers dans l’ordre sauvegardé
354
- return choixSelection.colonnes.map(k => colonnesFiltre.find(c => String(c.key) === k)).filter(Boolean) as Colonne[]
408
+ return colonnesBase
355
409
  })
356
410
 
357
411
  // ============================================================================
358
412
  // Computed - Tri initial (Vuetify sort-by)
359
413
  // ============================================================================
360
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
+
361
418
  if (props.triParDepart == null) return undefined
362
419
 
363
420
  const ordre = props.triDescDepart ? 'desc' : 'asc'
@@ -373,6 +430,7 @@
373
430
  })
374
431
 
375
432
  const estMultiTriActif = computed(() => {
433
+ if (estEnModeModificateurOrdre.value) return false
376
434
  if (props.triParDepart == null) return false
377
435
  if (typeof props.triParDepart === 'string') return false
378
436
  return true
@@ -387,6 +445,73 @@
387
445
  return props.liste.filter(item => Object.values(item).some(val => String(val).toLowerCase().includes(q)))
388
446
  })
389
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 },
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
+
390
515
  // ============================================================================
391
516
  // UI Actions - Recherche / Table events
392
517
  // ============================================================================
@@ -534,4 +659,13 @@
534
659
  .text-grisMoyen:hover {
535
660
  color: #095797 !important;
536
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
+ }
537
671
  </style>
@@ -1,187 +1,188 @@
1
- class RafraichisseurToken {
2
- private intervalleEnSecondes = 15
3
- private secondesAvantExpirationTokenPourRafraichir = 20
4
- private skewSeconds = 5 // marge anti-derives d’horloge
5
- private popupAffiche = false
6
- private timerId: number | null = null
7
-
8
- // Lance une seule fois
9
- public async demarrer(nomTemoin: string | null, urlPortail: string): Promise<void> {
10
- urlPortail = urlPortail.replace(/\/+$/, '')
11
- if (nomTemoin == null || nomTemoin === '') nomTemoin = 'csqc_jeton_secure_expiration'
12
- await this.verifierJeton(nomTemoin, urlPortail)
13
- if (this.timerId != null) return
14
- this.timerId = window.setInterval(() => {
15
- this.verifierJeton(nomTemoin, urlPortail)
16
- }, this.intervalleEnSecondes * 1000)
17
- }
18
-
19
- // Permet d’arrêter le timer (ex: au logout / destroy)
20
- public arreter(): void {
21
- if (this.timerId != null) {
22
- clearInterval(this.timerId)
23
- this.timerId = null
24
- }
25
- }
26
-
27
- public existeJeton = (nomTemoin: string) => {
28
- return this.estJetonValide(nomTemoin)
29
- }
30
-
31
- private async verifierJeton(nomTemoin: string, urlPortail: string): Promise<void> {
32
- if (this.popupAffiche) return
33
-
34
- if (!this.estJetonValide(nomTemoin)) {
35
- this.rafraichir(nomTemoin, urlPortail)
36
- return
37
- }
38
- }
39
-
40
- private estJetonValide(nomTemoin: string): boolean {
41
- if (this.popupAffiche || !nomTemoin) return true //On fait semblant que c'est valide pour ne pas provoquer un autre affichage du popup.
42
-
43
- const tokenEncode = this.lireCookie(nomTemoin)
44
- if (!tokenEncode) {
45
- return false
46
- }
47
-
48
- let token: any
49
- try {
50
- token = this.parseJwt(tokenEncode)
51
- } catch {
52
- return false
53
- }
54
-
55
- const exp = Number(token?.exp)
56
- if (!Number.isFinite(exp)) {
57
- // exp manquant/invalide → tente refresh
58
-
59
- return false
60
- }
61
-
62
- const now = Math.floor(Date.now() / 1000)
63
- const refreshAt = exp - this.secondesAvantExpirationTokenPourRafraichir - this.skewSeconds
64
- if (now >= refreshAt) {
65
- return false
66
- }
67
-
68
- return true
69
- }
70
-
71
- private async rafraichir(nomCookie: string, urlPortail: string): Promise<void> {
72
- if (!nomCookie) return
73
- const url = this.getRefreshUrl(urlPortail)
74
- const controller = new AbortController()
75
- const timeout = setTimeout(() => controller.abort(), 10_000)
76
-
77
- try {
78
- //Première tentative sans iframe, pour la majorité des cas.
79
- const resp = await fetch(url, {
80
- method: 'POST',
81
- credentials: 'include',
82
- headers: { Accept: 'application/json' },
83
- redirect: 'manual',
84
- signal: controller.signal,
85
- })
86
-
87
- // redirection (souvent => login) → traiter comme non auth
88
-
89
- if (resp.type === 'opaqueredirect' || resp.status === 302) {
90
- this.rafraichirParIframe(nomCookie, urlPortail)
91
- return
92
- }
93
-
94
- // OK ou No Content: le cookie devrait être réécrit
95
- if (resp.status === 200 || resp.status === 204) {
96
- const jeton = this.lireCookie(nomCookie)
97
- if (!jeton) this.rafraichirParIframe(nomCookie, urlPortail)
98
- return
99
- }
100
-
101
- // non auth / expiré (401, 419) + IIS timeout (440)
102
- if (resp.status === 401 || resp.status === 419 || resp.status === 440) {
103
- this.rafraichirParIframe(nomCookie, urlPortail)
104
- return
105
- }
106
-
107
- console.warn('Rafraichisseur token: statut inattendu', resp.status)
108
- } catch (err: any) {
109
- if (err?.name === 'AbortError') console.warn('RafraichisseurToken timeout')
110
- else console.error('Erreur rafraichisseur de token', err)
111
- // on réessaiera au prochain tick
112
- } finally {
113
- clearTimeout(timeout)
114
- }
115
- }
116
-
117
- private rafraichirParIframe(nomCookie: string, urlPortail: string): void {
118
- // Pour éviter les cross référence, on créé un iframe qui appel portail et force la MAJ du jeton ou l'invalidation du jeton, si jamais l'utilisateur n'est plus connecté
119
- // ajax vers le refresh
120
- let iframe = document.createElement('iframe')
121
- const url = this.getRefreshUrl(urlPortail)
122
- iframe.src = `${url}?urlRetour=${encodeURI(window.location.href)}`
123
- iframe.id = 'idRafrToken'
124
- iframe.style.display = 'none'
125
- document.body.appendChild(iframe)
126
- iframe.onload = () => {
127
- const jetonCSQC = this.lireCookie(nomCookie)
128
- if (jetonCSQC == null || jetonCSQC === '') {
129
- this.estDeconnecteAzure(urlPortail)
130
- }
131
-
132
- iframe.remove()
133
- }
134
- }
135
-
136
- private estDeconnecteAzure(urlPortail: string): void {
137
- //on envoie au portail, pas le choix
138
-
139
- const retour = encodeURI(window.location.href)
140
- window.open(`${urlPortail}/home/SeConnecter?urlRetour=${retour}`, '_self')
141
- return
142
- }
143
-
144
- public deconnecterPortail(urlPortail: string): void {
145
- window.location.replace(`${urlPortail}/deconnecte?urlRetour=${encodeURIComponent(window.location.href)}`)
146
- }
147
-
148
- private lireCookie(nom: string): string | null {
149
- if (!document.cookie) return null
150
- const cookies = document.cookie.split(';').map(c => c.trim())
151
- for (const cookie of cookies) {
152
- if (cookie.startsWith(`${nom}=`)) {
153
- try {
154
- return decodeURIComponent(cookie.substring(nom.length + 1))
155
- } catch {
156
- return cookie.substring(nom.length + 1)
157
- }
158
- }
159
- }
160
- return null
161
- }
162
-
163
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
- private parseJwt(token: string): any {
165
- const parts = token.split('.')
166
- const base64Url = parts[1]
167
- if (!base64Url) throw new Error('Invalid JWT format')
168
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
169
- const jsonPayload = decodeURIComponent(
170
- atob(base64)
171
- .split('')
172
- .map(c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
173
- .join(''),
174
- )
175
- return JSON.parse(jsonPayload)
176
- }
177
-
178
- // URL refresh selon env (dev = proxy Vite ; prod = portail)
179
- private getRefreshUrl(urlPortail: string): string {
180
- if (import.meta.env.MODE === 'development') return '/portail-refresh'
181
- return urlPortail + '/home/Refresh'
182
- }
183
- }
184
-
185
- // Instance
186
- const rafraichisseurToken = new RafraichisseurToken()
187
- export default rafraichisseurToken
1
+ class RafraichisseurToken {
2
+ private intervalleEnSecondes = 15
3
+ private secondesAvantExpirationTokenPourRafraichir = 20
4
+ private skewSeconds = 5 // marge anti-derives d’horloge
5
+ private popupAffiche = false
6
+ private timerId: number | null = null
7
+
8
+ // Lance une seule fois
9
+ public async demarrer(nomTemoin: string | null, urlPortail: string): Promise<void> {
10
+ urlPortail = urlPortail.replace(/\/+$/, '')
11
+ if (nomTemoin == null || nomTemoin === '') nomTemoin = 'csqc_jeton_secure_expiration'
12
+ await this.verifierJeton(nomTemoin, urlPortail)
13
+ if (this.timerId != null) return
14
+ this.timerId = window.setInterval(() => {
15
+ this.verifierJeton(nomTemoin, urlPortail)
16
+ }, this.intervalleEnSecondes * 1000)
17
+ }
18
+
19
+ // Permet d’arrêter le timer (ex: au logout / destroy)
20
+ public arreter(): void {
21
+ if (this.timerId != null) {
22
+ clearInterval(this.timerId)
23
+ this.timerId = null
24
+ }
25
+ }
26
+
27
+ public existeJeton = (nomTemoin: string) => {
28
+ return this.estJetonValide(nomTemoin)
29
+ }
30
+
31
+ private async verifierJeton(nomTemoin: string, urlPortail: string): Promise<void> {
32
+ if (this.popupAffiche) return
33
+
34
+ if (!this.estJetonValide(nomTemoin)) {
35
+ this.rafraichir(nomTemoin, urlPortail)
36
+ return
37
+ }
38
+ }
39
+
40
+ private estJetonValide(nomTemoin: string): boolean {
41
+ if (this.popupAffiche || !nomTemoin) return true //On fait semblant que c'est valide pour ne pas provoquer un autre affichage du popup.
42
+
43
+ const tokenEncode = this.lireCookie(nomTemoin)
44
+ if (!tokenEncode) {
45
+ return false
46
+ }
47
+
48
+ let token: any
49
+ try {
50
+ token = this.parseJwt(tokenEncode)
51
+ } catch {
52
+ return false
53
+ }
54
+
55
+ const exp = Number(token?.exp)
56
+ if (!Number.isFinite(exp)) {
57
+ // exp manquant/invalide → tente refresh
58
+
59
+ return false
60
+ }
61
+
62
+ const now = Math.floor(Date.now() / 1000)
63
+ const refreshAt = exp - this.secondesAvantExpirationTokenPourRafraichir - this.skewSeconds
64
+ if (now >= refreshAt) {
65
+ return false
66
+ }
67
+
68
+ return true
69
+ }
70
+
71
+ private async rafraichir(nomCookie: string, urlPortail: string): Promise<void> {
72
+ if (!nomCookie) return
73
+ const url = this.getRefreshUrl(urlPortail)
74
+ const controller = new AbortController()
75
+ const timeout = setTimeout(() => controller.abort(), 10_000)
76
+
77
+ try {
78
+ //Première tentative sans iframe, pour la majorité des cas.
79
+ const resp = await fetch(url, {
80
+ method: 'POST',
81
+ credentials: 'include',
82
+ headers: { Accept: 'application/json' },
83
+ redirect: 'manual',
84
+ signal: controller.signal,
85
+ })
86
+
87
+ // redirection (souvent => login) → traiter comme non auth
88
+
89
+ if (resp.type === 'opaqueredirect' || resp.status === 302) {
90
+ this.rafraichirParIframe(nomCookie, urlPortail)
91
+ return
92
+ }
93
+
94
+ // OK ou No Content: le cookie devrait être réécrit
95
+ if (resp.status === 200 || resp.status === 204) {
96
+ const jeton = this.lireCookie(nomCookie)
97
+ if (!jeton) this.rafraichirParIframe(nomCookie, urlPortail)
98
+ return
99
+ }
100
+
101
+ // non auth / expiré (401, 419) + IIS timeout (440)
102
+ if (resp.status === 401 || resp.status === 419 || resp.status === 440) {
103
+ this.rafraichirParIframe(nomCookie, urlPortail)
104
+ return
105
+ }
106
+
107
+ console.warn('Rafraichisseur token: statut inattendu', resp.status)
108
+ } catch (err: any) {
109
+ if (err?.name === 'AbortError') console.warn('RafraichisseurToken timeout')
110
+ else console.error('Erreur rafraichisseur de token', err)
111
+ // on réessaiera au prochain tick
112
+ } finally {
113
+ clearTimeout(timeout)
114
+ }
115
+ }
116
+
117
+ private rafraichirParIframe(nomCookie: string, urlPortail: string): void {
118
+ // Pour éviter les cross référence, on créé un iframe qui appel portail et force la MAJ du jeton ou l'invalidation du jeton, si jamais l'utilisateur n'est plus connecté
119
+ // ajax vers le refresh
120
+ let iframe = document.createElement('iframe')
121
+ const url = this.getRefreshUrl(urlPortail)
122
+ iframe.src = `${url}?urlRetour=${encodeURI(window.location.href)}`
123
+ iframe.id = 'idRafrToken'
124
+ iframe.style.display = 'none'
125
+ document.body.appendChild(iframe)
126
+ iframe.onload = () => {
127
+ const jetonCSQC = this.lireCookie(nomCookie)
128
+ if (jetonCSQC == null || jetonCSQC === '') {
129
+ this.estDeconnecteAzure(urlPortail)
130
+ }
131
+
132
+ iframe.remove()
133
+ }
134
+ }
135
+
136
+ private estDeconnecteAzure(urlPortail: string): void {
137
+ //on envoie au portail, pas le choix
138
+ urlPortail = urlPortail.replace(/\/+$/, '')
139
+ const retour = encodeURI(window.location.href)
140
+ window.open(`${urlPortail}/home/SeConnecter?urlRetour=${retour}`, '_self')
141
+ return
142
+ }
143
+
144
+ public deconnecterPortail(urlPortail: string): void {
145
+ urlPortail = urlPortail.replace(/\/+$/, '')
146
+ window.location.replace(`${urlPortail}/deconnecte?urlRetour=${encodeURIComponent(window.location.href)}`)
147
+ }
148
+
149
+ private lireCookie(nom: string): string | null {
150
+ if (!document.cookie) return null
151
+ const cookies = document.cookie.split(';').map(c => c.trim())
152
+ for (const cookie of cookies) {
153
+ if (cookie.startsWith(`${nom}=`)) {
154
+ try {
155
+ return decodeURIComponent(cookie.substring(nom.length + 1))
156
+ } catch {
157
+ return cookie.substring(nom.length + 1)
158
+ }
159
+ }
160
+ }
161
+ return null
162
+ }
163
+
164
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
165
+ private parseJwt(token: string): any {
166
+ const parts = token.split('.')
167
+ const base64Url = parts[1]
168
+ if (!base64Url) throw new Error('Invalid JWT format')
169
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
170
+ const jsonPayload = decodeURIComponent(
171
+ atob(base64)
172
+ .split('')
173
+ .map(c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
174
+ .join(''),
175
+ )
176
+ return JSON.parse(jsonPayload)
177
+ }
178
+
179
+ // URL refresh selon env (dev = proxy Vite ; prod = portail)
180
+ private getRefreshUrl(urlPortail: string): string {
181
+ if (import.meta.env.MODE === 'development') return '/portail-refresh'
182
+ return urlPortail + '/home/Refresh'
183
+ }
184
+ }
185
+
186
+ // Instance
187
+ const rafraichisseurToken = new RafraichisseurToken()
188
+ export default rafraichisseurToken
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevdesign",
3
- "version": "1.0.63",
3
+ "version": "1.0.65",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",