import { ref, type Ref } from 'vue' import type { Field } from '@/types/field' import { useApiClient } from '@/composables/useApiClient' export type DuplicateCheckState = { value: string passed: boolean } export type ValidateBeforeSubmitResult = { valid: boolean is_duplicate?: boolean duplicate_errors?: Record } type FormValues = Record export const getFieldValueKey = (field: Field): string => `${field.type}_${field.fieldIndex}` export const processWebsiteFieldValues = (fields: Field[], values: FormValues): FormValues => { const processed = { ...values } fields.forEach((field) => { if (field.type !== 'website') { return } const key = getFieldValueKey(field) const fullValue = processed[key] || '' const prefix = field.inputPrefix || '' const suffix = field.inputSuffix || '' let userInput = String(fullValue) if (fullValue && (prefix || suffix)) { if (prefix && !userInput.startsWith(prefix)) { userInput = prefix + userInput } if (suffix && !userInput.endsWith(suffix)) { userInput = userInput + suffix } } processed[key] = userInput }) return processed } const shouldSkipDuplicateCheck = ( stateByKey: Record, fieldKey: string, currentValue: string, ): boolean => { const state = stateByKey[fieldKey] return state !== undefined && state.value === currentValue && state.passed } export function buildDuplicateCheckValues( fields: Field[], formValues: FormValues, stateByKey: Record, options?: { bypassCache?: boolean }, ): { values: FormValues; fieldsToCheck: Field[] } { const values: FormValues = {} const fieldsToCheck: Field[] = [] for (const field of fields) { if (!field.noDuplicates) { continue } const fieldKey = getFieldValueKey(field) const rawValue = formValues[fieldKey] if (rawValue === undefined || rawValue === null || String(rawValue).trim() === '') { continue } const currentValue = String(rawValue) if (!options?.bypassCache && shouldSkipDuplicateCheck(stateByKey, fieldKey, currentValue)) { continue } values[fieldKey] = rawValue fieldsToCheck.push(field) } return { values, fieldsToCheck } } export async function callValidateBeforeSubmit( request: ReturnType['request'], params: { formId: string | number values: FormValues nonce: string pageId?: string }, ): Promise<{ result: ValidateBeforeSubmitResult | null; error: unknown }> { const { data, error, status } = await request('form/validate_before_submit/', { method: 'POST', headers: { 'Content-Type': 'application/json', }, data: { formId: params.formId, values: params.values, nonce: params.nonce, ...(params.pageId ? { pageId: params.pageId } : {}), }, }) if (error || status !== 200 || !data) { return { result: null, error: error || data } } const payload = (data as { data?: { data?: ValidateBeforeSubmitResult } }).data?.data if (!payload) { return { result: null, error: 'Invalid validate_before_submit response' } } return { result: payload, error: null } } export function useValidateBeforeSubmit(options: { request: ReturnType['request'] getLabel: (key: string) => string formData: Ref<{ id: string | number }> formFields: Ref formValues: Ref formNonce: Ref fieldErrors: Ref> getPageFields: (pageId: string) => Field[] }) { const duplicateCheckState = ref>({}) const applyDuplicateErrors = (duplicateFields: Record) => { for (const field of options.formFields.value) { const fieldId = String(field.id) if (!duplicateFields[field.id] && !duplicateFields[fieldId]) { continue } const fieldKey = getFieldValueKey(field) const fieldValue = options.formValues.value[fieldKey] if (!fieldValue || fieldValue.toString().trim() === '') { continue } options.fieldErrors.value[fieldKey] = options .getLabel('duplicate_value_error_label') .replace('{label}', field.label?.toLowerCase() || 'value') } } const recordDuplicateCheckResults = (fieldsToCheck: Field[], passed: boolean) => { for (const field of fieldsToCheck) { const fieldKey = getFieldValueKey(field) const rawValue = options.formValues.value[fieldKey] if (rawValue === undefined || rawValue === null) { continue } duplicateCheckState.value[fieldKey] = { value: String(rawValue), passed, } } } const validateDuplicatesBeforeProceed = async ( pageId?: string, checkOptions?: { bypassCache?: boolean }, ): Promise<'ok' | 'duplicate' | 'request_failed'> => { const scopeFields = pageId ? options.getPageFields(pageId).filter((field) => field.noDuplicates) : options.formFields.value.filter((field) => field.noDuplicates) if (scopeFields.length === 0) { return 'ok' } const { values, fieldsToCheck } = buildDuplicateCheckValues( scopeFields, options.formValues.value, duplicateCheckState.value, checkOptions, ) if (fieldsToCheck.length === 0) { return 'ok' } const processedValues = processWebsiteFieldValues(scopeFields, values) const nonce = options.formNonce.value if (!nonce) { return 'request_failed' } const { result, error } = await callValidateBeforeSubmit(options.request, { formId: options.formData.value.id, values: processedValues, nonce, pageId, }) if (!result) { console.error('validate_before_submit failed:', error) return 'request_failed' } if (result.valid) { recordDuplicateCheckResults(fieldsToCheck, true) return 'ok' } recordDuplicateCheckResults(fieldsToCheck, false) if (result.duplicate_errors) { applyDuplicateErrors(result.duplicate_errors) } return 'duplicate' } return { duplicateCheckState, validateDuplicatesBeforeProceed, applyDuplicateErrors, } } /** * Standalone client for integrations (e.g. Amelia booking flow). */ export async function validateBeforeSubmitForIntegration(params: { formId: string | number values: FormValues nonce: string pageId?: string }): Promise { const { request } = useApiClient() const { result, error } = await callValidateBeforeSubmit(request, params) if (!result) { throw error instanceof Error ? error : new Error(String(error)) } return result }