/** * Валидация условной обязательности полей формы. */ import { isFieldRequired, isFieldVisible, isFieldValueEmpty, type FieldConditionContext, } from "@shared/field-conditions"; import { buildFieldConditionContext } from "./field-condition-context"; export interface FieldValidationError { fieldId: number; message: string; } export interface ValidateRequiredFieldsOptions { task: { currentStatusId?: number | null; formId?: number; [key: string]: any }; formFields: Array<{ id: number; code: string; name: string; type?: string; options?: unknown; isRequired?: boolean | null; requiredRules?: any; visibilityRules?: any; autoProlongationLabel?: string | null; }>; customFields?: Record; existingFieldValues?: Array<{ fieldId: number; value: unknown }>; organizationId: number; storage: any; isCreation?: boolean; statuses?: Array<{ id: number; position: number; isFinal?: boolean | null }>; users?: Array<{ id: number; firstName?: string | null; middleName?: string | null; lastName?: string | null; email?: string }>; roles?: Array<{ id: number; name?: string }>; /** Если true, скрытые обязательные поля не будут требовать заполнения. */ respectVisibility?: boolean; /** Если задан, проверяются только указанные поля (по id). */ fieldIdsToCheck?: number[]; } export async function validateRequiredFieldsDetailed( options: ValidateRequiredFieldsOptions ): Promise { const { task, formFields, customFields, existingFieldValues, organizationId, storage, statuses: providedStatuses, users: providedUsers, roles: providedRoles, respectVisibility = true, fieldIdsToCheck, } = options; const fieldsWithRules = formFields.filter( (f) => (f.isRequired || (f.requiredRules && f.requiredRules.enabled)) && (!fieldIdsToCheck || fieldIdsToCheck.includes(f.id)) ); if (fieldsWithRules.length === 0) return []; let baseFieldValues: Array<{ fieldId: number; value: unknown }> = existingFieldValues ? [...existingFieldValues] : []; if (customFields && !existingFieldValues) { const codeToId = new Map(formFields.map((f) => [f.code, f.id])); for (const [key, value] of Object.entries(customFields)) { let fieldId: number | undefined; if (key.startsWith('customField_')) { fieldId = parseInt(key.replace('customField_', ''), 10); } else if (codeToId.has(key)) { fieldId = codeToId.get(key); } if (fieldId === undefined) continue; const idx = baseFieldValues.findIndex((fv) => fv.fieldId === fieldId); if (idx >= 0) { baseFieldValues[idx] = { fieldId, value }; } else { baseFieldValues.push({ fieldId, value }); } } } const ctxResult = await buildFieldConditionContext({ task, formFields, organizationId, storage, isCreation: options.isCreation, statuses: providedStatuses, existingFieldValues: baseFieldValues, users: providedUsers, roles: providedRoles, }); const { fields, fieldValues, ...context } = ctxResult; const errors: FieldValidationError[] = []; for (const field of fieldsWithRules) { const required = isFieldRequired(field as any, task, fieldValues, fields, context); if (!required) continue; // Если поле скрыто по visibilityRules, не требовать его заполнения. if (respectVisibility) { const visible = isFieldVisible(field as any, task, fieldValues, fields, context); if (!visible) continue; } const fv = fieldValues.find((v) => v.fieldId === field.id); if (isFieldValueEmpty(fv?.value, field.type)) { errors.push({ fieldId: field.id, message: `Поле «${field.name}» обязательно к заполнению` }); } } return errors; } export async function validateRequiredFields( options: ValidateRequiredFieldsOptions ): Promise { const detailed = await validateRequiredFieldsDetailed(options); return detailed.map((e) => e.message); }