admin page

This commit is contained in:
2026-06-05 22:33:02 +03:00
parent fd66ca9c9b
commit f4af2fd137
17 changed files with 508 additions and 43 deletions

View File

@@ -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<T>(
allowRetry: boolean,
): Promise<T> {
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<Organization> {
return doAdminRequest<Organization>(`/v1/organizations/${id}`, {}, true)
}
export function createOrganizationWallets(id: string): Promise<WalletResponse[]> {
return doAdminRequest<WalletResponse[]>(
`/v1/organizations/${id}/wallets/create`,
{ method: 'POST' },
true,
)
}
export async function getDocuments(orgId: string): Promise<DocumentResponse[]> {
const data = await doAdminRequest<DocumentResponse[]>(
`/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<DocumentResponse> {
const body = new FormData()
body.append('document_type', documentType)
body.append('file', file)
const data = await doAdminRequest<DocumentResponse>(
`/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<DocumentResponse> {
const data = await doAdminRequest<DocumentResponse>(
`/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,

View File

@@ -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) })
},
})
}

View File

@@ -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,
})
}

View File

@@ -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<string | null>(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 }
}

View File

@@ -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) })
},
})
}

View File

@@ -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'

View File

@@ -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