diff --git a/components/controller/src/extensions/reports/application/dto/requisites.dto.ts b/components/controller/src/extensions/reports/application/dto/requisites.dto.ts index e1200ef03bf..7f64ae5abb9 100644 --- a/components/controller/src/extensions/reports/application/dto/requisites.dto.ts +++ b/components/controller/src/extensions/reports/application/dto/requisites.dto.ts @@ -1,6 +1,7 @@ import { ObjectType, Field, InputType, registerEnumType } from '@nestjs/graphql'; import { IsOptional, IsString, IsIn, Matches, MaxLength } from 'class-validator'; import { ReportType } from '../../domain/enums/report-type.enum'; +import { SFR_REG_NUMBER_PATTERN } from '../../domain/patterns'; export enum RequisiteSource { DATABASE = 'database', @@ -65,7 +66,7 @@ export class ReportRequisitesViewDTO { * ОКТМО — `\d{8}` или `\d{11}` * ОКПО — `\d{8}` или `\d{10}` * СНИЛС — `\d{3}-\d{3}-\d{3} \d{2}` (14 симв.) или 11 цифр подряд - * РегНом СФР — `\d{3}-\d{3}-\d{6}` (14 симв. с тире) + * РегНом СФР — `\d{3}-\d{3}-\d{6}` (14 симв. с тире) или 10 цифр подряд */ @InputType('UpdateReportRequisitesInput') export class UpdateReportRequisitesInputDTO { @@ -99,10 +100,10 @@ export class UpdateReportRequisitesInputDTO { @Matches(/^\d{8}(\d{2})?$/, { message: 'ОКПО — 8 или 10 цифр' }) okpo?: string | null; - @Field(() => String, { nullable: true, description: 'Рег. номер СФР — XXX-XXX-XXXXXX' }) + @Field(() => String, { nullable: true, description: 'Рег. номер СФР — XXX-XXX-XXXXXX или 10 цифр' }) @IsOptional() @IsString() - @Matches(/^\d{3}-\d{3}-\d{6}$/, { message: 'Рег. номер СФР — XXX-XXX-XXXXXX (14 симв.)' }) + @Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег. номер СФР — XXX-XXX-XXXXXX или 10 цифр' }) sfrRegNumber?: string | null; @Field(() => String, { nullable: true }) diff --git a/components/controller/src/extensions/reports/application/dto/zero-report-edits.dto.ts b/components/controller/src/extensions/reports/application/dto/zero-report-edits.dto.ts index 0e1df669f5a..470ba6f0f04 100644 --- a/components/controller/src/extensions/reports/application/dto/zero-report-edits.dto.ts +++ b/components/controller/src/extensions/reports/application/dto/zero-report-edits.dto.ts @@ -200,7 +200,7 @@ export class ZeroReportSignerEditsInputDTO { @Field(() => String, { nullable: true }) @IsOptional() - @Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег.номер СФР — XXX-XXX-XXXXXX' }) + @Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег.номер СФР — XXX-XXX-XXXXXX или 10 цифр' }) sfrRegNumber!: string | null; @Field(() => String, { nullable: true }) diff --git a/components/controller/src/extensions/reports/domain/patterns.ts b/components/controller/src/extensions/reports/domain/patterns.ts index 9cc6ec49d07..4c9fe333120 100644 --- a/components/controller/src/extensions/reports/domain/patterns.ts +++ b/components/controller/src/extensions/reports/domain/patterns.ts @@ -43,8 +43,9 @@ export const OKPO_PATTERN = /^\d{8}(\d{2})?$/; // СНИЛС: XXX-XXX-XXX YY (с пробелом/тире) или 11 цифр подряд. export const SNILS_PATTERN = /^(\d{3}-\d{3}-\d{3} \d{2}|\d{11})$/; -// Регистрационный номер СФР: XXX-XXX-XXXXXX (3-3-6). -export const SFR_REG_NUMBER_PATTERN = /^\d{3}-\d{3}-\d{6}$/; +// Регистрационный номер СФР: действующий XXX-XXX-XXXXXX (3-3-6, 12 цифр) +// или прежний — 10 цифр без разделителей (ТипРегНомерОбщ в XSD ЕФС-1). +export const SFR_REG_NUMBER_PATTERN = /^(\d{3}-\d{3}-\d{6}|\d{10})$/; /** Дата в формате `DD.MM.YYYY` (XSD `xs:date`-совместимый ФНС-формат). */ export const DATE_DDMMYYYY_PATTERN = /^\d{2}\.\d{2}\.\d{4}$/; diff --git a/components/desktop/extensions/reports/pages/SettingsPage/ui/RequisiteField.vue b/components/desktop/extensions/reports/pages/SettingsPage/ui/RequisiteField.vue index 897ff6c74b2..4d8782fc967 100644 --- a/components/desktop/extensions/reports/pages/SettingsPage/ui/RequisiteField.vue +++ b/components/desktop/extensions/reports/pages/SettingsPage/ui/RequisiteField.vue @@ -36,9 +36,10 @@ const props = defineProps<{ mask?: string /** Подставлять плейсхолдеры маски при вводе */ fillMask?: boolean - /** Разрешить только цифры и точку (ОКВЭД: 94.99, 46.73.7). Блокирует - * keypress/paste — буквы вообще не появляются в поле. */ - digitsDotsOnly?: boolean + /** Разрешить только цифры + перечисленные доп. символы (напр. '.' для + * ОКВЭД: 94.99, 46.73.7; '-' для Рег. номера СФР). Блокирует + * keypress/paste — посторонние символы вообще не появляются в поле. */ + digitsExtraChars?: string /** HTML maxlength. */ maxLength?: number /** Допустимые точные длины (ИЛИ). Напр. `[8, 11]` для ОКТМО. */ @@ -60,12 +61,20 @@ const labelText = computed(() => ) const inputModeComputed = computed<'numeric' | 'decimal' | 'text'>(() => { - if (props.digitsDotsOnly) return 'decimal' + // '.' — десятичная клавиатура мобильных устройств её показывает; для + // прочих доп.символов (напр. '-') нужна полная клавиатура — 'text'. + if (props.digitsExtraChars === '.') return 'decimal' + if (props.digitsExtraChars) return 'text' // mask из одних `#` = чистая цифровая клавиатура на мобиле. if (props.mask && /^#+$/.test(props.mask)) return 'numeric' return 'text' }) +/** Экранирует спецсимволы regex, чтобы вставить строку в символьный класс `[...]`. */ +function escapeForCharClass(chars: string): string { + return chars.replace(/[\]\\^-]/g, '\\$&') +} + const NAV_KEYS = new Set([ 'Backspace', 'Delete', @@ -81,19 +90,21 @@ const NAV_KEYS = new Set([ ]) // Блокируем keypress не-допустимых символов — буквы не попадают в поле даже -// на миллисекунды. Работает только для digitsDotsOnly (для чисто цифровых +// на миллисекунды. Работает только для digitsExtraChars (для чисто цифровых // полей используем Quasar-mask — он нативно блокирует не-# символы). function onKeydown(e: KeyboardEvent): void { - if (!props.digitsDotsOnly) return + if (!props.digitsExtraChars) return if (e.ctrlKey || e.metaKey || e.altKey) return if (NAV_KEYS.has(e.key)) return - if (!/^[\d.]$/.test(e.key)) e.preventDefault() + const re = new RegExp(`^[\\d${escapeForCharClass(props.digitsExtraChars)}]$`) + if (!re.test(e.key)) e.preventDefault() } function onPaste(e: ClipboardEvent): void { - if (!props.digitsDotsOnly) return + if (!props.digitsExtraChars) return const text = e.clipboardData?.getData('text') ?? '' - if (!/^[\d.]*$/.test(text)) e.preventDefault() + const re = new RegExp(`^[\\d${escapeForCharClass(props.digitsExtraChars)}]*$`) + if (!re.test(text)) e.preventDefault() } const rules = computed(() => { @@ -128,7 +139,10 @@ function onInput(raw: string | number | null): void { // Фоллбек-фильтр на случай programmatic ввода (drop/autofill). Для ручного // набора актуальна либо mask (q-input сам режет), либо keydown-блокер. let s = String(raw ?? '') - if (props.digitsDotsOnly) s = s.replace(/[^\d.]+/g, '') + if (props.digitsExtraChars) { + const re = new RegExp(`[^\\d${escapeForCharClass(props.digitsExtraChars)}]+`, 'g') + s = s.replace(re, '') + } if (props.maxLength && s.length > props.maxLength) s = s.slice(0, props.maxLength) emit('update:value', s) } diff --git a/components/desktop/extensions/reports/pages/SettingsPage/ui/SettingsPage.vue b/components/desktop/extensions/reports/pages/SettingsPage/ui/SettingsPage.vue index 84b47d52efc..94afb49f5d9 100644 --- a/components/desktop/extensions/reports/pages/SettingsPage/ui/SettingsPage.vue +++ b/components/desktop/extensions/reports/pages/SettingsPage/ui/SettingsPage.vue @@ -37,7 +37,7 @@ q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError :value='manualInput[f.key]' :placeholder='f.placeholder' :mask='f.mask' - :digits-dots-only='f.digitsDotsOnly' + :digits-extra-chars='f.digitsExtraChars' :max-length='f.maxLength' :exact-lengths='f.exactLengths' :pattern='f.pattern' @@ -60,10 +60,11 @@ q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError id='field-sfrRegNumber' label='Рег. номер СФР' :value='manualInput.sfrRegNumber' - placeholder='XXX-XXX-XXXXXX' - mask='###-###-######' + placeholder='XXX-XXX-XXXXXX или 10 цифр' + digits-extra-chars='-' + :max-length='14' :pattern='SFR_REG_PATTERN' - pattern-message='Формат: XXX-XXX-XXXXXX (12 цифр)' + pattern-message='Формат: XXX-XXX-XXXXXX (12 цифр) или 10 цифр без разделителей' required @update:value='v => (manualInput.sfrRegNumber = v)' ) @@ -202,7 +203,7 @@ interface ClassifierField { label: string placeholder?: string mask?: string - digitsDotsOnly?: boolean + digitsExtraChars?: string maxLength?: number exactLengths?: number[] pattern?: RegExp @@ -212,13 +213,13 @@ interface ClassifierField { // Placeholder'ы — правило ввода (кол-во цифр), а не конкретные примеры. // Для цифровых полей фиксированной/короткой длины — используем Quasar-mask // (`#` = только цифра, буквы блокируются на keypress нативно). -// Для ОКВЭД (цифры + точка переменной длины) — digitsDotsOnly с @keydown-блокером. +// Для ОКВЭД (цифры + точка переменной длины) — digitsExtraChars='.' с @keydown-блокером. const classifierFields: ClassifierField[] = [ { key: 'okved', label: 'ОКВЭД', placeholder: 'XX.XX или XX.XX.XX', - digitsDotsOnly: true, + digitsExtraChars: '.', maxLength: 8, pattern: /^\d{2}(\.\d{1,2}){0,2}$/, patternMessage: 'Формат: XX.XX или XX.XX.XX', @@ -265,7 +266,7 @@ const signerTypeOptions = [ // пользователь прожал пару цифр и нажал «Сохранить»: mask не мешает // сохранить частичный ввод, паттерн-правило блокирует. const SNILS_PATTERN = /^\d{3}-\d{3}-\d{3} \d{2}$/ -const SFR_REG_PATTERN = /^\d{3}-\d{3}-\d{6}$/ +const SFR_REG_PATTERN = /^(\d{3}-\d{3}-\d{6}|\d{10})$/ function getValue(key: keyof IReportRequisitesView): string { const v = requisites.value?.[key] as any diff --git a/components/desktop/extensions/reports/widgets/report-forms/ZeroReportEditor.vue b/components/desktop/extensions/reports/widgets/report-forms/ZeroReportEditor.vue index 3b8e6d95271..71f782efe2b 100644 --- a/components/desktop/extensions/reports/widgets/report-forms/ZeroReportEditor.vue +++ b/components/desktop/extensions/reports/widgets/report-forms/ZeroReportEditor.vue @@ -162,8 +162,8 @@ :rules='[reportRules.sfrRegNumber()]' :error='errFor("signer.sfrRegNumber")' :error-message='msgFor("signer.sfrRegNumber")' - mask='###-###-######' - hint='XXX-XXX-XXXXXX — обязателен для ЕФС-1' + maxlength='14' + hint='XXX-XXX-XXXXXX или 10 цифр — обязателен для ЕФС-1' dense filled ) diff --git a/components/sdk/src/selectors/reports/patterns.ts b/components/sdk/src/selectors/reports/patterns.ts index 2bbd63ee1ef..1f97fd6d919 100644 --- a/components/sdk/src/selectors/reports/patterns.ts +++ b/components/sdk/src/selectors/reports/patterns.ts @@ -41,8 +41,8 @@ export const OKPO_PATTERN = /^\d{8}(\d{2})?$/; /** СНИЛС: XXX-XXX-XXX YY или 11 цифр подряд. */ export const SNILS_PATTERN = /^(\d{3}-\d{3}-\d{3} \d{2}|\d{11})$/; -/** Регистрационный номер СФР: XXX-XXX-XXXXXX (3-3-6). */ -export const SFR_REG_NUMBER_PATTERN = /^\d{3}-\d{3}-\d{6}$/; +/** Регистрационный номер СФР: действующий XXX-XXX-XXXXXX (3-3-6, 12 цифр) или прежний — 10 цифр без разделителей. */ +export const SFR_REG_NUMBER_PATTERN = /^(\d{3}-\d{3}-\d{6}|\d{10})$/; /** Дата в формате DD.MM.YYYY (ФНС). */ export const DATE_DDMMYYYY_PATTERN = /^\d{2}\.\d{2}\.\d{4}$/; @@ -70,7 +70,7 @@ export const reportRules = { okopf: (message = 'ОКОПФ — 5 цифр') => (v: unknown) => OKOPF_PATTERN.test(String(v ?? '')) || message, okpo: (message = 'ОКПО — 8 или 10 цифр') => (v: unknown) => OKPO_PATTERN.test(String(v ?? '')) || message, snils: (message = 'СНИЛС — XXX-XXX-XXX YY или 11 цифр') => (v: unknown) => SNILS_PATTERN.test(String(v ?? '')) || message, - sfrRegNumber: (message = 'Рег. номер СФР — XXX-XXX-XXXXXX') => (v: unknown) => SFR_REG_NUMBER_PATTERN.test(String(v ?? '')) || message, + sfrRegNumber: (message = 'Рег. номер СФР — XXX-XXX-XXXXXX или 10 цифр') => (v: unknown) => SFR_REG_NUMBER_PATTERN.test(String(v ?? '')) || message, dateDdMmYyyy: (message = 'Дата — DD.MM.YYYY') => (v: unknown) => DATE_DDMMYYYY_PATTERN.test(String(v ?? '')) || message, kbk: (message = 'КБК — 20 цифр') => (v: unknown) => KBK_PATTERN.test(String(v ?? '')) || message, /** Непустая строка длиной от `min` до `max` символов. */