- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
209 lines
5.7 KiB
TypeScript
209 lines
5.7 KiB
TypeScript
import { format } from 'date-fns';
|
||
import { ru } from 'date-fns/locale';
|
||
import { incline, inclineFirstname, inclineLastname, inclineMiddlename } from 'lvovich';
|
||
import numberToWordsRuPkg from 'number-to-words-ru';
|
||
import { ToWords } from 'to-words';
|
||
|
||
const convertRu = (numberToWordsRuPkg as any).convert || numberToWordsRuPkg;
|
||
|
||
export interface TransformConfig {
|
||
type: string;
|
||
params?: Record<string, any>;
|
||
}
|
||
|
||
export type TransformType =
|
||
| 'none'
|
||
| 'date_long'
|
||
| 'date_short'
|
||
| 'currency_text'
|
||
| 'currency_number'
|
||
| 'uppercase'
|
||
| 'lowercase'
|
||
| 'initials'
|
||
| 'declension'
|
||
| 'name_part'
|
||
| 'number_to_words';
|
||
|
||
const toWordsEn = new ToWords({ localeCode: 'en-US' });
|
||
|
||
export class TransformService {
|
||
applyTransform(
|
||
value: string | null | undefined,
|
||
transform?: TransformConfig | TransformType | string | null,
|
||
): string {
|
||
if (value === null || value === undefined || value === '') return '';
|
||
|
||
const config = this.normalizeTransform(transform);
|
||
if (!config || config.type === 'none') return String(value);
|
||
|
||
switch (config.type) {
|
||
case 'date_long':
|
||
return this.toDateLong(value);
|
||
case 'date_short':
|
||
return this.toDateShort(value);
|
||
case 'currency_text':
|
||
return this.toCurrencyText(value, config.params);
|
||
case 'currency_number':
|
||
return this.toCurrencyNumber(value);
|
||
case 'uppercase':
|
||
return String(value).toUpperCase();
|
||
case 'lowercase':
|
||
return String(value).toLowerCase();
|
||
case 'initials':
|
||
return this.toInitials(value);
|
||
case 'declension':
|
||
return this.toDeclension(value, config.params);
|
||
case 'name_part':
|
||
case 'namePart':
|
||
return this.toNamePart(value, config.params);
|
||
case 'number_to_words':
|
||
case 'numberToWords':
|
||
return this.toNumberToWords(value, config.params);
|
||
default:
|
||
return String(value);
|
||
}
|
||
}
|
||
|
||
private normalizeTransform(
|
||
transform?: TransformConfig | TransformType | string | null,
|
||
): TransformConfig | null {
|
||
if (!transform) return null;
|
||
if (typeof transform === 'string') {
|
||
return { type: transform };
|
||
}
|
||
return transform;
|
||
}
|
||
|
||
private toDateLong(value: string): string {
|
||
try {
|
||
const date = new Date(value);
|
||
if (isNaN(date.getTime())) return value;
|
||
return format(date, '«dd» MMMM yyyy\'г.\'', { locale: ru });
|
||
} catch {
|
||
return value;
|
||
}
|
||
}
|
||
|
||
private toDateShort(value: string): string {
|
||
try {
|
||
const date = new Date(value);
|
||
if (isNaN(date.getTime())) return value;
|
||
return format(date, 'dd.MM.yyyy');
|
||
} catch {
|
||
return value;
|
||
}
|
||
}
|
||
|
||
private parseNumber(value: string): number | null {
|
||
const cleaned = String(value)
|
||
.replace(/\s/g, '')
|
||
.replace(',', '.');
|
||
const num = parseFloat(cleaned);
|
||
return Number.isFinite(num) ? num : null;
|
||
}
|
||
|
||
private toCurrencyText(value: string, params?: Record<string, any>): string {
|
||
const num = this.parseNumber(value);
|
||
if (num === null) return value;
|
||
const currency = params?.currency || 'rub';
|
||
try {
|
||
return convertRu(String(num), { currency });
|
||
} catch {
|
||
return value;
|
||
}
|
||
}
|
||
|
||
private toCurrencyNumber(value: string): string {
|
||
const num = this.parseNumber(value);
|
||
if (num === null) return value;
|
||
return num.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
}
|
||
|
||
private toInitials(value: string): string {
|
||
const parts = String(value).trim().split(/\s+/);
|
||
if (parts.length >= 3) {
|
||
const [last, first, middle] = parts;
|
||
return `${last} ${first[0]}.${middle[0]}.`;
|
||
}
|
||
if (parts.length === 2) {
|
||
const [last, first] = parts;
|
||
return `${last} ${first[0]}.`;
|
||
}
|
||
return value;
|
||
}
|
||
|
||
private toDeclension(value: string, params?: Record<string, any>): string {
|
||
const raw = String(value).trim();
|
||
if (!raw) return value;
|
||
|
||
const parts = raw.split(/\s+/);
|
||
const declension = params?.case || 'dative';
|
||
|
||
if (parts.length >= 3) {
|
||
const [last, first, middle] = parts;
|
||
const result = incline({ last, first, middle }, declension);
|
||
return [result.last, result.first, result.middle].filter(Boolean).join(' ');
|
||
}
|
||
if (parts.length === 2) {
|
||
const [last, first] = parts;
|
||
const result = incline({ last, first }, declension);
|
||
return [result.last, result.first].filter(Boolean).join(' ');
|
||
}
|
||
if (parts.length === 1) {
|
||
const [last] = parts;
|
||
return inclineLastname(last, declension) || value;
|
||
}
|
||
return value;
|
||
}
|
||
|
||
private toNamePart(value: string, params?: Record<string, any>): string {
|
||
const raw = String(value).trim();
|
||
if (!raw) return value;
|
||
|
||
const parts = raw.split(/\s+/);
|
||
const part = params?.part || 'firstNameWithMiddleName';
|
||
|
||
switch (part) {
|
||
case 'lastName':
|
||
return parts[0] || '';
|
||
case 'firstName':
|
||
return parts[1] || '';
|
||
case 'middleName':
|
||
return parts[2] || '';
|
||
case 'firstNameWithMiddleName':
|
||
return parts.slice(1, 3).join(' ');
|
||
case 'shortName':
|
||
return this.toInitials(value);
|
||
case 'fullName':
|
||
default:
|
||
return value;
|
||
}
|
||
}
|
||
|
||
private toNumberToWords(value: string, params?: Record<string, any>): string {
|
||
const num = this.parseNumber(value);
|
||
if (num === null) return value;
|
||
const currency = params?.currency || 'rub';
|
||
try {
|
||
return convertRu(String(num), { currency });
|
||
} catch {
|
||
return value;
|
||
}
|
||
}
|
||
|
||
numberToWords(amount: number, locale: string = 'ru'): string {
|
||
if (locale === 'ru') {
|
||
try {
|
||
return convertRu(String(amount), { currency: 'rub' });
|
||
} catch {
|
||
return String(amount);
|
||
}
|
||
}
|
||
try {
|
||
return toWordsEn.convert(amount);
|
||
} catch {
|
||
return String(amount);
|
||
}
|
||
}
|
||
}
|