Files
IdP/apps/frontend/lib/document-catalog.ts
2026-07-01 23:27:48 +03:00

334 lines
14 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 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 type DocumentAttachmentMeta = {
fileName?: string;
contentType?: string;
};
export function parseAttachmentMeta(metadata: Record<string, unknown>): Record<string, DocumentAttachmentMeta> {
const raw = metadata.attachmentMeta;
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
return {};
}
const result: Record<string, DocumentAttachmentMeta> = {};
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
const entry = value as Record<string, unknown>;
result[key] = {
fileName: typeof entry.fileName === 'string' ? entry.fileName : undefined,
contentType: typeof entry.contentType === 'string' ? entry.contentType : undefined
};
}
return result;
}
export function withDocumentPhotos(metadata: Record<string, unknown>, photoStorageKeys: string[], attachmentMeta?: Record<string, DocumentAttachmentMeta>) {
const next: Record<string, unknown> = { ...metadata, photoStorageKeys };
delete next.photoStorageKey;
if (attachmentMeta && Object.keys(attachmentMeta).length > 0) {
next.attachmentMeta = attachmentMeta;
}
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;
}