271 lines
14 KiB
TypeScript
271 lines
14 KiB
TypeScript
'use client';
|
||
|
||
import { format } from 'date-fns';
|
||
import { ru } from 'date-fns/locale';
|
||
import {
|
||
buildDocumentNumber,
|
||
DOCUMENT_TYPES,
|
||
getDocumentType,
|
||
LICENSE_CATEGORIES,
|
||
parseMetadata,
|
||
type DocumentTypeCode,
|
||
type DocumentTypeDef
|
||
} from '@/lib/document-catalog';
|
||
import { UserDocument } from '@/lib/api';
|
||
import { cn } from '@/lib/utils';
|
||
|
||
function formatDateValue(value?: string) {
|
||
if (!value) return '';
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return value;
|
||
return format(date, 'd MMMM yyyy', { locale: ru });
|
||
}
|
||
|
||
function formatFieldValue(fieldKind: string, value: string) {
|
||
if (!value) return '—';
|
||
if (fieldKind === 'date') return formatDateValue(value);
|
||
if (fieldKind === 'gender') return value === 'male' ? 'Мужской' : value === 'female' ? 'Женский' : value;
|
||
if (fieldKind === 'checkbox') return value === 'true' ? 'Да' : '—';
|
||
if (fieldKind === 'categories') {
|
||
return value
|
||
.split(',')
|
||
.map((item) => item.trim())
|
||
.filter(Boolean)
|
||
.join(' · ');
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function fullName(values: Record<string, string>) {
|
||
const parts = [values.lastName, values.firstName, values.middleName].filter(Boolean);
|
||
return parts.length ? parts.join(' ') : '—';
|
||
}
|
||
|
||
function latinFullName(values: Record<string, string>) {
|
||
const parts = [values.lastNameLatin, values.firstNameLatin, values.middleNameLatin].filter(Boolean);
|
||
return parts.length ? parts.join(' ') : '';
|
||
}
|
||
|
||
function DocumentField({ label, value, className }: { label: string; value: string; className?: string }) {
|
||
if (!value || value === '—') return null;
|
||
return (
|
||
<div className={className}>
|
||
<p className="text-[10px] uppercase tracking-[0.12em] opacity-70">{label}</p>
|
||
<p className="mt-0.5 text-sm font-medium leading-snug">{value}</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function PassportRfCard({ config, values, document }: { config: DocumentTypeDef; values: Record<string, string>; document: UserDocument }) {
|
||
return (
|
||
<div className="overflow-hidden rounded-[24px] border border-[#6b1d24]/20 bg-[linear-gradient(145deg,#7a2430_0%,#5c1820_55%,#3d1016_100%)] p-5 text-[#fff6eb] shadow-[0_24px_60px_rgba(92,24,32,0.35)]">
|
||
<div className="flex items-start justify-between gap-4">
|
||
<div>
|
||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#f6d9a8]">Российская Федерация</p>
|
||
<p className="mt-1 text-lg font-semibold">{config.label}</p>
|
||
</div>
|
||
<div className="flex h-14 w-14 items-center justify-center rounded-full border border-[#f6d9a8]/40 bg-[#f6d9a8]/10 text-xs font-semibold text-[#f6d9a8]">
|
||
РФ
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-5 rounded-[18px] bg-black/15 p-4 backdrop-blur-sm">
|
||
<p className="text-[10px] uppercase tracking-[0.14em] text-[#f6d9a8]/80">Серия и номер</p>
|
||
<p className="mt-1 font-mono text-2xl font-semibold tracking-wider">{document.number || buildDocumentNumber(config, values)}</p>
|
||
</div>
|
||
|
||
<div className="mt-4 space-y-3">
|
||
<DocumentField label="ФИО" value={fullName(values)} />
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<DocumentField label="Дата рождения" value={formatFieldValue('date', values.birthDate ?? '')} />
|
||
<DocumentField label="Пол" value={formatFieldValue('gender', values.gender ?? '')} />
|
||
</div>
|
||
<DocumentField label="Место рождения" value={values.birthPlace ?? ''} />
|
||
<DocumentField label="Кем выдан" value={values.issuedBy ?? ''} />
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<DocumentField label="Дата выдачи" value={formatFieldValue('date', values.issuedAt ?? document.issuedAt?.slice(0, 10) ?? '')} />
|
||
<DocumentField label="Код подразделения" value={values.departmentCode ?? ''} />
|
||
</div>
|
||
<DocumentField label="Адрес регистрации" value={values.registrationAddress ?? ''} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ForeignPassportCard({ config, values, document }: { config: DocumentTypeDef; values: Record<string, string>; document: UserDocument }) {
|
||
const latin = latinFullName(values);
|
||
return (
|
||
<div className="overflow-hidden rounded-[24px] border border-[#5a1830]/20 bg-[linear-gradient(145deg,#6d2138_0%,#451222_100%)] p-5 text-[#fff4f0] shadow-[0_24px_60px_rgba(69,18,34,0.35)]">
|
||
<div className="flex items-start justify-between gap-4">
|
||
<div>
|
||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#ffd3bf]">Passport</p>
|
||
<p className="mt-1 text-lg font-semibold">{config.label}</p>
|
||
</div>
|
||
<div className="rounded-xl bg-white/10 px-3 py-2 text-xs font-semibold uppercase tracking-widest text-[#ffd3bf]">P</div>
|
||
</div>
|
||
<div className="mt-5 rounded-[18px] bg-black/15 p-4">
|
||
<p className="text-[10px] uppercase tracking-[0.14em] opacity-70">No.</p>
|
||
<p className="mt-1 font-mono text-2xl font-semibold tracking-wider">{document.number || values.number || '—'}</p>
|
||
</div>
|
||
<div className="mt-4 space-y-3">
|
||
<DocumentField label="ФИО" value={fullName(values)} />
|
||
{latin ? <DocumentField label="Name" value={latin} className="font-mono" /> : null}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<DocumentField label="Гражданство" value={values.citizenship ?? ''} />
|
||
<DocumentField label="Пол" value={formatFieldValue('gender', values.gender ?? '')} />
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<DocumentField label="Дата рождения" value={formatFieldValue('date', values.birthDate ?? '')} />
|
||
<DocumentField label="Дата выдачи" value={formatFieldValue('date', values.issuedAt ?? document.issuedAt?.slice(0, 10) ?? '')} />
|
||
</div>
|
||
<DocumentField label="Действует до" value={formatFieldValue('date', values.expiresAt ?? document.expiresAt?.slice(0, 10) ?? '')} />
|
||
<DocumentField label="Место рождения" value={values.birthPlace ?? ''} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function DriverLicenseCard({ config, values, document }: { config: DocumentTypeDef; values: Record<string, string>; document: UserDocument }) {
|
||
const categories = formatFieldValue('categories', values.categories ?? '');
|
||
return (
|
||
<div className="overflow-hidden rounded-[24px] border border-[#7c5cff]/20 bg-[linear-gradient(145deg,#f3ecff_0%,#ddd0ff_45%,#c8b6ff_100%)] p-5 text-[#2f2450] shadow-[0_24px_60px_rgba(124,92,255,0.18)]">
|
||
<div className="flex items-start justify-between gap-4">
|
||
<div>
|
||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6b4fd8]">Водительское удостоверение</p>
|
||
<p className="mt-1 text-lg font-semibold">{config.label}</p>
|
||
</div>
|
||
<div className="rounded-2xl bg-[#6b4fd8] px-3 py-2 text-xs font-bold uppercase tracking-widest text-white">RU</div>
|
||
</div>
|
||
<div className="mt-5 rounded-[18px] bg-white/70 p-4">
|
||
<p className="text-[10px] uppercase tracking-[0.14em] text-[#6b4fd8]">Номер</p>
|
||
<p className="mt-1 font-mono text-2xl font-semibold tracking-wider">{document.number || values.number || '—'}</p>
|
||
</div>
|
||
<div className="mt-4 space-y-3">
|
||
<DocumentField label="ФИО" value={fullName(values)} />
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<DocumentField label="Дата рождения" value={formatFieldValue('date', values.birthDate ?? '')} />
|
||
<DocumentField label="Дата выдачи" value={formatFieldValue('date', values.issuedAt ?? document.issuedAt?.slice(0, 10) ?? '')} />
|
||
</div>
|
||
<DocumentField label="Действует до" value={formatFieldValue('date', values.expiresAt ?? document.expiresAt?.slice(0, 10) ?? '')} />
|
||
<DocumentField label="Категории" value={categories} />
|
||
{values.specialMarks ? <DocumentField label="Особые отметки" value={values.specialMarks} /> : null}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function VehicleRegistrationCard({ config, values, document }: { config: DocumentTypeDef; values: Record<string, string>; document: UserDocument }) {
|
||
return (
|
||
<div className="overflow-hidden rounded-[24px] border border-[#3b82f6]/15 bg-[linear-gradient(145deg,#eff6ff_0%,#dbeafe_100%)] p-5 text-[#1e3a5f] shadow-[0_24px_60px_rgba(59,130,246,0.12)]">
|
||
<div className="flex items-start justify-between gap-4">
|
||
<div>
|
||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#2563eb]">Свидетельство о регистрации</p>
|
||
<p className="mt-1 text-lg font-semibold">{config.label}</p>
|
||
</div>
|
||
<div className="rounded-xl bg-[#2563eb] px-3 py-2 text-xs font-bold uppercase tracking-widest text-white">СТС</div>
|
||
</div>
|
||
<div className="mt-5 grid grid-cols-2 gap-3">
|
||
<div className="rounded-[18px] bg-white/80 p-4">
|
||
<p className="text-[10px] uppercase tracking-[0.14em] text-[#2563eb]">Серия и номер</p>
|
||
<p className="mt-1 font-mono text-xl font-semibold">{document.number || values.seriesNumber || '—'}</p>
|
||
</div>
|
||
<div className="rounded-[18px] bg-white/80 p-4">
|
||
<p className="text-[10px] uppercase tracking-[0.14em] text-[#2563eb]">Госномер</p>
|
||
<p className="mt-1 font-mono text-xl font-semibold uppercase">{values.plateNumber || '—'}</p>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4 space-y-3">
|
||
<DocumentField label="Марка, модель" value={values.makeModel ?? ''} />
|
||
<DocumentField label="VIN" value={values.vin ?? ''} className="font-mono" />
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<DocumentField label="Год выпуска" value={values.year ?? ''} />
|
||
<DocumentField label="Цвет" value={values.color ?? ''} />
|
||
</div>
|
||
<DocumentField label="Владелец" value={fullName(values)} />
|
||
<DocumentField label="Дата выдачи" value={formatFieldValue('date', values.issuedAt ?? document.issuedAt?.slice(0, 10) ?? '')} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function GenericDocumentCard({ config, values, document }: { config: DocumentTypeDef; values: Record<string, string>; document: UserDocument }) {
|
||
const Icon = config.icon;
|
||
const visibleFields = config.fields
|
||
.map((field) => ({
|
||
label: field.label,
|
||
value: formatFieldValue(field.kind, values[field.key] ?? (field.key === 'issuedAt' ? document.issuedAt?.slice(0, 10) ?? '' : field.key === 'expiresAt' ? document.expiresAt?.slice(0, 10) ?? '' : ''))
|
||
}))
|
||
.filter((field) => field.value && field.value !== '—');
|
||
|
||
return (
|
||
<div className={cn('overflow-hidden rounded-[24px] border border-[#eceef4] bg-white p-5 shadow-[0_20px_50px_rgba(31,36,48,0.08)]')}>
|
||
<div className="flex items-start gap-4">
|
||
<div className={cn('flex h-14 w-14 items-center justify-center rounded-2xl', config.accent)}>
|
||
<Icon className="h-6 w-6" />
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-[#667085]">Документ</p>
|
||
<p className="mt-1 text-xl font-semibold text-[#1f2430]">{config.label}</p>
|
||
<p className="mt-2 font-mono text-lg text-[#3390ec]">{document.number || buildDocumentNumber(config, values)}</p>
|
||
</div>
|
||
</div>
|
||
<div className="mt-5 grid gap-3 sm:grid-cols-2">
|
||
{visibleFields.map((field) => (
|
||
<DocumentField key={field.label} label={field.label} value={field.value} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function DocumentViewCard({
|
||
documentType,
|
||
document
|
||
}: {
|
||
documentType: DocumentTypeCode;
|
||
document: UserDocument;
|
||
}) {
|
||
const config = getDocumentType(documentType) ?? DOCUMENT_TYPES.find((item) => item.type === documentType);
|
||
if (!config) return null;
|
||
|
||
const metadata = parseMetadata(document.metadataJson);
|
||
const values: Record<string, string> = {};
|
||
for (const field of config.fields) {
|
||
const raw = metadata[field.key];
|
||
values[field.key] = typeof raw === 'string' ? raw : '';
|
||
if (field.latinKey) {
|
||
const latinRaw = metadata[field.latinKey];
|
||
values[field.latinKey] = typeof latinRaw === 'string' ? latinRaw : '';
|
||
}
|
||
}
|
||
if (document.issuedAt) values.issuedAt = document.issuedAt.slice(0, 10);
|
||
if (document.expiresAt) values.expiresAt = document.expiresAt.slice(0, 10);
|
||
|
||
switch (documentType) {
|
||
case 'PASSPORT_RF':
|
||
return <PassportRfCard config={config} values={values} document={document} />;
|
||
case 'FOREIGN_PASSPORT':
|
||
return <ForeignPassportCard config={config} values={values} document={document} />;
|
||
case 'DRIVER_LICENSE':
|
||
return <DriverLicenseCard config={config} values={values} document={document} />;
|
||
case 'VEHICLE_REGISTRATION':
|
||
return <VehicleRegistrationCard config={config} values={values} document={document} />;
|
||
default:
|
||
return <GenericDocumentCard config={config} values={values} document={document} />;
|
||
}
|
||
}
|
||
|
||
export function DriverLicenseCategoryBadges({ value }: { value: string }) {
|
||
const selected = new Set(value.split(',').map((item) => item.trim()).filter(Boolean));
|
||
if (!selected.size) return null;
|
||
return (
|
||
<div className="flex flex-wrap gap-1.5">
|
||
{LICENSE_CATEGORIES.filter((category) => selected.has(category)).map((category) => (
|
||
<span key={category} className="rounded-lg bg-[#20212b] px-2 py-1 text-xs font-medium text-white">
|
||
{category}
|
||
</span>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|