Files
IdP/apps/frontend/components/documents/document-view-card.tsx
2026-07-01 23:27:48 +03:00

271 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.
'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>
);
}