codevdesign 1.0.24 → 1.0.26
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/csqcCodeBudgetaireGenerique.vue +261 -257
- package/composants/gabarit/pivEntete.vue +205 -203
- package/package.json +1 -1
|
@@ -37,296 +37,300 @@
|
|
|
37
37
|
</template>
|
|
38
38
|
|
|
39
39
|
<script setup lang="ts">
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
40
|
+
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
|
41
|
+
import type { VForm } from 'vuetify/components'
|
|
42
|
+
|
|
43
|
+
const emit = defineEmits<{
|
|
44
|
+
'update:modelValue': [string | null]
|
|
45
|
+
'update:codeBudgetairesProp': [CodeBudgetaireItem[]]
|
|
46
|
+
'update:valide': [boolean]
|
|
47
|
+
}>()
|
|
48
|
+
type CodeBudgetaireItem = string | [string, string]
|
|
49
|
+
|
|
50
|
+
const props = withDefaults(
|
|
51
|
+
defineProps<{
|
|
52
|
+
codeBudgetairesProp: CodeBudgetaireItem[]
|
|
53
|
+
modelValue: string | null
|
|
54
|
+
afficherHint?: boolean
|
|
55
|
+
regleMessageErreur: string
|
|
56
|
+
format?: string
|
|
57
|
+
activerExtension?: boolean
|
|
58
|
+
reglesSupp?: ((v: string) => true | string)[]
|
|
59
|
+
}>(),
|
|
60
|
+
{
|
|
61
|
+
afficherHint: false,
|
|
62
|
+
format: '999-9-99999-999',
|
|
63
|
+
activerExtension: false,
|
|
64
|
+
reglesSupp: () => [],
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const formValide = ref(false)
|
|
69
|
+
const form = ref<VForm | null>(null)
|
|
70
|
+
const codeBudgetaire = ref(props.modelValue ?? '')
|
|
71
|
+
const derniereValeurSauvegardee = ref<string | null>(null)
|
|
72
|
+
const format = props.format
|
|
73
|
+
const activerExtension = props.activerExtension
|
|
74
|
+
|
|
75
|
+
onMounted(() => {
|
|
76
|
+
derniereValeurSauvegardee.value = codeBudgetaire.value
|
|
77
|
+
})
|
|
67
78
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
derniereValeurSauvegardee.value = codeBudgetaire.value
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
const itemsCombobox = computed(() => {
|
|
80
|
-
return props.codeBudgetairesProp.map(item => {
|
|
81
|
-
if (typeof item === 'string') {
|
|
82
|
-
return {
|
|
83
|
-
code: item,
|
|
84
|
-
nom: '', // pas de nom
|
|
85
|
-
}
|
|
86
|
-
}
|
|
79
|
+
const itemsCombobox = computed(() => {
|
|
80
|
+
return props.codeBudgetairesProp.map(item => {
|
|
81
|
+
if (typeof item === 'string') {
|
|
82
|
+
return {
|
|
83
|
+
code: item,
|
|
84
|
+
nom: '', // pas de nom
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const [code, nomBrut] = item
|
|
89
|
+
const nom = (nomBrut ?? '').toString().trim()
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
return { code, nom }
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
const placeholder = computed(() => {
|
|
96
|
+
const base = format.replace(/9/g, '0')
|
|
97
|
+
const extension = activerExtension ? '-XXX/XXX' : ''
|
|
98
|
+
return base + extension
|
|
99
|
+
})
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
const estValide = computed(() => {
|
|
102
|
+
const val = codeBudgetaire.value?.toUpperCase().trim() || ''
|
|
103
|
+
const base = val.slice(0, 15)
|
|
104
|
+
const extension = val.slice(15)
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
if (!/^\d{3}-\d{1}-\d{5}-\d{3}$/.test(base)) return false
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
if (!activerExtension) return val.length === 15
|
|
109
|
+
|
|
110
|
+
if (val.length === 15) return true
|
|
111
|
+
if (val.length !== 22) return false
|
|
112
|
+
if (extension.length !== 7) return false
|
|
113
|
+
|
|
114
|
+
if (extension[3] !== '/') return false
|
|
115
|
+
if (!/^[A-Z0-9/]$/i.test(extension[0]!)) return false
|
|
116
|
+
|
|
117
|
+
const alphanumAt = [1, 2, 4, 5, 6]
|
|
118
|
+
return alphanumAt.every(i => /^[A-Z0-9]$/i.test(extension[i]!))
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// Règles Vuetify combinées (interne + supplémentaires)
|
|
122
|
+
const reglesVuetify = computed(() => [
|
|
123
|
+
// règle interne
|
|
124
|
+
() => (estValide.value ? true : props.regleMessageErreur),
|
|
125
|
+
|
|
126
|
+
// règles supplémentaires venant du parent
|
|
127
|
+
...props.reglesSupp.map(rule => {
|
|
128
|
+
return () => rule(codeBudgetaire.value) // on passe la valeur formatée
|
|
129
|
+
}),
|
|
130
|
+
])
|
|
131
|
+
|
|
132
|
+
// Validité globale du composant (interne + toutes les règles supp)
|
|
133
|
+
const estValideComplet = computed(() => {
|
|
134
|
+
if (!estValide.value) return false
|
|
135
|
+
return props.reglesSupp.every(rule => rule(codeBudgetaire.value) === true)
|
|
136
|
+
})
|
|
109
137
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (extension.length !== 7) return false
|
|
138
|
+
const caractereAutorises = (e: KeyboardEvent) => {
|
|
139
|
+
if (e.ctrlKey || e.metaKey) return
|
|
113
140
|
|
|
114
|
-
|
|
115
|
-
|
|
141
|
+
const touchesSpecifiques = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Home', 'End']
|
|
142
|
+
if (touchesSpecifiques.includes(e.key)) return
|
|
116
143
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
})
|
|
144
|
+
const input = e.target as HTMLInputElement
|
|
145
|
+
let position = input.selectionStart ?? 0
|
|
120
146
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
147
|
+
// Gestion de la partie de base (15 premiers caractères)
|
|
148
|
+
if (position < 15) {
|
|
149
|
+
if (!/^\d$/.test(e.key)) {
|
|
150
|
+
e.preventDefault()
|
|
151
|
+
return
|
|
152
|
+
}
|
|
125
153
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return () => rule(codeBudgetaire.value) // on passe la valeur formatée
|
|
129
|
-
}),
|
|
130
|
-
])
|
|
154
|
+
// Insérer chiffre et auto-ajout des tirets
|
|
155
|
+
e.preventDefault()
|
|
131
156
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (!estValide.value) return false
|
|
135
|
-
return props.reglesSupp.every(rule => rule(codeBudgetaire.value) === true)
|
|
136
|
-
})
|
|
157
|
+
const value = codeBudgetaire.value.replace(/-/g, '')
|
|
158
|
+
const clean = value.slice(0, 12) + e.key
|
|
137
159
|
|
|
138
|
-
|
|
139
|
-
|
|
160
|
+
let formatted = ''
|
|
161
|
+
if (clean.length > 0) formatted += clean.slice(0, 3)
|
|
162
|
+
if (clean.length > 3) formatted += '-' + clean.slice(3, 4)
|
|
163
|
+
if (clean.length > 4) formatted += '-' + clean.slice(4, 9)
|
|
164
|
+
if (clean.length > 9) formatted += '-' + clean.slice(9, 12)
|
|
140
165
|
|
|
141
|
-
|
|
142
|
-
if (touchesSpecifiques.includes(e.key)) return
|
|
166
|
+
codeBudgetaire.value = formatted.slice(0, 15)
|
|
143
167
|
|
|
144
|
-
|
|
145
|
-
|
|
168
|
+
nextTick(() => {
|
|
169
|
+
const newPos = codeBudgetaire.value.length
|
|
170
|
+
input.selectionStart = input.selectionEnd = newPos
|
|
171
|
+
})
|
|
146
172
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- Gestion de l'extension ---
|
|
177
|
+
if (!activerExtension || position >= 22 || codeBudgetaire.value.length >= 22) {
|
|
178
|
+
e.preventDefault()
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const extensionPos = position - 15
|
|
183
|
+
|
|
184
|
+
// Règle 1 : extension[0] = alphanum ou /
|
|
185
|
+
if (extensionPos === 0) {
|
|
186
|
+
if (!/^[A-Z0-9/]$/i.test(e.key)) {
|
|
187
|
+
e.preventDefault()
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Règle 2 : extension[1,2,4,5,6] = alphanum
|
|
194
|
+
if ([1, 2, 4, 5, 6].includes(extensionPos)) {
|
|
195
|
+
if (!/^[A-Z0-9]$/i.test(e.key)) {
|
|
196
|
+
e.preventDefault()
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Règle 3 : slash automatique à position 3 (index 18)
|
|
203
|
+
if (extensionPos === 3) {
|
|
204
|
+
e.preventDefault()
|
|
205
|
+
const before = codeBudgetaire.value.slice(0, position)
|
|
206
|
+
const after = codeBudgetaire.value.slice(position)
|
|
207
|
+
codeBudgetaire.value = before + '/' + after
|
|
208
|
+
nextTick(() => {
|
|
209
|
+
input.selectionStart = input.selectionEnd = position + 1
|
|
210
|
+
})
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Tout autre cas = bloqué
|
|
150
215
|
e.preventDefault()
|
|
151
|
-
return
|
|
152
216
|
}
|
|
153
217
|
|
|
154
|
-
|
|
155
|
-
|
|
218
|
+
const formaterCodeBudgetaire = (valeur: string): string => {
|
|
219
|
+
if (!valeur) return ''
|
|
156
220
|
|
|
157
|
-
|
|
158
|
-
|
|
221
|
+
const upper = valeur.toUpperCase().replace(/[^A-Z0-9/]/g, '')
|
|
222
|
+
const chiffres = upper.replace(/[^0-9]/g, '').slice(0, 12)
|
|
159
223
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
224
|
+
let base = ''
|
|
225
|
+
if (chiffres.length > 0) base += chiffres.slice(0, 3)
|
|
226
|
+
if (chiffres.length > 3) base += '-' + chiffres.slice(3, 4)
|
|
227
|
+
if (chiffres.length > 4) base += '-' + chiffres.slice(4, 9)
|
|
228
|
+
if (chiffres.length > 9) base += '-' + chiffres.slice(9, 12)
|
|
165
229
|
|
|
166
|
-
|
|
230
|
+
if (!activerExtension || base.length < 15) return base
|
|
167
231
|
|
|
168
|
-
|
|
169
|
-
const newPos = codeBudgetaire.value.length
|
|
170
|
-
input.selectionStart = input.selectionEnd = newPos
|
|
171
|
-
})
|
|
232
|
+
const reste = upper.slice(chiffres.length).replace(/[^A-Z0-9/]/gi, '')
|
|
172
233
|
|
|
173
|
-
|
|
174
|
-
|
|
234
|
+
// Extraire les 7 premiers caractères restants pour l’extension
|
|
235
|
+
let ext = reste.slice(0, 7).split('')
|
|
175
236
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
237
|
+
// Ne garder que le premier slash s’il est à l’index 0 ou 3
|
|
238
|
+
ext = ext.filter((c, i) => {
|
|
239
|
+
if (c !== '/') return true
|
|
240
|
+
return i === 0 || i === 3
|
|
241
|
+
})
|
|
181
242
|
|
|
182
|
-
|
|
243
|
+
// Enlever les slash non autorisés
|
|
244
|
+
ext = ext.map((c, i) => {
|
|
245
|
+
if (c === '/' && i !== 0 && i !== 3) return ''
|
|
246
|
+
if (c !== '/' && !/^[A-Z0-9]$/i.test(c)) return ''
|
|
247
|
+
return c
|
|
248
|
+
})
|
|
183
249
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
250
|
+
// Forcer le slash à la 4e position
|
|
251
|
+
if (ext.length > 3) {
|
|
252
|
+
ext[3] = '/'
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Réduire à 7 caractères
|
|
256
|
+
ext = ext.slice(0, 7)
|
|
257
|
+
|
|
258
|
+
return (base + ext.join('')).slice(0, 22)
|
|
189
259
|
}
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
260
|
|
|
193
|
-
|
|
194
|
-
if ([1, 2, 4, 5, 6].includes(extensionPos)) {
|
|
195
|
-
if (!/^[A-Z0-9]$/i.test(e.key)) {
|
|
261
|
+
const gererPaste = (e: ClipboardEvent) => {
|
|
196
262
|
e.preventDefault()
|
|
197
|
-
|
|
263
|
+
const clipboardData = e.clipboardData
|
|
264
|
+
if (!clipboardData) return
|
|
265
|
+
let pasted = clipboardData.getData('text') || ''
|
|
266
|
+
codeBudgetaire.value = formaterCodeBudgetaire(pasted)
|
|
267
|
+
|
|
268
|
+
setTimeout(() => {
|
|
269
|
+
const input = e.target as HTMLInputElement
|
|
270
|
+
input.selectionStart = input.selectionEnd = codeBudgetaire.value.length
|
|
271
|
+
}, 0)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const sauvegarder = () => {
|
|
275
|
+
codeBudgetaire.value = formaterCodeBudgetaire(codeBudgetaire.value)
|
|
276
|
+
|
|
277
|
+
if (!estValideComplet.value) return
|
|
278
|
+
if (codeBudgetaire.value === derniereValeurSauvegardee.value) return
|
|
279
|
+
|
|
280
|
+
const codeNormalise = codeBudgetaire.value.trim().toUpperCase()
|
|
281
|
+
|
|
282
|
+
const existe = props.codeBudgetairesProp.some(item => {
|
|
283
|
+
const code = typeof item === 'string' ? item : item[0]
|
|
284
|
+
return code.trim().toUpperCase() === codeNormalise
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
if (!existe) {
|
|
288
|
+
const nouvelleListe = [...props.codeBudgetairesProp, codeBudgetaire.value]
|
|
289
|
+
emit('update:codeBudgetairesProp', nouvelleListe)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
derniereValeurSauvegardee.value = codeBudgetaire.value
|
|
293
|
+
emit('update:modelValue', codeBudgetaire.value)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const extraireCode = (val: unknown): string => {
|
|
297
|
+
if (val == null) return ''
|
|
298
|
+
if (typeof val === 'string') return val
|
|
299
|
+
|
|
300
|
+
// Cas où Vuetify enverrait { code, label }
|
|
301
|
+
if (typeof val === 'object' && 'code' in (val as Record<string, unknown>)) {
|
|
302
|
+
const obj = val as { code?: unknown }
|
|
303
|
+
return typeof obj.code === 'string' ? obj.code : String(obj.code ?? '')
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return String(val)
|
|
198
307
|
}
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Règle 3 : slash automatique à position 3 (index 18)
|
|
203
|
-
if (extensionPos === 3) {
|
|
204
|
-
e.preventDefault()
|
|
205
|
-
const before = codeBudgetaire.value.slice(0, position)
|
|
206
|
-
const after = codeBudgetaire.value.slice(position)
|
|
207
|
-
codeBudgetaire.value = before + '/' + after
|
|
208
|
-
nextTick(() => {
|
|
209
|
-
input.selectionStart = input.selectionEnd = position + 1
|
|
210
|
-
})
|
|
211
|
-
return
|
|
212
|
-
}
|
|
213
308
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
309
|
+
const gererChangement = (val: unknown) => {
|
|
310
|
+
const code = extraireCode(val)
|
|
311
|
+
codeBudgetaire.value = formaterCodeBudgetaire(code)
|
|
217
312
|
|
|
218
|
-
|
|
219
|
-
if (!valeur) return ''
|
|
313
|
+
const valeurFormatee = codeBudgetaire.value.trim().toUpperCase()
|
|
220
314
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// Enlever les slash non autorisés
|
|
244
|
-
ext = ext.map((c, i) => {
|
|
245
|
-
if (c === '/' && i !== 0 && i !== 3) return ''
|
|
246
|
-
if (c !== '/' && !/^[A-Z0-9]$/i.test(c)) return ''
|
|
247
|
-
return c
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
// Forcer le slash à la 4e position
|
|
251
|
-
if (ext.length > 3) {
|
|
252
|
-
ext[3] = '/'
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Réduire à 7 caractères
|
|
256
|
-
ext = ext.slice(0, 7)
|
|
257
|
-
|
|
258
|
-
return (base + ext.join('')).slice(0, 22)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const gererPaste = (e: ClipboardEvent) => {
|
|
262
|
-
e.preventDefault()
|
|
263
|
-
const clipboardData = e.clipboardData
|
|
264
|
-
if (!clipboardData) return
|
|
265
|
-
let pasted = clipboardData.getData('text') || ''
|
|
266
|
-
codeBudgetaire.value = formaterCodeBudgetaire(pasted)
|
|
267
|
-
|
|
268
|
-
setTimeout(() => {
|
|
269
|
-
const input = e.target as HTMLInputElement
|
|
270
|
-
input.selectionStart = input.selectionEnd = codeBudgetaire.value.length
|
|
271
|
-
}, 0)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const sauvegarder = () => {
|
|
275
|
-
codeBudgetaire.value = formaterCodeBudgetaire(codeBudgetaire.value)
|
|
276
|
-
|
|
277
|
-
if (!estValideComplet.value) return
|
|
278
|
-
if (codeBudgetaire.value === derniereValeurSauvegardee.value) return
|
|
279
|
-
|
|
280
|
-
const codeNormalise = codeBudgetaire.value.trim().toUpperCase()
|
|
281
|
-
|
|
282
|
-
const existe = props.codeBudgetairesProp.some(item => {
|
|
283
|
-
const code = typeof item === 'string' ? item : item[0]
|
|
284
|
-
return code.trim().toUpperCase() === codeNormalise
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
if (!existe) {
|
|
288
|
-
const nouvelleListe = [...props.codeBudgetairesProp, codeBudgetaire.value]
|
|
289
|
-
emit('update:codeBudgetairesProp', nouvelleListe)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
derniereValeurSauvegardee.value = codeBudgetaire.value
|
|
293
|
-
emit('update:modelValue', codeBudgetaire.value)
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const extraireCode = (val: unknown): string => {
|
|
297
|
-
if (val == null) return ''
|
|
298
|
-
if (typeof val === 'string') return val
|
|
299
|
-
|
|
300
|
-
// Cas où Vuetify enverrait { code, label }
|
|
301
|
-
if (typeof val === 'object' && 'code' in (val as Record<string, unknown>)) {
|
|
302
|
-
const obj = val as { code?: unknown }
|
|
303
|
-
return typeof obj.code === 'string' ? obj.code : String(obj.code ?? '')
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return String(val)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const gererChangement = (val: unknown) => {
|
|
310
|
-
const code = extraireCode(val)
|
|
311
|
-
codeBudgetaire.value = formaterCodeBudgetaire(code)
|
|
312
|
-
|
|
313
|
-
const valeurFormatee = codeBudgetaire.value.trim().toUpperCase()
|
|
314
|
-
|
|
315
|
-
const estDansListe = props.codeBudgetairesProp.some(item => {
|
|
316
|
-
const codeItem = typeof item === 'string' ? item : item[0]
|
|
317
|
-
return codeItem.trim().toUpperCase() === valeurFormatee
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
if (
|
|
321
|
-
estDansListe &&
|
|
322
|
-
valeurFormatee !== (derniereValeurSauvegardee.value ?? '').toUpperCase() &&
|
|
323
|
-
estValideComplet.value
|
|
324
|
-
) {
|
|
325
|
-
sauvegarder()
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
watch(estValideComplet, value => {
|
|
330
|
-
emit('update:valide', value)
|
|
331
|
-
})
|
|
315
|
+
const estDansListe = props.codeBudgetairesProp.some(item => {
|
|
316
|
+
const codeItem = typeof item === 'string' ? item : item[0]
|
|
317
|
+
return codeItem.trim().toUpperCase() === valeurFormatee
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
if (
|
|
321
|
+
estDansListe &&
|
|
322
|
+
valeurFormatee !== (derniereValeurSauvegardee.value ?? '').toUpperCase() &&
|
|
323
|
+
estValideComplet.value
|
|
324
|
+
) {
|
|
325
|
+
sauvegarder()
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
watch(
|
|
330
|
+
() => codeBudgetaire.value,
|
|
331
|
+
() => {
|
|
332
|
+
emit('update:valide', estValideComplet.value)
|
|
333
|
+
},
|
|
334
|
+
{ immediate: true },
|
|
335
|
+
)
|
|
332
336
|
</script>
|
|
@@ -1,203 +1,205 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<v-toolbar
|
|
3
|
-
color="primary"
|
|
4
|
-
height="72px"
|
|
5
|
-
elevation="1"
|
|
6
|
-
>
|
|
7
|
-
<v-row
|
|
8
|
-
class="pl-6 pr-6"
|
|
9
|
-
align="center"
|
|
10
|
-
no-gutters
|
|
11
|
-
>
|
|
12
|
-
<!-- Première colonne : Logo -->
|
|
13
|
-
<v-col cols="auto">
|
|
14
|
-
<a
|
|
15
|
-
:href="href"
|
|
16
|
-
:target="cssUrlValide ? '_blank' : undefined"
|
|
17
|
-
:rel="cssUrlValide ? 'noopener noreferrer' : undefined"
|
|
18
|
-
>
|
|
19
|
-
<!-- Placeholder (même taille) pendant la décision -->
|
|
20
|
-
<div
|
|
21
|
-
v-if="!ready"
|
|
22
|
-
class="logo-placeholder"
|
|
23
|
-
></div>
|
|
24
|
-
<!-- On ne rend l'image qu'une fois la source choisie -->
|
|
25
|
-
<img
|
|
26
|
-
v-else
|
|
27
|
-
class="logo-img"
|
|
28
|
-
id="pivImage"
|
|
29
|
-
:src="currentSrc!"
|
|
30
|
-
:alt="$t('csqc.pivFooter.logoAlt')"
|
|
31
|
-
decoding="async"
|
|
32
|
-
loading="eager"
|
|
33
|
-
@error="ErreurLogo"
|
|
34
|
-
/>
|
|
35
|
-
</a>
|
|
36
|
-
</v-col>
|
|
37
|
-
|
|
38
|
-
<!-- Colonne pour le nom de l'application (Pour le mode desktop) -->
|
|
39
|
-
<v-col
|
|
40
|
-
v-if="!estMobile"
|
|
41
|
-
class="d-flex justify-center"
|
|
42
|
-
>
|
|
43
|
-
<v-app-bar-title
|
|
44
|
-
class="pl-12 ml-12"
|
|
45
|
-
style="font-size: 16px !important"
|
|
46
|
-
>
|
|
47
|
-
{{ formulaireNom }}
|
|
48
|
-
</v-app-bar-title>
|
|
49
|
-
</v-col>
|
|
50
|
-
|
|
51
|
-
<!-- Colonne pour le bouton de langue et icône d'aide -->
|
|
52
|
-
<v-col class="d-none d-flex justify-end">
|
|
53
|
-
<!-- langue -->
|
|
54
|
-
<v-btn
|
|
55
|
-
variant="text"
|
|
56
|
-
@click="enregistrerLangue()"
|
|
57
|
-
>{{ $t('csqc.pivEntete.langue') }}
|
|
58
|
-
</v-btn>
|
|
59
|
-
|
|
60
|
-
<!-- icône d'aide si dispo -->
|
|
61
|
-
<v-btn
|
|
62
|
-
v-if="aideUrl"
|
|
63
|
-
icon="mdi-help-circle-outline"
|
|
64
|
-
:href="aideUrl"
|
|
65
|
-
target="_blank"
|
|
66
|
-
rel="noopener noreferrer"
|
|
67
|
-
style="margin-top: -6px"
|
|
68
|
-
>
|
|
69
|
-
</v-btn>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
</
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
import {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
1
|
+
<template>
|
|
2
|
+
<v-toolbar
|
|
3
|
+
color="primary"
|
|
4
|
+
height="72px"
|
|
5
|
+
elevation="1"
|
|
6
|
+
>
|
|
7
|
+
<v-row
|
|
8
|
+
class="pl-6 pr-6"
|
|
9
|
+
align="center"
|
|
10
|
+
no-gutters
|
|
11
|
+
>
|
|
12
|
+
<!-- Première colonne : Logo -->
|
|
13
|
+
<v-col cols="auto">
|
|
14
|
+
<a
|
|
15
|
+
:href="href"
|
|
16
|
+
:target="cssUrlValide ? '_blank' : undefined"
|
|
17
|
+
:rel="cssUrlValide ? 'noopener noreferrer' : undefined"
|
|
18
|
+
>
|
|
19
|
+
<!-- Placeholder (même taille) pendant la décision -->
|
|
20
|
+
<div
|
|
21
|
+
v-if="!ready"
|
|
22
|
+
class="logo-placeholder"
|
|
23
|
+
></div>
|
|
24
|
+
<!-- On ne rend l'image qu'une fois la source choisie -->
|
|
25
|
+
<img
|
|
26
|
+
v-else
|
|
27
|
+
class="logo-img"
|
|
28
|
+
id="pivImage"
|
|
29
|
+
:src="currentSrc!"
|
|
30
|
+
:alt="$t('csqc.pivFooter.logoAlt')"
|
|
31
|
+
decoding="async"
|
|
32
|
+
loading="eager"
|
|
33
|
+
@error="ErreurLogo"
|
|
34
|
+
/>
|
|
35
|
+
</a>
|
|
36
|
+
</v-col>
|
|
37
|
+
|
|
38
|
+
<!-- Colonne pour le nom de l'application (Pour le mode desktop) -->
|
|
39
|
+
<v-col
|
|
40
|
+
v-if="!estMobile"
|
|
41
|
+
class="d-flex justify-center"
|
|
42
|
+
>
|
|
43
|
+
<v-app-bar-title
|
|
44
|
+
class="pl-12 ml-12"
|
|
45
|
+
style="font-size: 16px !important"
|
|
46
|
+
>
|
|
47
|
+
{{ formulaireNom }}
|
|
48
|
+
</v-app-bar-title>
|
|
49
|
+
</v-col>
|
|
50
|
+
|
|
51
|
+
<!-- Colonne pour le bouton de langue et icône d'aide -->
|
|
52
|
+
<v-col class="d-none d-flex justify-end">
|
|
53
|
+
<!-- langue -->
|
|
54
|
+
<v-btn
|
|
55
|
+
variant="text"
|
|
56
|
+
@click="enregistrerLangue()"
|
|
57
|
+
>{{ $t('csqc.pivEntete.langue') }}
|
|
58
|
+
</v-btn>
|
|
59
|
+
|
|
60
|
+
<!-- icône d'aide si dispo -->
|
|
61
|
+
<v-btn
|
|
62
|
+
v-if="aideUrl"
|
|
63
|
+
icon="mdi-help-circle-outline"
|
|
64
|
+
:href="aideUrl"
|
|
65
|
+
target="_blank"
|
|
66
|
+
rel="noopener noreferrer"
|
|
67
|
+
style="margin-top: -6px"
|
|
68
|
+
>
|
|
69
|
+
</v-btn>
|
|
70
|
+
|
|
71
|
+
<slot name="droite"></slot>
|
|
72
|
+
</v-col>
|
|
73
|
+
|
|
74
|
+
<!-- Colonne pour le nom de l'application (Pour le mode mobile) -->
|
|
75
|
+
<v-col
|
|
76
|
+
v-if="props.estMobile"
|
|
77
|
+
cols="12"
|
|
78
|
+
>
|
|
79
|
+
<v-app-bar-title style="font-size: 16px !important">
|
|
80
|
+
{{ formulaireNom }}
|
|
81
|
+
</v-app-bar-title>
|
|
82
|
+
</v-col>
|
|
83
|
+
</v-row>
|
|
84
|
+
</v-toolbar>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<script setup lang="ts">
|
|
88
|
+
import { ref, watch, computed } from 'vue'
|
|
89
|
+
import { useLocale } from 'vuetify'
|
|
90
|
+
import { useI18n } from 'vue-i18n'
|
|
91
|
+
|
|
92
|
+
const { current } = useLocale()
|
|
93
|
+
const props = defineProps({
|
|
94
|
+
estMobile: { type: Boolean, default: false },
|
|
95
|
+
urlBase: { type: String, required: true },
|
|
96
|
+
aideUrl: { type: String, default: '' },
|
|
97
|
+
cssUrl: { type: String, default: '' },
|
|
98
|
+
logoUrl: { type: String, default: '' },
|
|
99
|
+
lienLogo: { type: String, default: '' },
|
|
100
|
+
formulaireId: { type: Number, default: 0 },
|
|
101
|
+
formulaireNom: { type: String, default: '' },
|
|
102
|
+
})
|
|
103
|
+
const emit = defineEmits(['changementLangue'])
|
|
104
|
+
const { t } = useI18n()
|
|
105
|
+
|
|
106
|
+
const FALLBACK = '/images/QUEBEC_blanc.svg'
|
|
107
|
+
const currentSrc = ref<string | null>(null) // pas d'image tant que null
|
|
108
|
+
const ready = ref(false)
|
|
109
|
+
|
|
110
|
+
const formulaireNom = computed(() => {
|
|
111
|
+
return props.formulaireNom || t('nom_application')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const href = computed(() => (props.cssUrl?.trim() ? props.cssUrl.trim() : '/'))
|
|
115
|
+
const cssUrlValide = computed(() => /^https?:\/\//i.test(href.value))
|
|
116
|
+
|
|
117
|
+
let loadToken = 0 // ← identifiant de “requête” pour annuler logiquement
|
|
118
|
+
|
|
119
|
+
// Sur changement de l’URL du logo, on tente de le charger, sinon fallback apres 3.5 secs
|
|
120
|
+
watch(
|
|
121
|
+
() => props.logoUrl,
|
|
122
|
+
async (nouvelle, ancienne) => {
|
|
123
|
+
const url = (nouvelle ?? '').trim()
|
|
124
|
+
|
|
125
|
+
// même URL → ne rien faire
|
|
126
|
+
if (url === (ancienne ?? '').trim() && currentSrc.value !== null) return
|
|
127
|
+
|
|
128
|
+
// pas d’URL → fallback immédiat
|
|
129
|
+
if (!url) {
|
|
130
|
+
currentSrc.value = FALLBACK
|
|
131
|
+
ready.value = true
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// nouvelle tentative (avec “annulation logique”)
|
|
136
|
+
const token = ++loadToken
|
|
137
|
+
ready.value = false
|
|
138
|
+
const ok = await loadWithTimeout(url, 3500)
|
|
139
|
+
if (token !== loadToken) return // une nouvelle tentative a démarré entre-temps
|
|
140
|
+
|
|
141
|
+
currentSrc.value = ok ? url : FALLBACK
|
|
142
|
+
ready.value = true
|
|
143
|
+
},
|
|
144
|
+
{ immediate: true },
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
// Charge une image avec un timeout
|
|
148
|
+
function loadWithTimeout(url: string, timeoutMs: number): Promise<boolean> {
|
|
149
|
+
return new Promise<boolean>(resolve => {
|
|
150
|
+
const img = new Image()
|
|
151
|
+
const timer = setTimeout(() => {
|
|
152
|
+
// trop long → on abandonne
|
|
153
|
+
img.src = '' // stoppe le chargement
|
|
154
|
+
resolve(false)
|
|
155
|
+
}, timeoutMs)
|
|
156
|
+
|
|
157
|
+
img.onload = () => {
|
|
158
|
+
clearTimeout(timer)
|
|
159
|
+
resolve(true)
|
|
160
|
+
}
|
|
161
|
+
img.onerror = () => {
|
|
162
|
+
clearTimeout(timer)
|
|
163
|
+
resolve(false)
|
|
164
|
+
}
|
|
165
|
+
img.src = url
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Si l'image choisie a un problème, switch sur le fallback
|
|
170
|
+
function ErreurLogo() {
|
|
171
|
+
if (currentSrc.value !== FALLBACK) currentSrc.value = FALLBACK
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const enregistrerLangue = (): void => {
|
|
175
|
+
const langueDispo: string = current.value === 'fr' ? 'en' : 'fr'
|
|
176
|
+
let returnUrl = window.location.pathname + window.location.search
|
|
177
|
+
if (import.meta.env.MODE === 'development') {
|
|
178
|
+
returnUrl = '/'
|
|
179
|
+
}
|
|
180
|
+
window.location.href =
|
|
181
|
+
props.urlBase + `/Traducteur/SetLanguage?culture=${langueDispo}&returnUrl=${encodeURIComponent(returnUrl)}`
|
|
182
|
+
emit('changementLangue')
|
|
183
|
+
}
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<style scoped>
|
|
187
|
+
.container {
|
|
188
|
+
max-width: none !important;
|
|
189
|
+
}
|
|
190
|
+
.theme--light.v-app-bar.v-toolbar.v-sheet {
|
|
191
|
+
background: #095797;
|
|
192
|
+
|
|
193
|
+
color: #fff;
|
|
194
|
+
}
|
|
195
|
+
.logo-placeholder {
|
|
196
|
+
height: 72px;
|
|
197
|
+
width: 180px;
|
|
198
|
+
}
|
|
199
|
+
.logo-img {
|
|
200
|
+
height: 72px;
|
|
201
|
+
transition: opacity 0.15s;
|
|
202
|
+
opacity: 1;
|
|
203
|
+
display: block;
|
|
204
|
+
}
|
|
205
|
+
</style>
|