@cnamts/synapse 0.0.11-alpha → 0.0.12-alpha

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.
Files changed (108) hide show
  1. package/dist/design-system-v3.js +3878 -3189
  2. package/dist/design-system-v3.umd.cjs +1 -1
  3. package/dist/src/components/Amelipro/types/languages.d.ts +6 -0
  4. package/dist/src/components/Amelipro/types/types.d.ts +65 -0
  5. package/dist/src/components/CookieBanner/CookieBanner.d.ts +1 -1
  6. package/dist/src/components/Customs/SyInputSelect/SyInputSelect.d.ts +2 -0
  7. package/dist/src/components/Customs/SyTextField/SyTextField.d.ts +29 -23
  8. package/dist/src/components/Customs/SyTextField/types.d.ts +1 -0
  9. package/dist/src/components/DatePicker/DatePicker.d.ts +70 -59
  10. package/dist/src/components/DatePicker/DateTextInput.d.ts +67 -56
  11. package/dist/src/components/ErrorPage/ErrorPage.d.ts +1 -1
  12. package/dist/src/components/FileList/FileList.d.ts +1 -0
  13. package/dist/src/components/FileList/UploadItem/UploadItem.d.ts +1 -1
  14. package/dist/src/components/FilterSideBar/FilterSideBar.d.ts +31 -0
  15. package/dist/src/components/FilterSideBar/locales.d.ts +7 -0
  16. package/dist/src/components/FilterSideBar/tests/FilterSideBar.spec.d.ts +1 -0
  17. package/dist/src/components/LangBtn/LangBtn.d.ts +2 -2
  18. package/dist/src/components/NirField/NirField.d.ts +940 -0
  19. package/dist/src/components/NotificationBar/NotificationBar.d.ts +1 -1
  20. package/dist/src/components/PasswordField/PasswordField.d.ts +40 -8
  21. package/dist/src/components/PeriodField/PeriodField.d.ts +142 -120
  22. package/dist/src/components/PhoneField/PhoneField.d.ts +11 -2
  23. package/dist/src/components/RatingPicker/EmotionPicker/EmotionPicker.d.ts +1 -1
  24. package/dist/src/components/RatingPicker/NumberPicker/NumberPicker.d.ts +1 -1
  25. package/dist/src/components/RatingPicker/StarsPicker/StarsPicker.d.ts +1 -1
  26. package/dist/src/components/UploadWorkflow/config.d.ts +29 -0
  27. package/dist/src/components/UploadWorkflow/locales.d.ts +7 -0
  28. package/dist/src/components/UploadWorkflow/tests/UploadWorkflow.spec.d.ts +1 -0
  29. package/dist/src/components/UploadWorkflow/types.d.ts +19 -0
  30. package/dist/src/components/UploadWorkflow/useFileList.d.ts +10 -0
  31. package/dist/src/components/UploadWorkflow/useFileUploadJourney.d.ts +9 -0
  32. package/dist/src/components/index.d.ts +2 -0
  33. package/dist/src/composables/rules/useFieldValidation.d.ts +1 -0
  34. package/dist/src/composables/validation/tests/useValidation.spec.d.ts +1 -0
  35. package/dist/src/composables/validation/useValidation.d.ts +39 -0
  36. package/dist/src/designTokens/index.d.ts +3 -1
  37. package/dist/src/vuetifyConfig.d.ts +81 -0
  38. package/dist/style.css +1 -1
  39. package/package.json +1 -1
  40. package/src/assets/_elevations.scss +89 -0
  41. package/src/assets/_fonts.scss +6 -0
  42. package/src/assets/_radius.scss +86 -0
  43. package/src/assets/_spacers.scss +149 -0
  44. package/src/assets/settings.scss +7 -3
  45. package/src/assets/tokens.scss +32 -29
  46. package/src/components/Amelipro/types/languages.d.ts +6 -0
  47. package/src/components/Amelipro/types/types.d.ts +65 -0
  48. package/src/components/Customs/SyInputSelect/SyInputSelect.stories.ts +65 -0
  49. package/src/components/Customs/SyInputSelect/SyInputSelect.vue +13 -3
  50. package/src/components/Customs/SySelect/SySelect.stories.ts +88 -5
  51. package/src/components/Customs/SySelect/SySelect.vue +36 -10
  52. package/src/components/Customs/SySelect/tests/SySelect.spec.ts +135 -2
  53. package/src/components/Customs/SyTextField/SyTextField.stories.ts +576 -85
  54. package/src/components/Customs/SyTextField/SyTextField.vue +132 -104
  55. package/src/components/Customs/SyTextField/tests/SyTextField.spec.ts +190 -38
  56. package/src/components/Customs/SyTextField/types.d.ts +1 -0
  57. package/src/components/DatePicker/DatePicker.vue +405 -137
  58. package/src/components/DatePicker/DateTextInput.vue +15 -0
  59. package/src/components/DatePicker/tests/DatePicker.spec.ts +8 -15
  60. package/src/components/FileList/FileList.vue +2 -1
  61. package/src/components/FileList/UploadItem/UploadItem.vue +10 -0
  62. package/src/components/FileUpload/FileUpload.stories.ts +84 -0
  63. package/src/components/FileUpload/FileUpload.vue +1 -0
  64. package/src/components/FileUpload/tests/FileUpload.spec.ts +4 -4
  65. package/src/components/FilterInline/FilterInline.mdx +180 -34
  66. package/src/components/FilterInline/FilterInline.stories.ts +363 -6
  67. package/src/components/FilterSideBar/FilterSideBar.mdx +237 -0
  68. package/src/components/FilterSideBar/FilterSideBar.stories.ts +798 -0
  69. package/src/components/FilterSideBar/FilterSideBar.vue +193 -0
  70. package/src/components/FilterSideBar/locales.ts +8 -0
  71. package/src/components/FilterSideBar/tests/FilterSideBar.spec.ts +305 -0
  72. package/src/components/FilterSideBar/tests/__snapshots__/FilterSideBar.spec.ts.snap +39 -0
  73. package/src/components/HeaderBar/Usages.mdx +1 -1
  74. package/src/components/NirField/NirField.stories.ts +573 -29
  75. package/src/components/NirField/NirField.vue +397 -359
  76. package/src/components/NirField/tests/NirField.spec.ts +88 -52
  77. package/src/components/NirField/tests//342/200/257dataset/342/200/257.md +12 -0
  78. package/src/components/NotificationBar/Accessibilite.stories.ts +4 -0
  79. package/src/components/NotificationBar/NotificationBar.stories.ts +18 -13
  80. package/src/components/PasswordField/PasswordField.mdx +129 -47
  81. package/src/components/PasswordField/PasswordField.stories.ts +924 -120
  82. package/src/components/PasswordField/PasswordField.vue +209 -99
  83. package/src/components/PasswordField/tests/PasswordField.spec.ts +138 -9
  84. package/src/components/PeriodField/PeriodField.vue +55 -54
  85. package/src/components/PhoneField/PhoneField.stories.ts +69 -0
  86. package/src/components/PhoneField/PhoneField.vue +3 -0
  87. package/src/components/PhoneField/indicatifs.ts +1 -1
  88. package/src/components/UploadWorkflow/UploadWorkflow.mdx +75 -0
  89. package/src/components/UploadWorkflow/UploadWorkflow.stories.ts +943 -0
  90. package/src/components/UploadWorkflow/UploadWorkflow.vue +230 -0
  91. package/src/components/UploadWorkflow/config.ts +29 -0
  92. package/src/components/UploadWorkflow/locales.ts +8 -0
  93. package/src/components/UploadWorkflow/tests/UploadWorkflow.spec.ts +257 -0
  94. package/src/components/UploadWorkflow/tests/__snapshots__/UploadWorkflow.spec.ts.snap +54 -0
  95. package/src/components/UploadWorkflow/types.ts +21 -0
  96. package/src/components/UploadWorkflow/useFileList.ts +84 -0
  97. package/src/components/UploadWorkflow/useFileUploadJourney.ts +18 -0
  98. package/src/components/index.ts +2 -0
  99. package/src/composables/rules/useFieldValidation.ts +5 -2
  100. package/src/composables/validation/tests/useValidation.spec.ts +154 -0
  101. package/src/composables/validation/useValidation.ts +165 -0
  102. package/src/designTokens/index.ts +4 -0
  103. package/src/stories/Demarrer/Accueil.mdx +1 -1
  104. package/src/stories/DesignTokens/ThemePA.mdx +4 -30
  105. package/src/stories/GuideDuDev/UtiliserLesRules.mdx +319 -76
  106. package/src/stories/GuideDuDev/moduleDeNotification.mdx +1 -1
  107. package/src/vuetifyConfig.ts +61 -0
  108. package/src/composables/useFilterable/__snapshots__/useFilterable.spec.ts.snap +0 -3
