codevdesign 0.0.58 → 0.0.60

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.
@@ -0,0 +1,259 @@
1
+ <template>
2
+ <div>
3
+ <v-form
4
+ ref="form"
5
+ v-model="formValide"
6
+ @submit.prevent
7
+ >
8
+ <v-combobox
9
+ v-model="codeBudgetaire"
10
+ :items="codeBudgetairesProp"
11
+ :label="label"
12
+ persistent-hint
13
+ variant="outlined"
14
+ hide-details="auto"
15
+ :error="!estValide"
16
+ :rules="[v => (estValide ? true : regleMessageErreur)]"
17
+ :disabled="disable"
18
+ :density="density"
19
+ :hint="placeholder"
20
+ :max-width="maxWidth || '100%'"
21
+ :min-width="minWidth || '100%'"
22
+ @blur="sauvegarder"
23
+ @keydown.enter="sauvegarder"
24
+ @keydown="caractereAutorises"
25
+ @update:modelValue="gererChangement"
26
+ @paste="gererPaste"
27
+ />
28
+ </v-form>
29
+ </div>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ import { ref, computed, onMounted, nextTick } from 'vue'
34
+ import { VForm } from 'vuetify/components'
35
+
36
+ const emit = defineEmits(['update:modelValue', 'update:codeBudgetairesProp'])
37
+
38
+ const props = withDefaults(
39
+ defineProps<{
40
+ codeBudgetairesProp: string[]
41
+ modelValue: string | null
42
+ label: string
43
+ disable: boolean
44
+ regleMessageErreur: string
45
+ density?: 'default' | 'comfortable' | 'compact'
46
+ maxWidth?: string | number
47
+ minWidth?: string | number
48
+ format?: string
49
+ activerExtension?: boolean
50
+ }>(),
51
+ {
52
+ format: '999-9-99999-999',
53
+ activerExtension: false,
54
+ },
55
+ )
56
+
57
+ const formValide = ref(false)
58
+ const form = ref<VForm | null>(null)
59
+ const codeBudgetaire = ref(props.modelValue ?? '')
60
+ const derniereValeurSauvegardee = ref<string | null>(null)
61
+ const format = props.format
62
+ const activerExtension = props.activerExtension
63
+
64
+ const estValide = computed(() => {
65
+ const val = codeBudgetaire.value?.toUpperCase().trim() || ''
66
+ const base = val.slice(0, 15)
67
+ const extension = val.slice(15)
68
+
69
+ if (!/^\d{3}-\d{1}-\d{5}-\d{3}$/.test(base)) return false
70
+
71
+ if (!activerExtension) return val.length === 15
72
+
73
+ if (val.length === 15) return true
74
+ if (val.length !== 22) return false
75
+ if (extension.length !== 7) return false
76
+
77
+ if (extension[3] !== '/') return false
78
+ if (!/^[A-Z0-9/]$/i.test(extension[0])) return false
79
+
80
+ const alphanumAt = [1, 2, 4, 5, 6]
81
+ return alphanumAt.every(i => /^[A-Z0-9]$/i.test(extension[i]))
82
+ })
83
+
84
+ const caractereAutorises = (e: KeyboardEvent) => {
85
+ if (e.ctrlKey || e.metaKey) return
86
+
87
+ const touchesSpecifiques = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Home', 'End']
88
+ if (touchesSpecifiques.includes(e.key)) return
89
+
90
+ const input = e.target as HTMLInputElement
91
+ let position = input.selectionStart ?? 0
92
+
93
+ // Gestion de la partie de base (15 premiers caractères)
94
+ if (position < 15) {
95
+ if (!/^\d$/.test(e.key)) {
96
+ e.preventDefault()
97
+ return
98
+ }
99
+
100
+ // Insérer chiffre et auto-ajout des tirets
101
+ e.preventDefault()
102
+
103
+ const value = codeBudgetaire.value.replace(/-/g, '')
104
+ const clean = value.slice(0, 12) + e.key
105
+
106
+ let formatted = ''
107
+ if (clean.length > 0) formatted += clean.slice(0, 3)
108
+ if (clean.length > 3) formatted += '-' + clean.slice(3, 4)
109
+ if (clean.length > 4) formatted += '-' + clean.slice(4, 9)
110
+ if (clean.length > 9) formatted += '-' + clean.slice(9, 12)
111
+
112
+ codeBudgetaire.value = formatted.slice(0, 15)
113
+
114
+ nextTick(() => {
115
+ const newPos = codeBudgetaire.value.length
116
+ input.selectionStart = input.selectionEnd = newPos
117
+ })
118
+
119
+ return
120
+ }
121
+
122
+ // --- Gestion de l'extension ---
123
+ if (!activerExtension || position >= 22 || codeBudgetaire.value.length >= 22) {
124
+ e.preventDefault()
125
+ return
126
+ }
127
+
128
+ const extensionPos = position - 15
129
+
130
+ // Règle 1 : extension[0] = alphanum ou /
131
+ if (extensionPos === 0) {
132
+ if (!/^[A-Z0-9/]$/i.test(e.key)) {
133
+ e.preventDefault()
134
+ return
135
+ }
136
+ return
137
+ }
138
+
139
+ // Règle 2 : extension[1,2,4,5,6] = alphanum
140
+ if ([1, 2, 4, 5, 6].includes(extensionPos)) {
141
+ if (!/^[A-Z0-9]$/i.test(e.key)) {
142
+ e.preventDefault()
143
+ return
144
+ }
145
+ return
146
+ }
147
+
148
+ // Règle 3 : slash automatique à position 3 (index 18)
149
+ if (extensionPos === 3) {
150
+ e.preventDefault()
151
+ const before = codeBudgetaire.value.slice(0, position)
152
+ const after = codeBudgetaire.value.slice(position)
153
+ codeBudgetaire.value = before + '/' + after
154
+ nextTick(() => {
155
+ input.selectionStart = input.selectionEnd = position + 1
156
+ })
157
+ return
158
+ }
159
+
160
+ // Tout autre cas = bloqué
161
+ e.preventDefault()
162
+ }
163
+
164
+ const formaterCodeBudgetaire = (valeur: string): string => {
165
+ if (!valeur) return ''
166
+
167
+ const upper = valeur.toUpperCase().replace(/[^A-Z0-9/]/g, '')
168
+ const chiffres = upper.replace(/[^0-9]/g, '').slice(0, 12)
169
+
170
+ let base = ''
171
+ if (chiffres.length > 0) base += chiffres.slice(0, 3)
172
+ if (chiffres.length > 3) base += '-' + chiffres.slice(3, 4)
173
+ if (chiffres.length > 4) base += '-' + chiffres.slice(4, 9)
174
+ if (chiffres.length > 9) base += '-' + chiffres.slice(9, 12)
175
+
176
+ if (!activerExtension || base.length < 15) return base
177
+
178
+ const reste = upper.slice(chiffres.length).replace(/[^A-Z0-9/]/gi, '')
179
+
180
+ // Extraire les 7 premiers caractères restants pour l’extension
181
+ let ext = reste.slice(0, 7).split('')
182
+
183
+ // Ne garder que le premier slash s’il est à l’index 0 ou 3
184
+ const slashIndices = ext.map((c, i) => (c === '/' ? i : -1)).filter(i => i !== -1)
185
+ ext = ext.filter((c, i) => {
186
+ if (c !== '/') return true
187
+ return i === 0 || i === 3
188
+ })
189
+
190
+ // Enlever les slash non autorisés
191
+ ext = ext.map((c, i) => {
192
+ if (c === '/' && i !== 0 && i !== 3) return ''
193
+ if (c !== '/' && !/^[A-Z0-9]$/i.test(c)) return ''
194
+ return c
195
+ })
196
+
197
+ // Forcer le slash à la 4e position
198
+ if (ext.length > 3) {
199
+ ext[3] = '/'
200
+ }
201
+
202
+ // Réduire à 7 caractères
203
+ ext = ext.slice(0, 7)
204
+
205
+ return (base + ext.join('')).slice(0, 22)
206
+ }
207
+
208
+ const gererPaste = (e: ClipboardEvent) => {
209
+ e.preventDefault()
210
+ const clipboardData = e.clipboardData
211
+ if (!clipboardData) return
212
+ let pasted = clipboardData.getData('text') || ''
213
+ codeBudgetaire.value = formaterCodeBudgetaire(pasted)
214
+
215
+ setTimeout(() => {
216
+ const input = e.target as HTMLInputElement
217
+ input.selectionStart = input.selectionEnd = codeBudgetaire.value.length
218
+ }, 0)
219
+ }
220
+
221
+ const sauvegarder = () => {
222
+ codeBudgetaire.value = formaterCodeBudgetaire(codeBudgetaire.value)
223
+ if (!estValide.value) return
224
+ if (codeBudgetaire.value === derniereValeurSauvegardee.value) return
225
+
226
+ const existe = props.codeBudgetairesProp.some(
227
+ item => item.trim().toUpperCase() === codeBudgetaire.value.trim().toUpperCase(),
228
+ )
229
+
230
+ if (!existe) {
231
+ const nouvelleListe = [...props.codeBudgetairesProp, codeBudgetaire.value]
232
+ emit('update:codeBudgetairesProp', nouvelleListe)
233
+ }
234
+
235
+ derniereValeurSauvegardee.value = codeBudgetaire.value
236
+ emit('update:modelValue', codeBudgetaire.value)
237
+ }
238
+
239
+ const gererChangement = (val: string) => {
240
+ codeBudgetaire.value = formaterCodeBudgetaire(val)
241
+
242
+ const valeurFormatee = codeBudgetaire.value
243
+ const estDansListe = props.codeBudgetairesProp.includes(valeurFormatee)
244
+
245
+ if (estDansListe && valeurFormatee !== derniereValeurSauvegardee.value && estValide.value) {
246
+ sauvegarder()
247
+ }
248
+ }
249
+
250
+ onMounted(() => {
251
+ derniereValeurSauvegardee.value = codeBudgetaire.value
252
+ })
253
+
254
+ const placeholder = computed(() => {
255
+ const base = format.replace(/9/g, '0')
256
+ const extension = activerExtension ? '-XXX/XXX' : ''
257
+ return base + extension
258
+ })
259
+ </script>
package/index.ts CHANGED
@@ -9,6 +9,7 @@ import pivFooter from './composants/gabarit/pivPiedPage.vue'
9
9
  import csqcMenu from './composants/gabarit/csqcMenu.vue'
10
10
  import csqcConfirmation from './composants/csqcConfirmation.vue'
11
11
  import csqcTable from './composants/csqcTable/csqcTable.vue'
12
+ import csqcCodeBudgetaire from './composants/codeBudgetaireGenerique.vue'
12
13
  //import csqcChaise from './composants/csqcChaise/chaiseConteneur.vue'
13
14
  import csqcAide from './composants/csqcAide.vue'
14
15
  import csqcEntete from './composants/csqcEntete.vue'
@@ -45,6 +46,7 @@ export {
45
46
  csqcTable,
46
47
  csqcTiroir,
47
48
  csqcMenu,
49
+ csqcCodeBudgetaire,
48
50
  //csqcChaise,
49
51
  pivFooter,
50
52
  pivEntete,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevdesign",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",