| 1 |
import { ref, type Ref } from 'vue' |
| 2 |
import type { Field } from '@/types/field' |
| 3 |
import { useApiClient } from '@/composables/useApiClient' |
| 4 |
|
| 5 |
export type DuplicateCheckState = { |
| 6 |
value: string |
| 7 |
passed: boolean |
| 8 |
} |
| 9 |
|
| 10 |
export type ValidateBeforeSubmitResult = { |
| 11 |
valid: boolean |
| 12 |
is_duplicate?: boolean |
| 13 |
duplicate_errors?: Record<string, boolean> |
| 14 |
} |
| 15 |
|
| 16 |
type FormValues = Record<string, string | number | null | undefined> |
| 17 |
|
| 18 |
export const getFieldValueKey = (field: Field): string => `${field.type}_${field.fieldIndex}` |
| 19 |
|
| 20 |
export const processWebsiteFieldValues = (fields: Field[], values: FormValues): FormValues => { |
| 21 |
const processed = { ...values } |
| 22 |
|
| 23 |
fields.forEach((field) => { |
| 24 |
if (field.type !== 'website') { |
| 25 |
return |
| 26 |
} |
| 27 |
|
| 28 |
const key = getFieldValueKey(field) |
| 29 |
const fullValue = processed[key] || '' |
| 30 |
const prefix = field.inputPrefix || '' |
| 31 |
const suffix = field.inputSuffix || '' |
| 32 |
let userInput = String(fullValue) |
| 33 |
|
| 34 |
if (fullValue && (prefix || suffix)) { |
| 35 |
if (prefix && !userInput.startsWith(prefix)) { |
| 36 |
userInput = prefix + userInput |
| 37 |
} |
| 38 |
if (suffix && !userInput.endsWith(suffix)) { |
| 39 |
userInput = userInput + suffix |
| 40 |
} |
| 41 |
} |
| 42 |
|
| 43 |
processed[key] = userInput |
| 44 |
}) |
| 45 |
|
| 46 |
return processed |
| 47 |
} |
| 48 |
|
| 49 |
const shouldSkipDuplicateCheck = ( |
| 50 |
stateByKey: Record<string, DuplicateCheckState>, |
| 51 |
fieldKey: string, |
| 52 |
currentValue: string, |
| 53 |
): boolean => { |
| 54 |
const state = stateByKey[fieldKey] |
| 55 |
return state !== undefined && state.value === currentValue && state.passed |
| 56 |
} |
| 57 |
|
| 58 |
export function buildDuplicateCheckValues( |
| 59 |
fields: Field[], |
| 60 |
formValues: FormValues, |
| 61 |
stateByKey: Record<string, DuplicateCheckState>, |
| 62 |
options?: { bypassCache?: boolean }, |
| 63 |
): { values: FormValues; fieldsToCheck: Field[] } { |
| 64 |
const values: FormValues = {} |
| 65 |
const fieldsToCheck: Field[] = [] |
| 66 |
|
| 67 |
for (const field of fields) { |
| 68 |
if (!field.noDuplicates) { |
| 69 |
continue |
| 70 |
} |
| 71 |
|
| 72 |
const fieldKey = getFieldValueKey(field) |
| 73 |
const rawValue = formValues[fieldKey] |
| 74 |
if (rawValue === undefined || rawValue === null || String(rawValue).trim() === '') { |
| 75 |
continue |
| 76 |
} |
| 77 |
|
| 78 |
const currentValue = String(rawValue) |
| 79 |
if (!options?.bypassCache && shouldSkipDuplicateCheck(stateByKey, fieldKey, currentValue)) { |
| 80 |
continue |
| 81 |
} |
| 82 |
|
| 83 |
values[fieldKey] = rawValue |
| 84 |
fieldsToCheck.push(field) |
| 85 |
} |
| 86 |
|
| 87 |
return { values, fieldsToCheck } |
| 88 |
} |
| 89 |
|
| 90 |
export async function callValidateBeforeSubmit( |
| 91 |
request: ReturnType<typeof useApiClient>['request'], |
| 92 |
params: { |
| 93 |
formId: string | number |
| 94 |
values: FormValues |
| 95 |
nonce: string |
| 96 |
pageId?: string |
| 97 |
}, |
| 98 |
): Promise<{ result: ValidateBeforeSubmitResult | null; error: unknown }> { |
| 99 |
const { data, error, status } = await request('form/validate_before_submit/', { |
| 100 |
method: 'POST', |
| 101 |
headers: { |
| 102 |
'Content-Type': 'application/json', |
| 103 |
}, |
| 104 |
data: { |
| 105 |
formId: params.formId, |
| 106 |
values: params.values, |
| 107 |
nonce: params.nonce, |
| 108 |
...(params.pageId ? { pageId: params.pageId } : {}), |
| 109 |
}, |
| 110 |
}) |
| 111 |
|
| 112 |
if (error || status !== 200 || !data) { |
| 113 |
return { result: null, error: error || data } |
| 114 |
} |
| 115 |
|
| 116 |
const payload = (data as { data?: { data?: ValidateBeforeSubmitResult } }).data?.data |
| 117 |
if (!payload) { |
| 118 |
return { result: null, error: 'Invalid validate_before_submit response' } |
| 119 |
} |
| 120 |
|
| 121 |
return { result: payload, error: null } |
| 122 |
} |
| 123 |
|
| 124 |
export function useValidateBeforeSubmit(options: { |
| 125 |
request: ReturnType<typeof useApiClient>['request'] |
| 126 |
getLabel: (key: string) => string |
| 127 |
formData: Ref<{ id: string | number }> |
| 128 |
formFields: Ref<Field[]> |
| 129 |
formValues: Ref<FormValues> |
| 130 |
formNonce: Ref<string | null> |
| 131 |
fieldErrors: Ref<Record<string, string>> |
| 132 |
getPageFields: (pageId: string) => Field[] |
| 133 |
}) { |
| 134 |
const duplicateCheckState = ref<Record<string, DuplicateCheckState>>({}) |
| 135 |
|
| 136 |
const applyDuplicateErrors = (duplicateFields: Record<string, boolean>) => { |
| 137 |
for (const field of options.formFields.value) { |
| 138 |
const fieldId = String(field.id) |
| 139 |
if (!duplicateFields[field.id] && !duplicateFields[fieldId]) { |
| 140 |
continue |
| 141 |
} |
| 142 |
|
| 143 |
const fieldKey = getFieldValueKey(field) |
| 144 |
const fieldValue = options.formValues.value[fieldKey] |
| 145 |
if (!fieldValue || fieldValue.toString().trim() === '') { |
| 146 |
continue |
| 147 |
} |
| 148 |
|
| 149 |
options.fieldErrors.value[fieldKey] = options |
| 150 |
.getLabel('duplicate_value_error_label') |
| 151 |
.replace('{label}', field.label?.toLowerCase() || 'value') |
| 152 |
} |
| 153 |
} |
| 154 |
|
| 155 |
const recordDuplicateCheckResults = (fieldsToCheck: Field[], passed: boolean) => { |
| 156 |
for (const field of fieldsToCheck) { |
| 157 |
const fieldKey = getFieldValueKey(field) |
| 158 |
const rawValue = options.formValues.value[fieldKey] |
| 159 |
if (rawValue === undefined || rawValue === null) { |
| 160 |
continue |
| 161 |
} |
| 162 |
duplicateCheckState.value[fieldKey] = { |
| 163 |
value: String(rawValue), |
| 164 |
passed, |
| 165 |
} |
| 166 |
} |
| 167 |
} |
| 168 |
|
| 169 |
const validateDuplicatesBeforeProceed = async ( |
| 170 |
pageId?: string, |
| 171 |
checkOptions?: { bypassCache?: boolean }, |
| 172 |
): Promise<'ok' | 'duplicate' | 'request_failed'> => { |
| 173 |
const scopeFields = pageId |
| 174 |
? options.getPageFields(pageId).filter((field) => field.noDuplicates) |
| 175 |
: options.formFields.value.filter((field) => field.noDuplicates) |
| 176 |
|
| 177 |
if (scopeFields.length === 0) { |
| 178 |
return 'ok' |
| 179 |
} |
| 180 |
|
| 181 |
const { values, fieldsToCheck } = buildDuplicateCheckValues( |
| 182 |
scopeFields, |
| 183 |
options.formValues.value, |
| 184 |
duplicateCheckState.value, |
| 185 |
checkOptions, |
| 186 |
) |
| 187 |
|
| 188 |
if (fieldsToCheck.length === 0) { |
| 189 |
return 'ok' |
| 190 |
} |
| 191 |
|
| 192 |
const processedValues = processWebsiteFieldValues(scopeFields, values) |
| 193 |
|
| 194 |
const nonce = options.formNonce.value |
| 195 |
if (!nonce) { |
| 196 |
return 'request_failed' |
| 197 |
} |
| 198 |
|
| 199 |
const { result, error } = await callValidateBeforeSubmit(options.request, { |
| 200 |
formId: options.formData.value.id, |
| 201 |
values: processedValues, |
| 202 |
nonce, |
| 203 |
pageId, |
| 204 |
}) |
| 205 |
|
| 206 |
if (!result) { |
| 207 |
console.error('validate_before_submit failed:', error) |
| 208 |
return 'request_failed' |
| 209 |
} |
| 210 |
|
| 211 |
if (result.valid) { |
| 212 |
recordDuplicateCheckResults(fieldsToCheck, true) |
| 213 |
return 'ok' |
| 214 |
} |
| 215 |
|
| 216 |
recordDuplicateCheckResults(fieldsToCheck, false) |
| 217 |
if (result.duplicate_errors) { |
| 218 |
applyDuplicateErrors(result.duplicate_errors) |
| 219 |
} |
| 220 |
return 'duplicate' |
| 221 |
} |
| 222 |
|
| 223 |
return { |
| 224 |
duplicateCheckState, |
| 225 |
validateDuplicatesBeforeProceed, |
| 226 |
applyDuplicateErrors, |
| 227 |
} |
| 228 |
} |
| 229 |
|
| 230 |
/** |
| 231 |
* Standalone client for integrations (e.g. Amelia booking flow). |
| 232 |
*/ |
| 233 |
export async function validateBeforeSubmitForIntegration(params: { |
| 234 |
formId: string | number |
| 235 |
values: FormValues |
| 236 |
nonce: string |
| 237 |
pageId?: string |
| 238 |
}): Promise<ValidateBeforeSubmitResult> { |
| 239 |
const { request } = useApiClient() |
| 240 |
const { result, error } = await callValidateBeforeSubmit(request, params) |
| 241 |
|
| 242 |
if (!result) { |
| 243 |
throw error instanceof Error ? error : new Error(String(error)) |
| 244 |
} |
| 245 |
|
| 246 |
return result |
| 247 |
} |
| 248 |
|