@@ -1,457 +1,495 @@
1
1
  <script lang="ts" setup>
2
- import { ref, watch, computed, nextTick } from 'vue'
3
- import { useFieldValidation } from '@/composables/rules/useFieldValidation'
2
+ import { ref, watch, computed, nextTick, toRef } from 'vue'
4
3
  import { vMaska } from 'maska/vue'
5
4
  import { checkNIR, isNIRKeyValid } from './nirValidation'
6
- import useCustomizableOptions, { type CustomizableOptions } from '@/composables/useCustomizableOptions'
5
+ // import useCustomizableOptions, { type CustomizableOptions } from '@/composables/useCustomizableOptions'
6
+ import { type CustomizableOptions } from '@/composables/useCustomizableOptions'
7
7
  import SyTextField from '../Customs/SyTextField/SyTextField.vue'
8
- import { mdiInformationOutline } from '@mdi/js'
9
8
  import { locales } from './locales'
10
- import defaultOptions from './config'
11
-
12
- type Rule = (value: string) => { error?: string, success?: string }
13
-
9
+ // import defaultOptions from './config'
10
+ import { useValidation, type ValidationRule } from '@/composables/validation/useValidation'
14
11
  const props = withDefaults(defineProps<CustomizableOptions & {
15
12
  modelValue?: string | undefined
16
- outlined?: boolean
17
- required?: boolean
18
- nirTooltip?: string
19
- keyTooltip?: string
13
+ label?: string
20
14
  numberLabel?: string
21
15
  keyLabel?: string
22
16
  displayKey?: boolean
17
+ outlined?: boolean
18
+ nirTooltip?: string
19
+ keyTooltip?: string
20
+ nirTooltipPosition?: 'prepend' | 'append'
21
+ keyTooltipPosition?: 'prepend' | 'append'
22
+ required?: boolean
23
+ displayAsterisk?: boolean
24
+ customNumberRules?: ValidationRule[]
25
+ customKeyRules?: ValidationRule[]
26
+ customNumberWarningRules?: ValidationRule[]
27
+ customKeyWarningRules?: ValidationRule[]
23
28
  showSuccessMessages?: boolean
24
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
25
- customNumberRules?: any
26
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
27
- customKeyRules?: any
28
29
  width?: string
30
+ bgColor?: string
31
+ isDisabled?: boolean
32
+ density?: 'default' | 'comfortable' | 'compact'
33
+ hideDetails?: boolean | 'auto'
34
+ hideSpinButtons?: boolean
35
+ placeholder?: string
36
+ readonly?: boolean
37
+ variant?: 'filled' | 'outlined' | 'plain' | 'underlined' | 'solo'
38
+ clearable?: boolean
39
+ counter?: boolean | number | string
40
+ hint?: string
41
+ persistentHint?: boolean
42
+ persistentPlaceholder?: boolean
29
43
  }>(), {
30
44
  modelValue: undefined,
31
- outlined: true,
32
- required: false,
33
- nirTooltip: undefined,
34
- keyTooltip: undefined,
45
+ label: undefined,
35
46
  numberLabel: 'Numéro de sécurité sociale',
36
47
  keyLabel: 'Clé',
37
48
  displayKey: true,
38
- showSuccessMessages: false,
39
- customNumberRules: [],
40
- customKeyRules: [],
49
+ outlined: true,
50
+ nirTooltip: undefined,
51
+ keyTooltip: undefined,
52
+ nirTooltipPosition: 'append',
53
+ keyTooltipPosition: 'append',
54
+ required: false,
55
+ displayAsterisk: false,
56
+ customNumberRules: () => [],
57
+ customKeyRules: () => [],
58
+ customNumberWarningRules: () => [],
59
+ customKeyWarningRules: () => [],
60
+ showSuccessMessages: true,
41
61
  width: '100%',
62
+ bgColor: undefined,
63
+ isDisabled: false,
64
+ density: 'default',
65
+ hideDetails: false,
66
+ hideSpinButtons: false,
67
+ placeholder: undefined,
68
+ readonly: false,
69
+ variant: 'outlined',
70
+ clearable: false,
71
+ counter: false,
72
+ hint: undefined,
73
+ persistentHint: false,
74
+ persistentPlaceholder: false,
42
75
  })
