import { unref } from 'vue' import type { ValidationRule } from '@/composables/unifyValidation/useValidation' import { locales } from '../../locales' import type { DatePickerRule } from '../../types' import { adaptCustomRules, validateEmptyOrIncompleteDate } from '../../utils/validationUtils' import { getRangeValidationError } from '../../utils/dateFormattingUtils' import { validateDateFormat, isDateComplete } from '../useDateFormatUtils' import type { ValidationContext } from './types' /** * Flow 3 : validateTextInput — validation de saisie texte (noCalendar / DateTextInput). * * Trois cas : * 1. Mode Vuetify natif → déléguer à useCustomValidation * 2. Valeur vide → valider required + custom rules sur null (champ vide) * 3. Plage de dates (avec séparateur) → valider start et end séparément, fusionner * 4. Date unique → validateSingleTextInput */ /** * Crée le flow validateTextInput et les helpers associés. * * @param ctx Le contexte de validation partagé * @returns `{ validateTextInput }` */ export function createValidateTextInputFlow(ctx: ValidationContext) { const { options } = ctx /** * Filtre un tableau de règles pour ne garder que celles qui sont "prêtes". * Une règle notBeforeDate/notAfterDate/exactDate est "prête" si son options.date est défini. */ const filterReadyRules = (rules: DatePickerRule[]): DatePickerRule[] => { return rules.filter((rule) => { if (rule.type === 'notBeforeDate' || rule.type === 'notAfterDate' || rule.type === 'exactDate') { return rule.options && rule.options.date !== undefined } return true }) } /** * Filtre les custom rules pour ne garder que celles qui sont "prêtes". * Si aucune règle n'est prête alors qu'il y en a, retourne null pour skip la validation * (évite l'erreur "Configuration de la règle invalide" quand les computed réactifs * ne sont pas encore mis à jour, ex: dateA pas encore sélectionnée pour dateBRules). */ const getReadyCustomRules = (): DatePickerRule[] | null => { const currentCustomRules = options.customRules.value const readyRules = filterReadyRules(currentCustomRules) if (readyRules.length === 0 && currentCustomRules.length > 0) { return null } return readyRules } /** * Adapte les règles DatePicker au format du validateur unifié. * Les règles personnalisées de saisie texte reçoivent une chaîne formatée : * au format d'affichage par défaut, ou au format de retour (`dateFormatReturn || format`) * quand `validateWithReturnFormat` est activé (option `returnFormat`). */ const getAdaptedRules = ( customRules: DatePickerRule[], warningRules: DatePickerRule[], successRules: DatePickerRule[], ): { customRules: ValidationRule[], warningRules: ValidationRule[], successRules: ValidationRule[] } => { const format = unref(options.returnFormat) || unref(options.displayFormat) || '' return { customRules: adaptCustomRules(customRules, format) as unknown as ValidationRule[], warningRules: adaptCustomRules(warningRules, format) as unknown as ValidationRule[], successRules: adaptCustomRules(successRules, format) as unknown as ValidationRule[], } } /** * Valide une date (objet Date) avec les custom rules filtrées et adaptées. * Retourne true si valide, false si erreur. * Si shouldDisplayErrors est false, retourne juste l'état sans afficher. */ const validateCustomRulesForDate = (date: Date, token: number): boolean | Promise => { if (ctx.shouldDisplayErrors() === false) { return !ctx.displayHasError.value } const readyRules = getReadyCustomRules() if (readyRules === null) { return true } const adapted = getAdaptedRules(readyRules, filterReadyRules(options.customWarningRules.value), filterReadyRules(options.customSuccessRules?.value ?? [])) const result = ctx.validateField(date, adapted.customRules, adapted.warningRules, adapted.successRules) if (result instanceof Promise) { return result.then(resolvedResult => ( token === ctx.currentValidationToken.value && !resolvedResult.hasError )) } return !result.hasError } /** * Valide une saisie texte correspondant à une date unique. * * Étapes : * 1. Vérifie si la valeur est vide ou incomplète (validateEmptyOrIncompleteDate) * 2. Valide le format (validateDateFormat) * 3. Parse la date avec parseDate * 4. Valide la date parsée avec les custom rules (validateCustomRulesForDate) */ const validateSingleTextInput = async (value: string, token: number, force = false): Promise => { const format = unref(options.displayFormat) ?? '' // 1. Vérifier si vide ou incomplet const emptyCheck = validateEmptyOrIncompleteDate( value, unref(options.required), (dateValue: string) => isDateComplete(dateValue, format), options.hasInteracted?.value ?? false, ) if (!emptyCheck.isValid && emptyCheck.errorMessage && ctx.shouldDisplayErrors()) { ctx.pushError(emptyCheck.errorMessage) } if (!emptyCheck.shouldContinue) { // Une date incomplète est tolérée pendant la frappe (isValid: true) mais // invalide en validation forcée (blur/submit) : l'utilisateur a fini de // saisir et le modèle ne peut pas refléter une valeur partielle. if (force && emptyCheck.isValid) { if (ctx.shouldDisplayErrors()) { ctx.pushError(locales.invalidDateFormatWithFormat(format)) } return false } return emptyCheck.isValid } // 2. Valider le format const formatValidation = validateDateFormat( value, format, format, unref(options.required), options.hasInteracted?.value ?? false, !ctx.shouldDisplayErrors(), ) if (!formatValidation.isValid) { if (ctx.shouldDisplayErrors()) { ctx.pushError(formatValidation.message) } return false } // 3. Parser la date const date = options.parseDate?.(value, format) ?? null if (!date) { if (ctx.shouldDisplayErrors()) { ctx.pushError(locales.invalidDateFormatWithFormat(format)) } return false } // 4. Valider avec les custom rules const isValid = await validateCustomRulesForDate(date, token) if (token !== ctx.currentValidationToken.value) return !ctx.displayHasError.value return isValid } const validateTextInput = async (value: string, force = false): Promise => { const token = ++ctx.currentValidationToken.value ctx.clearValidation() // 1. Mode Vuetify natif if (unref(options.useVuetifyValidation)) { const hasInteracted = options.hasInteracted?.value ?? false if (!hasInteracted) { return true } const result = await Promise.resolve(ctx.validateField(value)) if (token !== ctx.currentValidationToken.value) return !ctx.displayHasError.value return !result.hasError } // 2. Valeur vide → valider required + custom rules sur null if (!value || value.trim() === '') { if (unref(options.required) && (options.hasInteracted?.value ?? false) && !unref(options.readonly) && ctx.shouldDisplayErrors()) { ctx.pushError(locales.required) return false } // Si des customRules existent et que l'utilisateur a interagi, les exécuter sur null // (permet aux règles métier de valider les champs vides, ex: date obligatoire conditionnelle) if (options.customRules.value.length > 0 && (options.hasInteracted?.value ?? false)) { const readyRules = getReadyCustomRules() if (readyRules === null) { return true } const adapted = getAdaptedRules(readyRules, filterReadyRules(options.customWarningRules.value), filterReadyRules(options.customSuccessRules?.value ?? [])) const result = await ctx.validateField( null, adapted.customRules, adapted.warningRules, adapted.successRules, ) if (token !== ctx.currentValidationToken.value) return !ctx.displayHasError.value return !result.hasError } return true } // 3. Plage de dates (avec séparateur) if (unref(options.displayRange) && value.includes(locales.rangeSeparator)) { const [startDateText = '', endDateText = ''] = value.split(locales.rangeSeparator) // Si seulement la date de début est saisie, valider juste celle-ci if (startDateText && !endDateText) { return await validateSingleTextInput(startDateText, token, force) } // Si ni start ni end n'est saisie, retourner l'état courant if (!(startDateText && endDateText)) { return !ctx.displayHasError.value } // Valider le format des deux dates const format = unref(options.displayFormat) ?? '' const startFormatValidation = validateDateFormat(startDateText, format, format, unref(options.required), options.hasInteracted?.value ?? false, !ctx.shouldDisplayErrors()) const endFormatValidation = validateDateFormat(endDateText, format, format, unref(options.required), options.hasInteracted?.value ?? false, !ctx.shouldDisplayErrors()) if (!startFormatValidation.isValid) { if (ctx.shouldDisplayErrors()) ctx.pushError(startFormatValidation.message) return false } if (!endFormatValidation.isValid) { if (ctx.shouldDisplayErrors()) ctx.pushError(endFormatValidation.message) return false } // Parser les deux dates const startDate = options.parseDate?.(startDateText, format) ?? null const endDate = options.parseDate?.(endDateText, format) ?? null if (!(startDate && endDate)) { return !ctx.displayHasError.value } // Valider la plage (start <= end) // Note : on appelle getRangeValidationError directement (et non useDateRangeValidation) // car les dates sont parsées depuis le texte et ne sont pas encore dans selectedDates. const rangeErrors: string[] = [] const rangeError = getRangeValidationError(startDate, endDate) if (rangeError && ctx.shouldDisplayErrors()) { rangeErrors.push(rangeError) } // Valider les custom rules pour chaque date, puis fusionner les résultats await validateCustomRulesForDate(startDate, token) if (token !== ctx.currentValidationToken.value) return !ctx.displayHasError.value const startErrors = [...ctx.errors.value] const startWarnings = [...ctx.warnings.value] const startSuccesses = [...ctx.successes.value] await validateCustomRulesForDate(endDate, token) if (token !== ctx.currentValidationToken.value) return !ctx.displayHasError.value // Fusionner : range errors + start errors + end errors ctx.replaceErrors([...rangeErrors, ...startErrors, ...ctx.errors.value]) ctx.warnings.value = [...new Set([...startWarnings, ...ctx.warnings.value].filter(Boolean))] ctx.successes.value = [...new Set([...startSuccesses, ...ctx.successes.value].filter(Boolean))] return !ctx.displayHasError.value } // 4. Date unique return await validateSingleTextInput(value, token, force) } return { validateTextInput } }