admin page
This commit is contained in:
161
dist/assets/index-BFYBIKDe.js
vendored
161
dist/assets/index-BFYBIKDe.js
vendored
File diff suppressed because one or more lines are too long
165
dist/assets/index-Bzh-PW6c.js
vendored
Normal file
165
dist/assets/index-Bzh-PW6c.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ЭКСА — Ваш мост в мир цифровых активов</title>
|
<title>ЭКСА — Ваш мост в мир цифровых активов</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BFYBIKDe.js"></script>
|
<script type="module" crossorigin src="/assets/index-Bzh-PW6c.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CVaTO0Sb.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D1yEGVJz.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { SoglasiePage } from '@pages/soglasie-personalnyh-dannyh'
|
|||||||
import { ReestryPage } from '@pages/reestr-pd-rkn'
|
import { ReestryPage } from '@pages/reestr-pd-rkn'
|
||||||
import { TransactionsPage } from '@pages/transactions'
|
import { TransactionsPage } from '@pages/transactions'
|
||||||
import { AdminPage } from '@pages/admin'
|
import { AdminPage } from '@pages/admin'
|
||||||
|
import { AdminOrganizationPage } from '@pages/admin-organization'
|
||||||
import { WalletLayout } from '@widgets/wallet-layout'
|
import { WalletLayout } from '@widgets/wallet-layout'
|
||||||
import { ROUTES } from '@shared/config/routes'
|
import { ROUTES } from '@shared/config/routes'
|
||||||
import { ScrollToTop } from './ScrollToTop'
|
import { ScrollToTop } from './ScrollToTop'
|
||||||
@@ -41,6 +42,7 @@ export function RouterProvider() {
|
|||||||
|
|
||||||
{/* Admin panel — own auth gate, independent of the user auth system */}
|
{/* Admin panel — own auth gate, independent of the user auth system */}
|
||||||
<Route path={ROUTES.ADMIN} element={<AdminPage />} />
|
<Route path={ROUTES.ADMIN} element={<AdminPage />} />
|
||||||
|
<Route path={ROUTES.ADMIN_ORGANIZATION} element={<AdminOrganizationPage />} />
|
||||||
|
|
||||||
<Route element={<GuestRoute />}>
|
<Route element={<GuestRoute />}>
|
||||||
<Route path={ROUTES.LOGIN} element={<LoginPage />} />
|
<Route path={ROUTES.LOGIN} element={<LoginPage />} />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
CreateOrganizationRequest,
|
CreateOrganizationRequest,
|
||||||
Organization,
|
Organization,
|
||||||
OrganizationListResponse,
|
OrganizationListResponse,
|
||||||
|
UpdateOrganizationRequest,
|
||||||
} from '../model/types'
|
} from '../model/types'
|
||||||
|
|
||||||
const ADMIN_API_URL = 'https://app.admin.elcsa.ru'
|
const ADMIN_API_URL = 'https://app.admin.elcsa.ru'
|
||||||
@@ -101,3 +102,18 @@ export function createOrganization(payload: CreateOrganizationRequest): Promise<
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getOrganization(id: string): Promise<Organization> {
|
||||||
|
return doAdminRequest<Organization>(`/v1/organizations/${id}`, {}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateOrganization(
|
||||||
|
id: string,
|
||||||
|
payload: UpdateOrganizationRequest,
|
||||||
|
): Promise<Organization> {
|
||||||
|
return doAdminRequest<Organization>(
|
||||||
|
`/v1/organizations/${id}`,
|
||||||
|
{ method: 'PATCH', body: JSON.stringify(payload) },
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
12
src/features/admin/hooks/useOrganization.ts
Normal file
12
src/features/admin/hooks/useOrganization.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { getOrganization } from '../api/adminApi'
|
||||||
|
|
||||||
|
export const ORGANIZATION_QUERY_KEY = (id: string) => ['admin-organization', id]
|
||||||
|
|
||||||
|
export function useOrganization(id: string | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ORGANIZATION_QUERY_KEY(id ?? ''),
|
||||||
|
queryFn: () => getOrganization(id as string),
|
||||||
|
enabled: !!id,
|
||||||
|
})
|
||||||
|
}
|
||||||
16
src/features/admin/hooks/useUpdateOrganization.ts
Normal file
16
src/features/admin/hooks/useUpdateOrganization.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { updateOrganization } from '../api/adminApi'
|
||||||
|
import type { UpdateOrganizationRequest } from '../model/types'
|
||||||
|
import { ORGANIZATIONS_QUERY_KEY } from './useOrganizations'
|
||||||
|
import { ORGANIZATION_QUERY_KEY } from './useOrganization'
|
||||||
|
|
||||||
|
export function useUpdateOrganization(id: string) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: UpdateOrganizationRequest) => updateOrganization(id, payload),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData(ORGANIZATION_QUERY_KEY(id), data)
|
||||||
|
queryClient.invalidateQueries({ queryKey: ORGANIZATIONS_QUERY_KEY })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ export {
|
|||||||
adminLogout,
|
adminLogout,
|
||||||
getAdminMe,
|
getAdminMe,
|
||||||
getOrganizations,
|
getOrganizations,
|
||||||
|
getOrganization,
|
||||||
createOrganization,
|
createOrganization,
|
||||||
|
updateOrganization,
|
||||||
refreshAdminToken,
|
refreshAdminToken,
|
||||||
adminTokenStore,
|
adminTokenStore,
|
||||||
} from './api/adminApi'
|
} from './api/adminApi'
|
||||||
@@ -14,10 +16,13 @@ export type {
|
|||||||
Organization,
|
Organization,
|
||||||
OrganizationListResponse,
|
OrganizationListResponse,
|
||||||
CreateOrganizationRequest,
|
CreateOrganizationRequest,
|
||||||
|
UpdateOrganizationRequest,
|
||||||
BankDetails,
|
BankDetails,
|
||||||
} from './model/types'
|
} from './model/types'
|
||||||
export { useAdminAuth, ADMIN_AUTH_QUERY_KEY } from './hooks/useAdminAuth'
|
export { useAdminAuth, ADMIN_AUTH_QUERY_KEY } from './hooks/useAdminAuth'
|
||||||
export { useAdminLogin } from './hooks/useAdminLogin'
|
export { useAdminLogin } from './hooks/useAdminLogin'
|
||||||
export { useAdminLogout } from './hooks/useAdminLogout'
|
export { useAdminLogout } from './hooks/useAdminLogout'
|
||||||
export { useOrganizations, ORGANIZATIONS_QUERY_KEY } from './hooks/useOrganizations'
|
export { useOrganizations, ORGANIZATIONS_QUERY_KEY } from './hooks/useOrganizations'
|
||||||
|
export { useOrganization, ORGANIZATION_QUERY_KEY } from './hooks/useOrganization'
|
||||||
export { useCreateOrganization } from './hooks/useCreateOrganization'
|
export { useCreateOrganization } from './hooks/useCreateOrganization'
|
||||||
|
export { useUpdateOrganization } from './hooks/useUpdateOrganization'
|
||||||
|
|||||||
@@ -50,6 +50,19 @@ export interface OrganizationListResponse {
|
|||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateOrganizationRequest {
|
||||||
|
name?: string | null
|
||||||
|
short_name?: string | null
|
||||||
|
ogrn?: string | null
|
||||||
|
kpp?: string | null
|
||||||
|
legal_address?: string | null
|
||||||
|
actual_address?: string | null
|
||||||
|
bank_details?: BankDetails | null
|
||||||
|
contact_person?: string | null
|
||||||
|
contact_phone?: string | null
|
||||||
|
status?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateOrganizationRequest {
|
export interface CreateOrganizationRequest {
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
|||||||
1
src/pages/admin-organization/index.ts
Normal file
1
src/pages/admin-organization/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { AdminOrganizationPage } from './ui/AdminOrganizationPage'
|
||||||
106
src/pages/admin-organization/model/useOrganizationForm.ts
Normal file
106
src/pages/admin-organization/model/useOrganizationForm.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useUpdateOrganization } from '@features/admin'
|
||||||
|
import type { Organization, UpdateOrganizationRequest } from '@features/admin'
|
||||||
|
|
||||||
|
interface FormState {
|
||||||
|
name: string
|
||||||
|
short_name: string
|
||||||
|
ogrn: string
|
||||||
|
kpp: string
|
||||||
|
legal_address: string
|
||||||
|
actual_address: string
|
||||||
|
contact_person: string
|
||||||
|
contact_phone: string
|
||||||
|
status: string
|
||||||
|
bank_details: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function toForm(org: Organization): FormState {
|
||||||
|
return {
|
||||||
|
name: org.name ?? '',
|
||||||
|
short_name: org.short_name ?? '',
|
||||||
|
ogrn: org.ogrn ?? '',
|
||||||
|
kpp: org.kpp ?? '',
|
||||||
|
legal_address: org.legal_address ?? '',
|
||||||
|
actual_address: org.actual_address ?? '',
|
||||||
|
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) : '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractErrorMessage(error: unknown): string {
|
||||||
|
const e = error as { detail?: unknown }
|
||||||
|
if (typeof e?.detail === 'string') return e.detail
|
||||||
|
if (Array.isArray(e?.detail) && (e.detail[0] as { msg?: string })?.msg) {
|
||||||
|
return (e.detail[0] as { msg: string }).msg
|
||||||
|
}
|
||||||
|
return 'Не удалось сохранить изменения'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOrganizationForm(
|
||||||
|
org: Organization | undefined,
|
||||||
|
id: string,
|
||||||
|
onSaved?: () => void,
|
||||||
|
) {
|
||||||
|
const [form, setForm] = useState<FormState>(() =>
|
||||||
|
org ? toForm(org) : {
|
||||||
|
name: '', short_name: '', ogrn: '', kpp: '', legal_address: '',
|
||||||
|
actual_address: '', contact_person: '', contact_phone: '', status: '', bank_details: '',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const [bankError, setBankError] = useState<string | null>(null)
|
||||||
|
const mutation = useUpdateOrganization(id)
|
||||||
|
|
||||||
|
// Sync local form state once the organization loads / changes.
|
||||||
|
useEffect(() => {
|
||||||
|
if (org) setForm(toForm(org))
|
||||||
|
}, [org])
|
||||||
|
|
||||||
|
const setField = (key: keyof FormState) => (value: string) =>
|
||||||
|
setForm((prev) => ({ ...prev, [key]: value }))
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setBankError(null)
|
||||||
|
|
||||||
|
const trimmedOrNull = (v: string) => (v.trim() ? v.trim() : null)
|
||||||
|
|
||||||
|
let bank_details: Record<string, unknown> | 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),
|
||||||
|
ogrn: trimmedOrNull(form.ogrn),
|
||||||
|
kpp: trimmedOrNull(form.kpp),
|
||||||
|
legal_address: trimmedOrNull(form.legal_address),
|
||||||
|
actual_address: trimmedOrNull(form.actual_address),
|
||||||
|
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)
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
setField,
|
||||||
|
handleSubmit,
|
||||||
|
isSaving: mutation.isPending,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/pages/admin-organization/ui/AdminOrganizationPage.module.css
Normal file
120
src/pages/admin-organization/ui/AdminOrganizationPage.module.css
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg-deep);
|
||||||
|
padding: 40px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary, rgba(255, 255, 255, 0.6));
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back:hover {
|
||||||
|
color: var(--text-primary, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: clamp(24px, 3.5vw, 34px);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary, #fff);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-secondary, rgba(255, 255, 255, 0.5));
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bankLabel {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary, rgba(255, 255, 255, 0.5));
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--glass-bg, rgba(255, 255, 255, 0.06));
|
||||||
|
border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--text-primary, #fff);
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea:focus {
|
||||||
|
border-color: var(--interactive, #4a6dff);
|
||||||
|
box-shadow: 0 0 0 3px rgba(74, 109, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary, rgba(255, 255, 255, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #ff5a5a;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
max-width: 320px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page {
|
||||||
|
padding: 28px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/pages/admin-organization/ui/AdminOrganizationPage.tsx
Normal file
117
src/pages/admin-organization/ui/AdminOrganizationPage.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
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 { useOrganizationForm } from '../model/useOrganizationForm'
|
||||||
|
import styles from './AdminOrganizationPage.module.css'
|
||||||
|
|
||||||
|
function formatDateTime(value: string | null): string {
|
||||||
|
if (!value) return '—'
|
||||||
|
const d = new Date(value)
|
||||||
|
if (Number.isNaN(d.getTime())) return '—'
|
||||||
|
return d.toLocaleString('ru-RU')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdminOrganizationPage() {
|
||||||
|
const { isAuthenticated, isLoading: isAuthLoading } = useAdminAuth()
|
||||||
|
const { organizationId } = useParams<{ organizationId: string }>()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { data: org, isLoading, isError } = useOrganization(organizationId)
|
||||||
|
const [notice, setNotice] = useState(false)
|
||||||
|
const { form, setField, handleSubmit, isSaving, error } = useOrganizationForm(
|
||||||
|
org,
|
||||||
|
organizationId ?? '',
|
||||||
|
() => setNotice(true),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isAuthLoading) return null
|
||||||
|
if (!isAuthenticated) return <AdminLoginForm />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.page}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<button className={styles.back} type="button" onClick={() => navigate(ROUTES.ADMIN)}>
|
||||||
|
← Назад к списку
|
||||||
|
</button>
|
||||||
|
<h1 className={styles.title}>{org ? org.name : 'Юридическое лицо'}</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{isLoading && <div className={styles.state}>Загрузка...</div>}
|
||||||
|
{isError && <div className={styles.state}>Не удалось загрузить организацию</div>}
|
||||||
|
|
||||||
|
{org && (
|
||||||
|
<form className={styles.form} onSubmit={handleSubmit}>
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>Реквизиты</h2>
|
||||||
|
<div className={styles.grid}>
|
||||||
|
<FormField label="Наименование" value={form.name} onChange={setField('name')} required />
|
||||||
|
<FormField label="Краткое наименование" value={form.short_name} onChange={setField('short_name')} />
|
||||||
|
<FormField label="ИНН" value={org.inn} readOnly icon="lock" />
|
||||||
|
<FormField label="ОГРН" value={form.ogrn} onChange={setField('ogrn')} />
|
||||||
|
<FormField label="КПП" value={form.kpp} onChange={setField('kpp')} />
|
||||||
|
<FormField label="Статус" value={form.status} onChange={setField('status')} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>Адреса</h2>
|
||||||
|
<div className={styles.grid}>
|
||||||
|
<FormField label="Юридический адрес" value={form.legal_address} onChange={setField('legal_address')} />
|
||||||
|
<FormField label="Фактический адрес" value={form.actual_address} onChange={setField('actual_address')} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>Контакты</h2>
|
||||||
|
<div className={styles.grid}>
|
||||||
|
<FormField label="Контактное лицо" value={form.contact_person} onChange={setField('contact_person')} />
|
||||||
|
<FormField label="Контактный телефон" type="tel" value={form.contact_phone} onChange={setField('contact_phone')} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>Банковские реквизиты</h2>
|
||||||
|
<label className={styles.bankLabel}>JSON-объект реквизитов</label>
|
||||||
|
<textarea
|
||||||
|
className={styles.textarea}
|
||||||
|
value={form.bank_details}
|
||||||
|
onChange={(e) => setField('bank_details')(e.target.value)}
|
||||||
|
placeholder={'{\n "bank_name": "...",\n "bik": "...",\n "account": "..."\n}'}
|
||||||
|
rows={6}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>Системная информация</h2>
|
||||||
|
<div className={styles.grid}>
|
||||||
|
<FormField label="ID организации" value={org.id} readOnly icon="lock" />
|
||||||
|
<FormField label="ID пользователя" value={org.user_id} readOnly icon="lock" />
|
||||||
|
<FormField label="KYC" value={org.kyc_verified ? 'Подтверждён' : 'Не подтверждён'} readOnly />
|
||||||
|
<FormField label="Дата KYC" value={formatDateTime(org.kyc_verified_at)} readOnly />
|
||||||
|
<FormField label="Кошельки" value={org.has_wallets ? 'Есть' : 'Нет'} readOnly />
|
||||||
|
<FormField label="Создано" value={formatDateTime(org.created_at)} readOnly />
|
||||||
|
<FormField label="Обновлено" value={formatDateTime(org.updated_at)} readOnly />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{error && <p className={styles.error}>{error}</p>}
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<PrimaryButton label={isSaving ? 'Сохранение...' : 'Сохранить изменения'} disabled={isSaving} />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{notice && (
|
||||||
|
<Notification
|
||||||
|
status="success"
|
||||||
|
message="Изменения сохранены"
|
||||||
|
onClose={() => setNotice(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -20,4 +20,8 @@ export const ROUTES = {
|
|||||||
REESTR_PD_RKN: '/reestr-pd-rkn',
|
REESTR_PD_RKN: '/reestr-pd-rkn',
|
||||||
TRANSACTIONS: '/transactions',
|
TRANSACTIONS: '/transactions',
|
||||||
ADMIN: '/sys-c7f29a4e-d81b-4630-ops-console',
|
ADMIN: '/sys-c7f29a4e-d81b-4630-ops-console',
|
||||||
|
ADMIN_ORGANIZATION: '/sys-c7f29a4e-d81b-4630-ops-console/organizations/:organizationId',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export const adminOrganizationPath = (id: string) =>
|
||||||
|
`/sys-c7f29a4e-d81b-4630-ops-console/organizations/${id}`
|
||||||
|
|||||||
@@ -35,6 +35,10 @@
|
|||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useOrganizations } from '@features/admin'
|
import { useOrganizations } from '@features/admin'
|
||||||
|
import { adminOrganizationPath } from '@shared/config/routes'
|
||||||
import styles from './LegalEntitiesTable.module.css'
|
import styles from './LegalEntitiesTable.module.css'
|
||||||
|
|
||||||
const STATUS_LABELS: Record<string, string> = {
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
@@ -16,6 +18,7 @@ function formatDate(value: string | null): string {
|
|||||||
|
|
||||||
export function LegalEntitiesTable() {
|
export function LegalEntitiesTable() {
|
||||||
const { data, isLoading, isError } = useOrganizations()
|
const { data, isLoading, isError } = useOrganizations()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div className={styles.tableWrap}><div className={styles.state}>Загрузка...</div></div>
|
return <div className={styles.tableWrap}><div className={styles.state}>Загрузка...</div></div>
|
||||||
@@ -54,7 +57,11 @@ export function LegalEntitiesTable() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.items.map((org) => (
|
{data.items.map((org) => (
|
||||||
<tr key={org.id}>
|
<tr
|
||||||
|
key={org.id}
|
||||||
|
className={styles.row}
|
||||||
|
onClick={() => navigate(adminOrganizationPath(org.id))}
|
||||||
|
>
|
||||||
<td>
|
<td>
|
||||||
<span className={styles.name}>{org.name}</span>
|
<span className={styles.name}>{org.name}</span>
|
||||||
{org.short_name && <span className={styles.subname}>{org.short_name}</span>}
|
{org.short_name && <span className={styles.subname}>{org.short_name}</span>}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user