43
76
 
44
77
  const emit = defineEmits(['update:modelValue'])
45
- const options = useCustomizableOptions(defaultOptions, props)
46
- const infoIcon = mdiInformationOutline
78
+ const modelValueRef = toRef(props, 'modelValue')
79
+ // const options = useCustomizableOptions(defaultOptions, props)
47
80
 
48
81
  // Champs
49
82
  const numberValue = ref('')
50
83
  const keyValue = ref('')
51
- const keyDeleted = ref(false)
52
84
 
53
85
  // Refs pour les champs
54
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
55
- const keyField = ref<any | null>(null)
56
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
57
- const numberField = ref<any | null>(null)
86
+ const keyField = ref<InstanceType<typeof SyTextField> | null>(null)
87
+ const numberField = ref<InstanceType<typeof SyTextField> | null>(null)
58
88
 
89
+ // Valeurs non masquées
59
90
  const unmaskedNumberValue = computed(() => numberValue.value.replace(/\s/g, ''))
60
-
61
- watch(() => props.modelValue, async (value) => {
62
- numberValue.value = value?.slice(0, 13) ?? ''
63
- keyValue.value = value?.slice(13, 15) ?? ''
64
- }, { immediate: true })
65
-
66
- // Etats d’erreur/succès
67
- const errors = ref<string[]>([])
68
- const successes = ref<string[]>([])
69
-
70
- // Flags de validation
71
- const isValidating = ref(false)
91
+ const unmaskedKeyValue = computed(() => keyValue.value.replace(/\s/g, ''))
72
92
 
73
93
  // Masques
