From f4af2fd13769af603aa166d9f1fb9c1b59539681 Mon Sep 17 00:00:00 2001 From: rassadin11 Date: Fri, 5 Jun 2026 22:33:02 +0300 Subject: [PATCH] admin page --- src/features/admin/api/adminApi.ts | 59 ++++++- .../hooks/useCreateOrganizationWallets.ts | 16 ++ src/features/admin/hooks/useDocuments.ts | 12 ++ .../admin/hooks/useDownloadDocument.ts | 32 ++++ src/features/admin/hooks/useUploadDocument.ts | 19 +++ src/features/admin/index.ts | 10 ++ src/features/admin/model/types.ts | 20 +++ .../model/useOrganizationForm.ts | 20 +-- .../ui/AdminOrganizationPage.module.css | 5 + .../ui/AdminOrganizationPage.tsx | 20 +-- src/pages/admin/ui/AdminPage.tsx | 30 +++- .../model/useAddLegalEntityForm.ts | 8 +- .../ui/AddLegalEntityModal.tsx | 7 +- src/widgets/organization-documents/index.ts | 1 + .../ui/OrganizationDocuments.module.css | 145 ++++++++++++++++++ .../ui/OrganizationDocuments.tsx | 145 ++++++++++++++++++ tsconfig.tsbuildinfo | 2 +- 17 files changed, 508 insertions(+), 43 deletions(-) create mode 100644 src/features/admin/hooks/useCreateOrganizationWallets.ts create mode 100644 src/features/admin/hooks/useDocuments.ts create mode 100644 src/features/admin/hooks/useDownloadDocument.ts create mode 100644 src/features/admin/hooks/useUploadDocument.ts create mode 100644 src/widgets/organization-documents/index.ts create mode 100644 src/widgets/organization-documents/ui/OrganizationDocuments.module.css create mode 100644 src/widgets/organization-documents/ui/OrganizationDocuments.tsx diff --git a/src/features/admin/api/adminApi.ts b/src/features/admin/api/adminApi.ts index 67e5813..f177de7 100644 --- a/src/features/admin/api/adminApi.ts +++ b/src/features/admin/api/adminApi.ts @@ -3,9 +3,11 @@ import type { AdminLoginResponse, AdminMeResponse, CreateOrganizationRequest, + DocumentResponse, Organization, OrganizationListResponse, UpdateOrganizationRequest, + WalletResponse, } from '../model/types' const ADMIN_API_URL = 'https://app.admin.elcsa.ru' @@ -26,12 +28,15 @@ async function doAdminRequest( allowRetry: boolean, ): Promise { const bearer = adminTokenStore.get() + // For multipart uploads we must NOT set Content-Type — the browser adds the + // boundary itself. Detect FormData bodies and skip the JSON header. + const isFormData = options.body instanceof FormData const res = await fetch(`${ADMIN_API_URL}${path}`, { ...options, credentials: 'include', headers: { - 'Content-Type': 'application/json', + ...(isFormData ? {} : { 'Content-Type': 'application/json' }), ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}), ...options.headers, }, @@ -107,6 +112,58 @@ export function getOrganization(id: string): Promise { return doAdminRequest(`/v1/organizations/${id}`, {}, true) } +export function createOrganizationWallets(id: string): Promise { + return doAdminRequest( + `/v1/organizations/${id}/wallets/create`, + { method: 'POST' }, + true, + ) +} + +export async function getDocuments(orgId: string): Promise { + const data = await doAdminRequest( + `/v1/organizations/${orgId}/documents`, + {}, + true, + ) + // TEMP: inspect the real backend shape (download_url presence, fields). + console.log('[documents] list response:', data) + return data +} + +export async function uploadDocument( + orgId: string, + documentType: string, + file: File, +): Promise { + const body = new FormData() + body.append('document_type', documentType) + body.append('file', file) + + const data = await doAdminRequest( + `/v1/organizations/${orgId}/documents`, + { method: 'POST', body }, + true, + ) + // TEMP: inspect the real backend shape after upload. + console.log('[documents] upload response:', data) + return data +} + +export async function getDocument( + orgId: string, + documentId: string, +): Promise { + const data = await doAdminRequest( + `/v1/organizations/${orgId}/documents/${documentId}`, + {}, + true, + ) + // TEMP: inspect single-document shape (this is where download_url should appear). + console.log('[documents] get-one response:', data) + return data +} + export function updateOrganization( id: string, payload: UpdateOrganizationRequest, diff --git a/src/features/admin/hooks/useCreateOrganizationWallets.ts b/src/features/admin/hooks/useCreateOrganizationWallets.ts new file mode 100644 index 0000000..dbb6c4e --- /dev/null +++ b/src/features/admin/hooks/useCreateOrganizationWallets.ts @@ -0,0 +1,16 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { createOrganizationWallets } from '../api/adminApi' +import { ORGANIZATIONS_QUERY_KEY } from './useOrganizations' +import { ORGANIZATION_QUERY_KEY } from './useOrganization' + +export function useCreateOrganizationWallets() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (organizationId: string) => createOrganizationWallets(organizationId), + onSuccess: (_wallets, organizationId) => { + // `has_wallets` flips to true server-side — refresh list and detail view. + queryClient.invalidateQueries({ queryKey: ORGANIZATIONS_QUERY_KEY }) + queryClient.invalidateQueries({ queryKey: ORGANIZATION_QUERY_KEY(organizationId) }) + }, + }) +} diff --git a/src/features/admin/hooks/useDocuments.ts b/src/features/admin/hooks/useDocuments.ts new file mode 100644 index 0000000..6e76234 --- /dev/null +++ b/src/features/admin/hooks/useDocuments.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query' +import { getDocuments } from '../api/adminApi' + +export const DOCUMENTS_QUERY_KEY = (orgId: string) => ['admin-documents', orgId] + +export function useDocuments(orgId: string | undefined) { + return useQuery({ + queryKey: DOCUMENTS_QUERY_KEY(orgId ?? ''), + queryFn: () => getDocuments(orgId as string), + enabled: !!orgId, + }) +} diff --git a/src/features/admin/hooks/useDownloadDocument.ts b/src/features/admin/hooks/useDownloadDocument.ts new file mode 100644 index 0000000..52fea3e --- /dev/null +++ b/src/features/admin/hooks/useDownloadDocument.ts @@ -0,0 +1,32 @@ +import { useState } from 'react' +import { getDocument } from '../api/adminApi' +import type { DocumentResponse } from '../model/types' + +/** + * Resolves a document's download URL and opens it. The list endpoint may not + * include a fresh `download_url`, so we fall back to fetching the single + * document (which is where the presigned URL is expected to appear). + */ +export function useDownloadDocument(orgId: string) { + const [downloadingId, setDownloadingId] = useState(null) + + async function download(doc: DocumentResponse) { + setDownloadingId(doc.id) + try { + let url = doc.download_url + if (!url) { + const fresh = await getDocument(orgId, doc.id) + url = fresh.download_url + } + if (url) { + window.open(url, '_blank', 'noopener,noreferrer') + } else { + throw new Error('Сервер не вернул ссылку для скачивания') + } + } finally { + setDownloadingId(null) + } + } + + return { download, downloadingId } +} diff --git a/src/features/admin/hooks/useUploadDocument.ts b/src/features/admin/hooks/useUploadDocument.ts new file mode 100644 index 0000000..2b6fd60 --- /dev/null +++ b/src/features/admin/hooks/useUploadDocument.ts @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { uploadDocument } from '../api/adminApi' +import { DOCUMENTS_QUERY_KEY } from './useDocuments' + +interface UploadArgs { + documentType: string + file: File +} + +export function useUploadDocument(orgId: string) { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: ({ documentType, file }: UploadArgs) => + uploadDocument(orgId, documentType, file), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: DOCUMENTS_QUERY_KEY(orgId) }) + }, + }) +} diff --git a/src/features/admin/index.ts b/src/features/admin/index.ts index 38f22e7..4890abe 100644 --- a/src/features/admin/index.ts +++ b/src/features/admin/index.ts @@ -5,7 +5,11 @@ export { getOrganizations, getOrganization, createOrganization, + createOrganizationWallets, updateOrganization, + getDocuments, + uploadDocument, + getDocument, refreshAdminToken, adminTokenStore, } from './api/adminApi' @@ -17,6 +21,8 @@ export type { OrganizationListResponse, CreateOrganizationRequest, UpdateOrganizationRequest, + WalletResponse, + DocumentResponse, BankDetails, } from './model/types' export { useAdminAuth, ADMIN_AUTH_QUERY_KEY } from './hooks/useAdminAuth' @@ -25,4 +31,8 @@ export { useAdminLogout } from './hooks/useAdminLogout' export { useOrganizations, ORGANIZATIONS_QUERY_KEY } from './hooks/useOrganizations' export { useOrganization, ORGANIZATION_QUERY_KEY } from './hooks/useOrganization' export { useCreateOrganization } from './hooks/useCreateOrganization' +export { useCreateOrganizationWallets } from './hooks/useCreateOrganizationWallets' export { useUpdateOrganization } from './hooks/useUpdateOrganization' +export { useDocuments, DOCUMENTS_QUERY_KEY } from './hooks/useDocuments' +export { useUploadDocument } from './hooks/useUploadDocument' +export { useDownloadDocument } from './hooks/useDownloadDocument' diff --git a/src/features/admin/model/types.ts b/src/features/admin/model/types.ts index c950646..42f919b 100644 --- a/src/features/admin/model/types.ts +++ b/src/features/admin/model/types.ts @@ -50,6 +50,26 @@ export interface OrganizationListResponse { total: number } +export interface WalletResponse { + id: string + chain: string + address: string + derivation_path: string + created_at: string | null +} + +export interface DocumentResponse { + id: string + organization_id: string + document_type: string + file_name: string + content_type: string + file_size_bytes: number + uploaded_by: string | null + created_at: string | null + download_url: string | null +} + export interface UpdateOrganizationRequest { name?: string | null short_name?: string | null diff --git a/src/pages/admin-organization/model/useOrganizationForm.ts b/src/pages/admin-organization/model/useOrganizationForm.ts index db49d34..a484aa1 100644 --- a/src/pages/admin-organization/model/useOrganizationForm.ts +++ b/src/pages/admin-organization/model/useOrganizationForm.ts @@ -12,7 +12,6 @@ interface FormState { contact_person: string contact_phone: string status: string - bank_details: string } function toForm(org: Organization): FormState { @@ -26,7 +25,6 @@ function toForm(org: Organization): FormState { contact_person: org.contact_person ?? '', contact_phone: org.contact_phone ?? '', status: org.status ?? '', - bank_details: org.bank_details ? JSON.stringify(org.bank_details, null, 2) : '', } } @@ -47,10 +45,9 @@ export function useOrganizationForm( const [form, setForm] = useState(() => org ? toForm(org) : { name: '', short_name: '', ogrn: '', kpp: '', legal_address: '', - actual_address: '', contact_person: '', contact_phone: '', status: '', bank_details: '', + actual_address: '', contact_person: '', contact_phone: '', status: '', }, ) - const [bankError, setBankError] = useState(null) const mutation = useUpdateOrganization(id) // Sync local form state once the organization loads / changes. @@ -63,20 +60,9 @@ export function useOrganizationForm( const handleSubmit = (e: React.FormEvent) => { e.preventDefault() - setBankError(null) const trimmedOrNull = (v: string) => (v.trim() ? v.trim() : null) - let bank_details: Record | null = null - if (form.bank_details.trim()) { - try { - bank_details = JSON.parse(form.bank_details) - } catch { - setBankError('Банковские реквизиты должны быть корректным JSON') - return - } - } - const payload: UpdateOrganizationRequest = { name: form.name.trim(), short_name: trimmedOrNull(form.short_name), @@ -87,14 +73,12 @@ export function useOrganizationForm( contact_person: trimmedOrNull(form.contact_person), contact_phone: trimmedOrNull(form.contact_phone), status: trimmedOrNull(form.status), - bank_details, } mutation.mutate(payload, { onSuccess: () => onSaved?.() }) } - const error = - bankError ?? (mutation.isError ? extractErrorMessage(mutation.error) : null) + const error = mutation.isError ? extractErrorMessage(mutation.error) : null return { form, diff --git a/src/pages/admin-organization/ui/AdminOrganizationPage.module.css b/src/pages/admin-organization/ui/AdminOrganizationPage.module.css index 6f02829..ad4fb8b 100644 --- a/src/pages/admin-organization/ui/AdminOrganizationPage.module.css +++ b/src/pages/admin-organization/ui/AdminOrganizationPage.module.css @@ -39,6 +39,11 @@ gap: 24px; } +.documents { + max-width: 900px; + margin: 24px auto 0; +} + .section { background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.08); diff --git a/src/pages/admin-organization/ui/AdminOrganizationPage.tsx b/src/pages/admin-organization/ui/AdminOrganizationPage.tsx index a6959f7..24fdad2 100644 --- a/src/pages/admin-organization/ui/AdminOrganizationPage.tsx +++ b/src/pages/admin-organization/ui/AdminOrganizationPage.tsx @@ -4,6 +4,7 @@ import { useAdminAuth, useOrganization } from '@features/admin' import { ROUTES } from '@shared/config/routes' import { FormField, Notification, PrimaryButton } from '@shared/ui' import { AdminLoginForm } from '@widgets/admin-login-form' +import { OrganizationDocuments } from '@widgets/organization-documents' import { useOrganizationForm } from '../model/useOrganizationForm' import styles from './AdminOrganizationPage.module.css' @@ -71,19 +72,6 @@ export function AdminOrganizationPage() { -
-

Банковские реквизиты

- -