fix and update

This commit is contained in:
lendry
2026-07-01 23:27:48 +03:00
parent 2b88e028c6
commit bcdfbc3861
36 changed files with 1504 additions and 320 deletions

View File

@@ -0,0 +1,270 @@
'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>
);
}