first commit
This commit is contained in:
133
apps/frontend/app/documents/page.tsx
Normal file
133
apps/frontend/app/documents/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user