74
94
  const numberMask = {
75
95
  mask: '# ## ## #C ### ###',
76
96
  preProcess: (value: string) => value.toUpperCase(),
77
97
  tokens: {
78
- C: {
98
+ '#': {
99
+ pattern: /[0-9]/,
100
+ },
101
+ 'C': {
79
102
  pattern: /[0-9AB]/,
80
103
  transform: (char: string) => char.toUpperCase(),
81
104
  },
82
105
  },
83
106
  }
84
- const keyMask = { mask: '##' }
85
-
86
- // Règles de validation par défaut
87
- const { generateRules } = useFieldValidation()
88
-
89
- const defaultNumberRules = [
90
- {
91
- type: 'exactLength',
92
- options: { length: 13, message: locales.errorLengthNumber(13), ignoreSpace: true, fieldIdentifier: 'numéro' },
93
- },
94
- {
95
- type: 'custom',
96
- options: {
97
- validate: checkNIR,
98
- message: 'Le numéro de sécurité sociale est invalide.',
99
- successMessage: 'Le numéro de sécurité sociale est valide.',
100
- fieldIdentifier: 'numéro',
101
- },
102
- },
103
- ...(props.required
104
- ? [{
105
- type: 'required',
106
- options: { message: 'Le numéro de sécurité sociale est requis.', fieldIdentifier: 'numéro' },
107
- }]
108
- : []),
109
- ]
110
-
111
- const defaultKeyRules = [
112
- {
113
- type: 'exactLength',
114
- options: { length: 2, message: locales.errorLengthKey(2), ignoreSpace: true, fieldIdentifier: 'clé' },
115
- },
116
- {
117
- type: 'custom',
118
- options: {
119
- validate: () => isNIRKeyValid(`${numberValue.value}${keyValue.value}`),
120
- message: 'La clé du numéro de sécurité sociale est invalide.',
121
- successMessage: 'Le champ clé est valide.',
122
- fieldIdentifier: 'clé',
107
+ const keyMask = {
108
+ mask: '##',
109
+ tokens: {
110
+ '#': {
111
+ pattern: /[0-9]/,
123
112
  },
124
113
  },
125
- ...(props.required
126
- ? [{
127
- type: 'required',
128
- options: { message: 'La clé est requise.', fieldIdentifier: 'clé' },
129
- }]
130
- : []),
131
- ]
114
+ }
132
115
 
133
- // Computed pour statut des champs
134
- const fieldIdentifierNumber = defaultNumberRules[0]?.options?.fieldIdentifier
135
- const fieldIdentifierKey = defaultKeyRules[0]?.options?.fieldIdentifier
136
-
137
- const hasNumberErrors = computed(() => errors.value.some(error => error.includes(fieldIdentifierNumber)))
138
- const hasKeyErrors = computed(() => errors.value.some(error => error.includes(fieldIdentifierKey)))
139
- const hasNumberSuccess = computed(() => successes.value.some(success => success.includes(fieldIdentifierNumber)))
140
- const hasKeySuccess = computed(() => successes.value.some(success => success.includes(fieldIdentifierKey)))
141
-
142
- // Génération des règles finales
143
- const numberRules = props.customNumberRules?.length
144
- ? generateRules(props.customNumberRules)
145
- : generateRules(defaultNumberRules)
146
-
147
- const keyRules = props.displayKey
148
- ? (props.customKeyRules?.length
149
- ? generateRules(props.customKeyRules)
150
- : generateRules(defaultKeyRules))
151
- : []
152
-
153
- /**
154
- * Valide une liste de règles sur une valeur et met à jour les tableaux d'erreurs et de succès.
155
- * @param value Valeur du champ à valider
156
- * @param rules Ensemble de règles
157
- */
158
- function validateFieldSet(value: string, rules: Rule[]) {
159
- rules.forEach((rule) => {
160
- const { error, success } = rule(value)
161
- if (error) errors.value.push(error)
162
- if (success && success !== 'Le champ est valide.') successes.value.push(success)
116
+ // Fonction pour gérer le focus des champs
117
+ const focusField = (field: typeof numberField | typeof keyField) => {
118
+ nextTick(() => {
119
+ const input = field.value?.$el.querySelector('input')
120
+ if (input) {
121
+ // Focus and select all text
122
+ input.focus()
123
+ // Adding a slight delay to ensure focus is applied
124
+ setTimeout(() => {
125
+ input.click()
126
+ }, 50)
127
+ }
163
128
  })
164
129
  }
165
130
 
166
- /**
167
- * Valide les champs numéro et clé (si activée).
168
- * @param onBlur Si true, la validation est lancée suite à un blur, sinon validation continue
169
- */
170
- function validateFields(onBlur = false) {
171
- errors.value = []
172
- successes.value = []
173
-
174
- const shouldValidateNumber = onBlur || isValidating.value || numberValue.value.length === 18
175
- const shouldValidateKey = props.displayKey && (onBlur || isValidating.value || keyValue.value.length === 2)
176
-
177
- if (shouldValidateNumber) {
178
- validateFieldSet(numberValue.value, numberRules)
131
+ // Watch sur la valeur non masquée du numéro pour gérer le focus automatique
132
+ watch(unmaskedNumberValue, (newValue) => {
133
+ if (newValue.length === 13 && props.displayKey) {
134
+ focusField(keyField)
179
135
  }
180
-
181
- if (shouldValidateKey) {
182
- validateFieldSet(keyValue.value, keyRules)
183
- }
184
-
185
- // Unicité des succès
186
- successes.value = Array.from(new Set(successes.value))
187
- }
188
-
189
- // Compteurs
190
- const numberCounter = computed(() => {
191
- const length = numberValue.value.replace(/\s/g, '').length
192
- return `${Math.min(length, 13)}/13`
193
136
  })
194
137
 
195
- const keyCounter = computed(() => {
196
- const length = keyValue.value.replace(/\s/g, '').length
197
- return `${Math.min(length, 2)}/2`
138
+ watch(unmaskedKeyValue, (newValue) => {
139
+ if (newValue.length === 0) {
140
+ focusField(numberField)
141
+ }
198
142
  })
199
143
 
200
- watch([unmaskedNumberValue, keyValue], () => {
201
- validateFields()
202
- if (unmaskedNumberValue.value + keyValue.value !== props.modelValue) {
203
- emit('update:modelValue', `${unmaskedNumberValue.value}${keyValue.value}`)
144
+ // Watch pour détecter la suppression des chiffres de la clé
145
+ watch(keyValue, (newValue, oldValue) => {
146
+ // Si l'ancienne valeur avait des chiffres et la nouvelle est vide ou ne contient que des espaces
147
+ if (oldValue.trim() && !newValue.trim()) {
148
+ focusField(numberField)
204
149
  }
205
150
  })
206
151
 
207
- watch(keyValue, (newValue, oldValue) => {
208
- keyDeleted.value = !!(!newValue && oldValue)
152
+ // Initialisation des validations
153
+ const numberValidation = useValidation({
154
+ showSuccessMessages: props.showSuccessMessages,
155
+ fieldIdentifier: props.numberLabel,
209
156
  })
210
157
 
211
- watch(numberValue, () => {
212
- if (unmaskedNumberValue.value.length < 13) {
213
- keyDeleted.value = false
214
- }
158
+ const keyValidation = useValidation({
159
+ showSuccessMessages: props.showSuccessMessages,
160
+ fieldIdentifier: props.keyLabel,
215
161
  })
216
162
 
217
- // Déplacement du focus sur la clé quand le numéro est rempli
218
- const focusElement = computed(() => {
219
- if (props.displayKey && numberValue.value.length === 18) {
220
- if (!keyDeleted.value) {
221
- return keyField.value?.$el?.querySelector('input')
222
- }
223
- else {
224
- return numberField.value?.$el?.querySelector('input')
225
- }
163
+ // Règles de validation
164
+ const defaultNumberRules = computed(() => {
165
+ const rules: ValidationRule[] = []
166
+
167
+ if (props.required) {
168
+ rules.push({
169
+ type: 'required',
170
+ options: {
171
+ message: `Le champ ${props.numberLabel} est requis.`,
172
+ fieldIdentifier: props.numberLabel,
173
+ },
174
+ })
226
175
  }
227
- return null
228
- })
229
176
 
230
- watch(focusElement, (newEl) => {
231
- nextTick(() => {
232
- newEl?.focus()
177
+ rules.push({
178
+ type: 'custom',
179
+ options: {
180
+ validate: (value: string) => {
181
+ if (!value) return true
182
+ // Ne valider que si tous les caractères sont saisis
183
+ if (value.length < 13) {
184
+ return 'Le numéro de sécurité sociale est invalide.'
185
+ }
186
+ const result = checkNIR(value)
187
+ return result === true ? true : 'Le numéro de sécurité sociale est invalide.'
188
+ },
189
+ message: 'Le numéro de sécurité sociale est invalide.',
190
+ successMessage: 'Le numéro de sécurité sociale est valide.',
191
+ fieldIdentifier: props.numberLabel,
192
+ },
233
193
  })
234
- })
235
194
 
236
- function validateOnSubmit() {
237
- isValidating.value = true
238
- validateFields(true)
239
- return errors.value.length === 0
240
- }
195
+ // Ajout des règles personnalisées
196
+ if (props.customNumberRules) {
197
+ rules.push(...props.customNumberRules.map(rule => ({
198
+ ...rule,
199
+ options: rule.options || {},
200
+ })))
201
+ }
241
202
 
242
- defineExpose({
243
- validateOnSubmit,
203
+ return rules
244
204
  })
245
- </script>
246
205
 
247
- <template>
248
- <div class="d-flex align-start nir-container">
249
- <v-input
250
- ref="vInput"
251
- :class="{
252
- 'v-messages__message--success': successes.length > 0 && props.showSuccessMessages,
253
- 'v-messages__message--error': errors.length > 0
254
- }"
255
- :error-messages="errors"
256
- :label="numberLabel"
257
- :max-errors="3"
258
- :messages="props.showSuccessMessages ? successes : []"
259
- :model-value="[numberValue, keyValue]"
260
- class="vd-nir-field__fields-wrapper multi-line"
261
- validate-on="blur lazy"
262
- >
263
- <VTooltip v-if="nirTooltip">
264
- <template #activator="{ props: iconProps }">
265
- <VIcon
266
- class="vd-tooltip-icon mt-4 mr-4"
267
- v-bind="{ ...iconProps, ...options.tooltip }"
268
- >
269
- {{ infoIcon }}
270
- </VIcon>
271
- </template>
272
- <slot name="nirTooltip">
273
- {{ nirTooltip }}
274
- </slot>
275
- </VTooltip>
276
- <SyTextField
277
- ref="numberField"
278
- v-model="numberValue"
279
- v-maska="numberMask"
280
- :append-inner-icon="hasNumberErrors ? 'error' : (hasNumberSuccess ? 'success' : undefined)"
281
- :aria-errormessage="hasNumberErrors ? 'number-field-errors' : undefined"
282
- :aria-invalid="hasNumberErrors"
283
- :aria-required="required"
284
- :color="hasNumberErrors ? 'error' : 'primary'"
285
- :error="hasNumberErrors"
286
- :hint="locales.numberHint"
287
- :label="numberLabel"
288
- :variant="outlined ? 'outlined' : 'underlined'"
289
- class="vd-number-field"
290
- title="nirField"
291
- @blur="validateFields(true)"
292
- >
293
- <template #details>
294
- <span class="custom-counter">
295
- {{ numberCounter }}
296
- </span>
297
- </template>
298
- </SyTextField>
299
-
300
- <template v-if="displayKey">
301
- <SyTextField
302
- ref="keyField"
303
- v-model="keyValue"
304
- v-maska="keyMask"
305
- :append-inner-icon="hasKeyErrors ? 'error' : (hasKeySuccess ? 'success' : undefined)"
306
- :aria-errormessage="hasKeyErrors ? 'key-field-errors' : undefined"
307
- :aria-invalid="hasKeyErrors"
308
- :aria-required="required"
309
- :color="hasKeyErrors ? 'error' : 'primary'"
310
- :error="hasKeyErrors"
311
- :hint="locales.keyHint"
312
- :label="keyLabel"
313
- :variant="outlined ? 'outlined' : 'underlined'"
314
- class="vd-key-field"
315
- title="nirKeyField"
316
- @blur="validateFields(true)"
317
- >
318
- <template #details>
319
- <span class="custom-counter">
320
- {{ keyCounter }}
321
- </span>
322
- </template>
323
- </SyTextField>
324
-
325
- <VTooltip v-if="keyTooltip">
326
- <template #activator="{ props: iconProps }">
327
- <VIcon
328
- class="vd-tooltip-icon mt-4 ml-4"
329
- v-bind="{ ...iconProps, ...options.icon }"
330
- >
331
- {{ infoIcon }}
332
- </VIcon>
333
- </template>
334
- <slot name="keyTooltip">
335
- {{ keyTooltip }}
336
- </slot>
337
- </VTooltip>
338
- </template>
339
- </v-input>
340
- </div>
341
- </template>
206
+ const defaultKeyRules = computed(() => {
207
+ const rules: ValidationRule[] = []
342
208
 
343
- <style lang="scss" scoped>
344
- @use '@/assets/tokens';
209
+ if (props.required) {
210
+ rules.push({
211
+ type: 'required',
212
+ options: {
213
+ message: `Le champ ${props.keyLabel} est requis.`,
214
+ fieldIdentifier: props.keyLabel,
215
+ },
216
+ })
217
+ }
345
218
 
346
- .nir-container {
347
- width: v-bind('props.width');
348
- }
219
+ const validateKey = (value: string) => {
220
+ if (!value) return true
221
+ if (!unmaskedNumberValue.value) return true
222
+ const fullNir = unmaskedNumberValue.value + value
223
+ return isNIRKeyValid(fullNir)
224
+ }
349
225
 
350
- .v-messages__message--success {
351
- color: tokens.$colors-border-success !important;
226
+ // Ajout des règles personnalisées
227
+ if (props.customKeyRules) {
228
+ rules.push(...props.customKeyRules)
229
+ }
352
230
 
353
- .v-field--active & {
354
- color: tokens.$colors-border-success !important;
355
- }
356
- }
231
+ // Ajout de la règle de validation par défaut si pas de règle personnalisée avec validation de clé
232
+ if (!props.customKeyRules?.some(rule => rule.options.validate)) {
233
+ rules.push({
234
+ type: 'custom',
235
+ options: {
236
+ validate: validateKey,
237
+ message: 'La clé du numéro de sécurité sociale est invalide.',
238
+ successMessage: `Le champ ${props.keyLabel} est valide.`,
239
+ fieldIdentifier: props.keyLabel,
240
+ },
241
+ })
242
+ }
357
243
 
358
- .v-messages__message--error {
359
- color: tokens.$colors-border-error;
244
+ return rules
245
+ })
360
246
 
361
- .v-field--active & {
362
- color: tokens.$colors-border-error;
363
- }
364
- }
247
+ // Synchronisation avec modelValue
248
+ watch(modelValueRef, (newValue) => {
249
+ if (newValue === undefined) {
250
+ numberValue.value = ''
251
+ keyValue.value = ''
252
+ return
253
+ }
254
+ if (newValue.length === 15) {
255
+ const number = newValue.slice(0, -2)
256
+ const key = newValue.slice(-2)
257
+ numberValue.value = number
258
+ keyValue.value = key
259
+ }
260
+ if (newValue.length === 14) {
261
+ const number = newValue.slice(0, -1)
262
+ const key = newValue.slice(-1)
263
+ numberValue.value = number
264
+ keyValue.value = key
265
+ }
266
+ if (newValue.length === 13) {
267
+ const number = newValue
268
+ numberValue.value = number
269
+ keyValue.value = ''
270
+ }
271
+ }, { immediate: true })
365
272
 
366
- :deep(.v-field.v-field--active .v-label.v-field-label--floating) {
367
- opacity: 1;
368
- }
273
+ // Émission de la valeur
274
+ const emitValue = () => {
275
+ const number = unmaskedNumberValue.value
276
+ const key = unmaskedKeyValue.value
369
277
 
370
- .multi-line {
371
- white-space: pre-line !important;
372
- }
278
+ if (!number && !key) {
279
+ emit('update:modelValue', undefined)
280
+ return
281
+ }
373
282
 
374
- .vd-number-field {
375
- min-width: 296px;
376
- }
283
+ emit('update:modelValue', `${number}${key}`)
284
+ }
377
285
 
378
- .vd-key-field {
379
- min-width: 104px;
380
- flex: none;
381
- margin-left: 1rem;
382
- }
286
+ // Validation des champs
287
+ const validateFields = async (onBlur = false) => {
288
+ // Valider le numéro
289
+ const numberResult = numberValidation.validateField(
290
+ unmaskedNumberValue.value,
291
+ defaultNumberRules.value,
292
+ // N'appliquer les warnings que si le numéro est complet
293
+ unmaskedNumberValue.value?.length === 13 ? props.customNumberWarningRules : [],
294
+ )
295
+
296
+ // Valider la clé si elle est affichée
297
+ let keyResult = { hasError: false }
298
+ if (props.displayKey) {
299
+ keyResult = keyValidation.validateField(
300
+ keyValue.value,
301
+ defaultKeyRules.value,
302
+ // N'appliquer les warnings que si la clé est complète
303
+ keyValue.value?.length === 2 ? props.customKeyWarningRules : [],
304
+ )
305
+ }
383
306
 
384
- .custom-counter {
385
- color: rgb(0 0 0 / 54%);
386
- }
307
+ // Si on est en mode blur et qu'il y a des erreurs, focus sur le premier champ en erreur
308
+ if (onBlur) {
309
+ await nextTick()
310
+ if (numberResult.hasError) {
311
+ numberField.value?.$el.querySelector('input')?.focus()
312
+ }
313
+ else if (keyResult.hasError) {
314
+ keyField.value?.$el.querySelector('input')?.focus()
315
+ }
316
+ }
387
317
 
388
- .vd-nir-field :deep(.v-input__append-inner),
389
- .vd-tooltip-icon {
390
- flex: none;
391
- color: rgb(0 0 0 / 54%);
392
- }
318
+ return !numberResult.hasError && !keyResult.hasError
319
+ }
393
320
 
394
- :deep(.v-overlay__content) {
395
- background: rgb(84 88 89 / 95%) !important;
396
- }
321
+ const validateOnSubmit = () => {
322
+ return validateFields(true)
323
+ }
397
324
 
398
- .vd-nir-field--outlined :deep(.v-messages.error--text) {
399
- padding: 6px;
400
- }
325
+ // Computed pour statut des champs
326
+ const hasNumberErrors = computed(() => numberValidation.hasError.value)
327
+ const hasNumberWarning = computed(() => !hasNumberErrors.value && numberValidation.hasWarning.value)
328
+ const hasNumberSuccess = computed(() => !hasNumberErrors.value && !hasNumberWarning.value && numberValidation.hasSuccess.value)
401
329
 
402
- .vd-nir-field {
403
- container-name: nirfieldwrapper;
404
- }
330
+ const hasKeyErrors = computed(() => keyValidation.hasError.value)
331
+ const hasKeyWarning = computed(() => !hasKeyErrors.value && keyValidation.hasWarning.value)
332
+ const hasKeySuccess = computed(() => !hasKeyErrors.value && !hasKeyWarning.value && keyValidation.hasSuccess.value)
405
333
 
406
- :deep(.v-input__append) {
407
- margin-inline-start: 0 !important;
408
- }
334
+ // Labels avec astérisque si nécessaire
335
+ const numberLabelWithAsterisk = computed(() => {
336
+ return props.required && props.displayAsterisk ? `${props.numberLabel} *` : props.numberLabel
337
+ })
409
338
 
410
- :deep(.vd-number-field > .v-input__prepend) {
411
- margin-right: 0 !important;
412
- }
339
+ const keyLabelWithAsterisk = computed(() => {
340
+ return props.required && props.displayAsterisk ? `${props.keyLabel} *` : props.keyLabel
341
+ })
413
342
 
414
- :deep(.vd-key-field > .v-input__prepend) {
415
- @media screen and (width <= 360px) {
416
- margin-inline-end: 0 !important;
343
+ // Gestion des événements
344
+ const handleNumberInput = () => {
345
+ emitValue()
346
+ validateFields()
417
347
  }
418
- }
419
348
 
420
- :deep(.v-text-field .v-input__details) {
421
- padding-inline: 0 !important;
422
- flex: none !important;
423
- }
349
+ const handleKeyInput = () => {
350
+ emitValue()
351
+ validateFields()
424
352
 
425
- :deep(.v-text-field .v-input__details .v-messages) {
426
- color: rgb(0 0 0 / 100%) !important;
427
- }
353
+ // Si on supprime le contenu de la clé, on revient au champ NIR
354
+ if (unmaskedKeyValue.value.length === 0) {
355
+ nextTick(() => {
356
+ numberField.value?.$el.querySelector('input')?.focus()
357
+ })
358
+ }
359
+ }
428
360
 
429
- @mixin responsive-nir-wrapper {
430
- .vd-nir-field__fields-wrapper :deep(> .v-input__control) {
431
- justify-content: start;
432
- flex-wrap: wrap;
433
- gap: 4px;
434
- margin-bottom: 4px;
361
+ const handleNumberBlur = () => {
362
+ validateFields(true)
363
+ }
435
364
 
436
- .vd-number-field {
437
- flex: 100% 0 0;
438
- }
365
+ const handleKeyBlur = () => {
366
+ validateFields(true)
439
367
  }
440
- }
441
368
 
442
- @container nirFieldwrapper (max-width: 300px) {
443
- @include responsive-nir-wrapper;
369
+ defineExpose({
370
+ validateOnSubmit,
371
+ numberMask,
372
+ keyMask,
373
+ numberValidation,
374
+ keyValidation,
375
+ } satisfies {
376
+ validateOnSubmit: () => Promise<boolean>
377
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
378
+ numberMask: { mask: string, preProcess: (value: string) => string, tokens: Record<string, any> }
379
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
380
+ keyMask: { mask: string, tokens: Record<string, any> }
381
+ numberValidation: ReturnType<typeof useValidation>
382
+ keyValidation: ReturnType<typeof useValidation>
383
+ })
384
+ </script>
385
+
386
+ <template>
387
+ <div
388
+ class="nir-field"
389
+ >
390
+ <div class="number-field-container">
391
+ <SyTextField
392
+ ref="numberField"
393
+ v-model="numberValue"
394
+ v-maska="numberMask"
395
+ :label="numberLabelWithAsterisk"
396
+ :variant-style="outlined ? 'outlined' : 'underlined'"
397
+ :prepend-icon="nirTooltip && nirTooltipPosition === 'prepend' ? 'info' : undefined"
398
+ :append-icon="nirTooltip && nirTooltipPosition === 'append' ? 'info' : undefined"
399
+ :prepend-tooltip="nirTooltip && nirTooltipPosition === 'prepend' ? nirTooltip : undefined"
400
+ :append-tooltip="nirTooltip && nirTooltipPosition === 'append' ? nirTooltip : undefined"
401
+ :max-errors="2"
402
+ :error-messages="[...numberValidation.errors.value, ...keyValidation.errors.value]"
403
+ :warning-messages="numberValidation.warnings.value"
404
+ :success-messages="numberValidation.successes.value"
405
+ :show-success-messages="showSuccessMessages"
406
+ :has-warning="hasNumberWarning"
407
+ :has-success="hasNumberSuccess"
408
+ :error="hasNumberErrors"
409
+ :messages="hasNumberErrors ? numberValidation.errors.value : (hasNumberWarning ? numberValidation.warnings.value : (hasNumberSuccess ? numberValidation.successes.value : []))"
410
+ :has-error="hasNumberErrors"
411
+ :required="required"
412
+ :is-disabled="isDisabled"
413
+ :bg-color="bgColor"
414
+ :density="props.density"
415
+ :hide-details="props.hideDetails"
416
+ :hide-spin-buttons="props.hideSpinButtons"
417
+ :placeholder="props.placeholder"
418
+ :readonly="props.readonly"
419
+ :variant="props.variant"
420
+ :clearable="props.clearable"
421
+ :counter="props.counter"
422
+ :persistent-hint="props.persistentHint"
423
+ :persistent-placeholder="props.persistentPlaceholder"
424
+ :hint="props.hint || locales.numberHint"
425
+ class="number-field"
426
+ :display-asterisk="false"
427
+ @input="handleNumberInput"
428
+ @blur="handleNumberBlur"
429
+ />
430
+ </div>
431
+ <div
432
+ v-if="displayKey"
433
+ class="key-field-container"
434
+ >
435
+ <SyTextField
436
+ ref="keyField"
437
+ v-model="keyValue"
438
+ v-maska="keyMask"
439
+ :label="keyLabelWithAsterisk"
440
+ :variant-style="outlined ? 'outlined' : 'underlined'"
441
+ :prepend-icon="keyTooltip && keyTooltipPosition === 'prepend' ? 'info' : undefined"
442
+ :append-icon="keyTooltip && keyTooltipPosition === 'append' ? 'info' : undefined"
443
+ :prepend-tooltip="keyTooltip && keyTooltipPosition === 'prepend' ? keyTooltip : undefined"
444
+ :append-tooltip="keyTooltip && keyTooltipPosition === 'append' ? keyTooltip : undefined"
445
+ :error-messages="keyValidation.errors.value.length > 0 ? [''] : []"
446
+ :warning-messages="keyValidation.warnings.value"
447
+ :success-messages="keyValidation.successes.value"
448
+ :show-success-messages="showSuccessMessages"
449
+ :has-warning="hasKeyWarning"
450
+ :has-success="hasKeySuccess"
451
+ :hint="props.hint || locales.keyHint"
452
+ :messages="hasKeyErrors ? keyValidation.errors.value : (hasKeyWarning ? keyValidation.warnings.value : (hasKeySuccess ? keyValidation.successes.value : []))"
453
+ :has-error="hasKeyErrors"
454
+ :is-disabled="isDisabled"
455
+ :bg-color="bgColor"
456
+ :density="props.density"
457
+ :hide-details="props.hideDetails"
458
+ :hide-spin-buttons="props.hideSpinButtons"
459
+ :placeholder="props.placeholder"
460
+ :readonly="props.readonly"
461
+ :variant="props.variant"
462
+ :clearable="props.clearable"
463
+ :counter="props.counter"
464
+ :persistent-hint="props.persistentHint"
465
+ :persistent-placeholder="props.persistentPlaceholder"
466
+ class="key-field"
467
+ :display-asterisk="false"
468
+ @input="handleKeyInput"
469
+ @blur="handleKeyBlur"
470
+ />
471
+ </div>
472
+ </div>
473
+ </template>
474
+
475
+ <style lang="scss" scoped>
476
+ .nir-field {
477
+ display: flex;
478
+ gap: 16px;
479
+ width: v-bind('props.width');
480
+ align-items: flex-start;
444
481
  }
445
482
 
446
- @media screen and (width <= 360px) {
447
- @include responsive-nir-wrapper;
483
+ .number-field-container {
484
+ flex: 0 0 80%;
448
485
  }
449
486
 
450
- .v-text-field .v-input__append-inner {
451
- padding-left: 0 !important;
487
+ .key-field-container {
488
+ flex: 0 0 20%;
452
489
  }
453
490
 
454
- :deep(.v-text-field > .v-input__control > .v-input__slot > .v-text-field__slot) {
455
- width: min-content !important;
491
+ .number-field,
492
+ .key-field {
493
+ width: 100%;
456
494
  }
457
495
  </style>