309 lines
14 KiB
TypeScript
309 lines
14 KiB
TypeScript
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;
|
||
}
|