first commit

This commit is contained in:
lendry
2026-06-24 14:37:15 +03:00
commit 995adeedd4
188 changed files with 28810 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { ChevronRight, Plus } from 'lucide-react';
import { DocumentFormDialog } from '@/components/documents/document-form-dialog';
import { useAuth } from '@/components/id/auth-provider';
import { IdShell } from '@/components/id/shell';
import { useToast } from '@/components/id/toast-provider';
import {
DOCUMENT_CATEGORIES,
DOCUMENT_TYPES,
getDocumentType,
QUICK_DOCUMENT_TYPES,
indexDocumentsByType,
type DocumentTypeCode
} from '@/lib/document-catalog';
import { apiFetch, getApiErrorMessage, UserDocument } from '@/lib/api';
import { cn } from '@/lib/utils';
import { useRequireAuth } from '@/hooks/use-require-auth';
export default function DocumentsPage() {
const router = useRouter();
const { user, token } = useAuth();
const { isReady, isPinLocked } = useRequireAuth();
const { showToast } = useToast();
const [documents, setDocuments] = useState<UserDocument[]>([]);
const [activeType, setActiveType] = useState<DocumentTypeCode | null>(null);
const loadDocuments = useCallback(async () => {
if (!user || !token || isPinLocked) return;
try {
const response = await apiFetch<{ documents?: UserDocument[] }>(`/documents/users/${user.id}`, {}, token);
setDocuments(response.documents ?? []);
} catch (error) {
const message = getApiErrorMessage(error, 'Не удалось загрузить документы');
if (message) showToast(message);
}
}, [isPinLocked, showToast, token, user]);
useEffect(() => {
if (!isReady || !user || isPinLocked) return;
void loadDocuments();
}, [isPinLocked, isReady, loadDocuments, user]);
const documentsByType = useMemo(() => indexDocumentsByType(documents), [documents]);
function openCreate(type: DocumentTypeCode) {
setActiveType(type);
}
return (
<IdShell active="/documents" wide>
<h1 className="text-3xl font-medium tracking-tight">Документы</h1>
<p className="mt-1 text-sm text-[#667085]">Храните документы и заполняйте формы ID подставит данные там, где вы разрешите.</p>
<div className="mt-6 overflow-hidden rounded-[28px] bg-[linear-gradient(135deg,#fff4f4,#fff8ef)] p-5">
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-lg font-medium">Здесь будут ваши документы</p>
<p className="mt-1 text-sm text-[#667085]">Добавьте паспорт, права или полис данные сохранятся в зашифрованном виде.</p>
</div>
<div className="flex h-16 w-16 items-center justify-center rounded-3xl bg-white text-3xl shadow-sm">🛂</div>
</div>
</div>
<div className="mt-5 grid grid-cols-3 gap-3">
{QUICK_DOCUMENT_TYPES.map((typeCode) => {
const config = getDocumentType(typeCode);
if (!config) return null;
const Icon = config.icon;
return (
<button
key={typeCode}
type="button"
onClick={() => openCreate(typeCode)}
className="flex min-h-[110px] flex-col items-center justify-center gap-2 rounded-[22px] bg-[#f4f5f8] p-3 text-center transition hover:bg-[#eceef4]"
>
<div className={cn('flex h-12 w-12 items-center justify-center rounded-2xl', config.accent)}>
<Icon className="h-5 w-5" />
</div>
<span className="text-[12px] font-medium leading-tight">Добавить {config.shortLabel.toLowerCase()}</span>
</button>
);
})}
</div>
{DOCUMENT_CATEGORIES.map((category) => (
<section key={category.id} className="mt-8">
<h2 className="mb-3 text-xl font-medium">{category.title}</h2>
<div className="overflow-hidden rounded-[24px] bg-[#f4f5f8]">
{DOCUMENT_TYPES.filter((item) => item.category === category.id).map((item) => {
const Icon = item.icon;
const existing = documentsByType.get(item.type);
return (
<button
key={item.type}
type="button"
onClick={() => openCreate(item.type)}
className="flex w-full items-center gap-4 border-b border-white/70 px-4 py-4 text-left last:border-b-0 hover:bg-white/60"
>
<div className={cn('flex h-11 w-11 items-center justify-center rounded-2xl', item.accent)}>
<Icon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1">
<div className="font-medium">{item.label}</div>
{existing ? <div className="truncate text-sm text-[#667085]">{existing.number}</div> : null}
</div>
{existing ? <ChevronRight className="h-5 w-5 text-[#a8adbc]" /> : <Plus className="h-5 w-5 text-[#a8adbc]" />}
</button>
);
})}
</div>
</section>
))}
{activeType ? (
<DocumentFormDialog
open={Boolean(activeType)}
onOpenChange={(open) => {
if (!open) setActiveType(null);
}}
documentType={activeType}
userId={user?.id ?? ''}
token={token}
existing={documentsByType.get(activeType) ?? null}
onSaved={loadDocuments}
/>
) : null}
</IdShell>
);
}