first commit

This commit is contained in:
lendry
2026-06-24 14:37:15 +03:00
commit 995adeedd4
188 changed files with 28810 additions and 0 deletions

View File

@@ -0,0 +1,308 @@
import type { LucideIcon } from 'lucide-react';
import {
Baby,
Building2,
Car,
CreditCard,
FileText,
HeartPulse,
Plane,
Stethoscope,
Wallet
} from 'lucide-react';
export type DocumentTypeCode =
| 'PASSPORT_RF'
| 'FOREIGN_PASSPORT'
| 'BIRTH_CERTIFICATE'
| 'DRIVER_LICENSE'
| 'VEHICLE_REGISTRATION'
| 'OMS'
| 'DMS'
| 'INN'
| 'SNILS';
export type FieldKind = 'text' | 'date' | 'gender' | 'checkbox' | 'textarea' | 'categories';
export interface DocumentFieldDef {
key: string;
label: string;
kind: FieldKind;
placeholder?: string;
latinKey?: string;
latinLabel?: string;
}
export interface DocumentTypeDef {
type: DocumentTypeCode;
label: string;
shortLabel: string;
category: 'personal' | 'transport' | 'health' | 'finance';
icon: LucideIcon;
accent: string;
numberKey: string;
fields: DocumentFieldDef[];
}
export const DOCUMENT_CATEGORIES = [
{ id: 'personal' as const, title: 'Личные документы' },
{ id: 'transport' as const, title: 'Транспорт' },
{ id: 'health' as const, title: 'Здоровье' },
{ id: 'finance' as const, title: 'Финансы' }
];
const baseNameFields: DocumentFieldDef[] = [
{ key: 'lastName', label: 'Фамилия', kind: 'text', placeholder: 'Фамилия' },
{ key: 'firstName', label: 'Имя', kind: 'text', placeholder: 'Имя' },
{ key: 'middleName', label: 'Отчество', kind: 'text', placeholder: 'Отчество' },
{ key: 'noMiddleName', label: 'Нет отчества', kind: 'checkbox' }
];
const latinNameFields: DocumentFieldDef[] = [
{ key: 'lastName', label: 'Фамилия', kind: 'text', placeholder: 'Фамилия', latinKey: 'lastNameLatin', latinLabel: 'Фамилия латинскими буквами' },
{ key: 'firstName', label: 'Имя', kind: 'text', placeholder: 'Имя', latinKey: 'firstNameLatin', latinLabel: 'Имя латинскими буквами' },
{ key: 'middleName', label: 'Отчество', kind: 'text', placeholder: 'Отчество', latinKey: 'middleNameLatin', latinLabel: 'Отчество латинскими буквами' },
{ key: 'noMiddleName', label: 'Нет отчества', kind: 'checkbox' }
];
export const DOCUMENT_TYPES: DocumentTypeDef[] = [
{
type: 'PASSPORT_RF',
label: 'Паспорт РФ',
shortLabel: 'Паспорт РФ',
category: 'personal',
icon: FileText,
accent: 'text-red-500 bg-red-50',
numberKey: 'seriesNumber',
fields: [
{ key: 'seriesNumber', label: 'Серия и номер', kind: 'text', placeholder: '0000 000000' },
...baseNameFields,
{ key: 'birthDate', label: 'Дата рождения', kind: 'date' },
{ key: 'birthPlace', label: 'Место рождения', kind: 'text', placeholder: 'Место рождения' },
{ key: 'gender', label: 'Пол', kind: 'gender' },
{ key: 'issuedBy', label: 'Кем выдан', kind: 'text', placeholder: 'Кем выдан' },
{ key: 'departmentCode', label: 'Код подразделения', kind: 'text', placeholder: '000-000' },
{ key: 'issuedAt', label: 'Дата выдачи', kind: 'date' },
{ key: 'registrationAddress', label: 'Адрес регистрации', kind: 'text', placeholder: 'Адрес регистрации' }
]
},
{
type: 'FOREIGN_PASSPORT',
label: 'Загран',
shortLabel: 'Загран',
category: 'personal',
icon: Plane,
accent: 'text-red-500 bg-red-50',
numberKey: 'number',
fields: [
{ key: 'number', label: 'Номер', kind: 'text', placeholder: 'Номер' },
...latinNameFields,
{ key: 'citizenship', label: 'Гражданство', kind: 'text', placeholder: 'Гражданство', latinKey: 'citizenshipLatin', latinLabel: 'Гражданство латинскими буквами' },
{ key: 'birthDate', label: 'Дата рождения', kind: 'date' },
{ key: 'birthPlace', label: 'Место рождения', kind: 'text', placeholder: 'Место рождения', latinKey: 'birthPlaceLatin', latinLabel: 'Место рождения латинскими буквами' },
{ key: 'gender', label: 'Пол', kind: 'gender' },
{ key: 'issuedAt', label: 'Дата выдачи', kind: 'date' },
{ key: 'expiresAt', label: 'Дата окончания срока действия', kind: 'date' }
]
},
{
type: 'BIRTH_CERTIFICATE',
label: 'Свидетельство о рождении',
shortLabel: 'Св-во о рождении',
category: 'personal',
icon: Baby,
accent: 'text-red-500 bg-red-50',
numberKey: 'seriesNumber',
fields: [
{ key: 'series', label: 'Серия', kind: 'text', placeholder: 'Серия' },
{ key: 'number', label: 'Номер', kind: 'text', placeholder: 'Номер' },
...baseNameFields,
{ key: 'birthDate', label: 'Дата рождения', kind: 'date' },
{ key: 'birthPlace', label: 'Место рождения', kind: 'text' },
{ key: 'recordNumber', label: 'Номер записи акта о рождении', kind: 'text' },
{ key: 'fatherLastName', label: 'Фамилия отца', kind: 'text', placeholder: 'Фамилия' },
{ key: 'fatherFirstName', label: 'Имя отца', kind: 'text', placeholder: 'Имя' },
{ key: 'fatherMiddleName', label: 'Отчество отца', kind: 'text', placeholder: 'Отчество' },
{ key: 'fatherNoMiddleName', label: 'Нет отчества (отец)', kind: 'checkbox' },
{ key: 'fatherCitizenship', label: 'Гражданство отца', kind: 'text' },
{ key: 'fatherBirthDate', label: 'Дата рождения отца', kind: 'date' }
]
},
{
type: 'DRIVER_LICENSE',
label: 'ВУ',
shortLabel: 'ВУ',
category: 'transport',
icon: CreditCard,
accent: 'text-violet-500 bg-violet-50',
numberKey: 'number',
fields: [
{ key: 'number', label: 'Номер', kind: 'text' },
...latinNameFields,
{ key: 'birthDate', label: 'Дата рождения', kind: 'date' },
{ key: 'birthPlace', label: 'Место рождения', kind: 'text', latinKey: 'birthPlaceLatin', latinLabel: 'Место рождения латинскими буквами' },
{ key: 'issuedAt', label: 'Дата выдачи', kind: 'date' },
{ key: 'expiresAt', label: 'Действует до', kind: 'date' },
{ key: 'issuedBy', label: 'Кем выдано', kind: 'text', latinKey: 'issuedByLatin', latinLabel: 'Кем выдано латинскими буквами' },
{ key: 'issuePlace', label: 'Место выдачи', kind: 'text', latinKey: 'issuePlaceLatin', latinLabel: 'Место выдачи латинскими буквами' },
{ key: 'categories', label: 'Категории', kind: 'categories' },
{ key: 'specialMarks', label: 'Особые отметки', kind: 'textarea' }
]
},
{
type: 'VEHICLE_REGISTRATION',
label: 'СТС / СРТС',
shortLabel: 'СТС / СРТС',
category: 'transport',
icon: Car,
accent: 'text-violet-500 bg-violet-50',
numberKey: 'seriesNumber',
fields: [
{ key: 'seriesNumber', label: 'Серия и номер', kind: 'text' },
{ key: 'plateNumber', label: 'Регистрационный знак', kind: 'text', placeholder: 'X000XX00' },
{ key: 'vin', label: 'Идентификационный номер (VIN)', kind: 'text' },
{ key: 'makeModel', label: 'Марка, модель', kind: 'text', latinKey: 'makeModelLatin', latinLabel: 'Марка, модель латинскими буквами' },
{ key: 'vehicleType', label: 'Тип ТС', kind: 'text' },
{ key: 'vehicleCategory', label: 'Категория ТС', kind: 'text' },
{ key: 'year', label: 'Год выпуска', kind: 'text' },
{ key: 'chassisNumber', label: 'Номер шасси (рамы)', kind: 'text' },
{ key: 'bodyNumber', label: 'Номер кузова (кабины, прицепа)', kind: 'text' },
{ key: 'color', label: 'Цвет', kind: 'text' },
{ key: 'enginePower', label: 'Мощность двигателя, кВт/л.с.', kind: 'text' },
{ key: 'ecoClass', label: 'Экологический класс', kind: 'text' },
{ key: 'maxMass', label: 'Разрешенная максимальная масса, кг', kind: 'text' },
{ key: 'curbWeight', label: 'Масса в снаряженном состоянии, кг', kind: 'text' },
{ key: 'tempRegistration', label: 'Срок временной регистрации', kind: 'text' },
{ key: 'pts', label: 'ПТС', kind: 'text', placeholder: '77TX000006 или 123456789012345' },
...latinNameFields.slice(0, 3),
{ key: 'registrationAddress', label: 'Адрес регистрации', kind: 'text' },
{ key: 'departmentCode', label: 'Код подразделения (выдано ГИБДД)', kind: 'text' },
{ key: 'issuedAt', label: 'Дата выдачи', kind: 'date' },
{ key: 'specialMarks', label: 'Особые отметки', kind: 'textarea' }
]
},
{
type: 'OMS',
label: 'ОМС',
shortLabel: 'ОМС',
category: 'health',
icon: Stethoscope,
accent: 'text-sky-500 bg-sky-50',
numberKey: 'number',
fields: [
{ key: 'number', label: 'Номер', kind: 'text' },
...baseNameFields,
{ key: 'birthDate', label: 'Дата рождения', kind: 'date' },
{ key: 'gender', label: 'Пол', kind: 'gender' },
{ key: 'blankSeriesNumber', label: 'Серия и номер бланка', kind: 'text' }
]
},
{
type: 'DMS',
label: 'ДМС',
shortLabel: 'ДМС',
category: 'health',
icon: HeartPulse,
accent: 'text-sky-500 bg-sky-50',
numberKey: 'number',
fields: [
{ key: 'number', label: 'Номер', kind: 'text' },
...baseNameFields,
{ key: 'birthDate', label: 'Дата рождения', kind: 'date' },
{ key: 'gender', label: 'Пол', kind: 'gender' },
{ key: 'validFrom', label: 'Действует с', kind: 'date' },
{ key: 'validTo', label: 'Действует до', kind: 'date' },
{ key: 'insurer', label: 'Страхователь', kind: 'text' }
]
},
{
type: 'INN',
label: 'ИНН',
shortLabel: 'ИНН',
category: 'finance',
icon: Building2,
accent: 'text-emerald-500 bg-emerald-50',
numberKey: 'number',
fields: [
{ key: 'number', label: 'ИНН', kind: 'text' },
...baseNameFields,
{ key: 'birthDate', label: 'Дата рождения', kind: 'date' },
{ key: 'birthPlace', label: 'Место рождения', kind: 'text' },
{ key: 'gender', label: 'Пол', kind: 'gender' },
{ key: 'issuedBy', label: 'Орган, выдавший документ', kind: 'text' },
{ key: 'issuedAt', label: 'Дата выдачи', kind: 'date' }
]
},
{
type: 'SNILS',
label: 'СНИЛС',
shortLabel: 'СНИЛС',
category: 'finance',
icon: Wallet,
accent: 'text-emerald-500 bg-emerald-50',
numberKey: 'number',
fields: [
{ key: 'number', label: 'СНИЛС', kind: 'text' },
...baseNameFields,
{ key: 'birthDate', label: 'Дата рождения', kind: 'date' },
{ key: 'birthPlace', label: 'Место рождения', kind: 'text' },
{ key: 'gender', label: 'Пол', kind: 'gender' },
{ key: 'registeredAt', label: 'Дата регистрации', kind: 'date' }
]
}
];
export const QUICK_DOCUMENT_TYPES = ['PASSPORT_RF', 'DRIVER_LICENSE', 'FOREIGN_PASSPORT'] as const;
export const LICENSE_CATEGORIES = ['A', 'B', 'C', 'D', 'A1', 'B1', 'C1', 'D1', 'M', 'BE', 'CE', 'DE', 'Tm', 'Tb', 'C1E', 'D1E'];
export function getDocumentType(type: string) {
return DOCUMENT_TYPES.find((item) => item.type === type);
}
export function buildDocumentNumber(type: DocumentTypeDef, values: Record<string, string>) {
if (type.type === 'BIRTH_CERTIFICATE') {
return [values.series, values.number].filter(Boolean).join(' ');
}
if (type.type === 'PASSPORT_RF' || type.type === 'VEHICLE_REGISTRATION') {
return values.seriesNumber || values.number || '—';
}
return values[type.numberKey] || '—';
}
export function parseMetadata(metadataJson?: string) {
if (!metadataJson) return {} as Record<string, unknown>;
try {
return JSON.parse(metadataJson) as Record<string, unknown>;
} catch {
return {} as Record<string, unknown>;
}
}
export function parseDocumentPhotos(metadata: Record<string, unknown>): string[] {
const keys = metadata.photoStorageKeys;
if (Array.isArray(keys)) {
return keys.filter((item): item is string => typeof item === 'string' && item.length > 0);
}
if (typeof metadata.photoStorageKey === 'string' && metadata.photoStorageKey) {
return [metadata.photoStorageKey];
}
return [];
}
export function withDocumentPhotos(metadata: Record<string, unknown>, photoStorageKeys: string[]) {
const next: Record<string, unknown> = { ...metadata, photoStorageKeys };
delete next.photoStorageKey;
return next;
}
/** Берёт самый свежий документ каждого типа (список с бэкенда отсортирован по updatedAt desc). */
export function indexDocumentsByType<T extends { type: string }>(documents: T[]): Map<string, T> {
const map = new Map<string, T>();
for (const document of documents) {
if (!map.has(document.type)) {
map.set(document.type, document);
}
}
return map;
}