Files
iistwin/server/utils/validate-required-fields.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

131 lines
4.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Валидация условной обязательности полей формы.
*/
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);
}