- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
131 lines
4.2 KiB
TypeScript
131 lines
4.2 KiB
TypeScript
/**
|
||
* Валидация условной обязательности полей формы.
|
||
*/
|
||
|
||
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<string, unknown>;
|
||
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<FieldValidationError[]> {
|
||
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<string[]> {
|
||
const detailed = await validateRequiredFieldsDetailed(options);
|
||
return detailed.map((e) => e.message);
|
||
}
|