admin page

This commit is contained in:
2026-06-06 10:28:21 +03:00
parent d296032e90
commit d1e6529950
6 changed files with 132 additions and 25 deletions

View File

@@ -3,15 +3,36 @@ import { tokenStore } from '@shared/api/tokenStore'
const USERS_API_URL = 'https://app.users.elcsa.ru'
export type AccountType = 'individual' | 'legal_entity'
// Nested organization payload — present only on legal_entity accounts.
export interface LegalEntityInfo {
id: string
name: string
inn: string
status: string
short_name: string | null
ogrn: string | null
kpp: string | null
legal_address: string | null
actual_address: string | null
bank_details: Record<string, unknown> | null
contact_person: string | null
contact_phone: string | null
kyc_verified: boolean
kyc_verified_at: string | null
}
export interface MeResponse {
id: string
email: string
first_name: string
middle_name: string
last_name: string
birth_date: string
// Person fields are null on legal_entity accounts.
first_name: string | null
middle_name: string | null
last_name: string | null
birth_date: string | null
encrypted_mnemonic: string | null
phone: string
phone: string | null
passport_data: string | null
inn: string | null
erc20: string | null
@@ -21,7 +42,11 @@ export interface MeResponse {
created_at: string
updated_at: string
kyc_verified_at: string | null
webp_size_bytes: number
webp_size_bytes?: number
// "individual" -> физлицо, "legal_entity" -> аккаунт юр.лица.
account_type: AccountType
// Populated only for legal_entity accounts.
legal_entity?: LegalEntityInfo | null
}
export interface UploadAvatarPayload {

View File

@@ -0,0 +1,34 @@
import type { MeResponse } from '@features/auth'
import { FormField } from '@shared/ui'
import { ProfileSection } from '@widgets/profile'
import styles from './ProfilePage.module.css'
interface Props {
data: MeResponse
fullName: string
phone: string
onPhoneChange: (value: string) => void
onPhoneBlur: () => void
}
export function IndividualFields({ data, fullName, phone, onPhoneChange, onPhoneBlur }: Props) {
return (
<>
<ProfileSection title="Личные данные">
<div className={styles.grid2}>
<FormField label="Полное ФИО" value={fullName} placeholder="Например: Иванов Иван Иванович" readOnly />
<FormField label="Адрес электронной почты" value={data.email ?? ''} type="email" icon="check" placeholder="example@mail.ru" readOnly />
<FormField label="Серия и номер паспорта" value={data.passport_data ?? ''} placeholder="0000 000000" readOnly />
<FormField label="Номер телефона" value={phone} onChange={onPhoneChange} onBlur={onPhoneBlur} type="tel" placeholder="+7 (999) 000-00-00" />
</div>
</ProfileSection>
<ProfileSection title="Верификация">
<div className={styles.grid2}>
<FormField label="ИНН" value={data.inn ?? ''} readOnly icon="lock" placeholder="000000000000" />
<FormField label="ID аккаунта" value={data.id ?? ''} readOnly icon="lock" placeholder="ECSA-00000000" />
</div>
</ProfileSection>
</>
)
}

View File

@@ -0,0 +1,47 @@
import type { MeResponse } from '@features/auth'
import { FormField } from '@shared/ui'
import { ProfileSection } from '@widgets/profile'
import styles from './ProfilePage.module.css'
interface Props {
data: MeResponse
}
// Legal-account fields. Organization data lives in the nested `legal_entity`
// object; person-level fields are null on these accounts.
// All read-only — organization data is managed admin-side, not by the user.
export function LegalEntityFields({ data }: Props) {
const le = data.legal_entity
if (!le) return null
return (
<>
<ProfileSection title="Данные организации">
<div className={styles.grid2}>
<FormField label="Наименование" value={le.name ?? ''} placeholder="ООО «Ромашка»" readOnly />
<FormField label="Краткое наименование" value={le.short_name ?? ''} placeholder="Ромашка" readOnly />
<FormField label="ИНН" value={le.inn ?? ''} readOnly icon="lock" placeholder="000000000000" />
<FormField label="ОГРН" value={le.ogrn ?? ''} placeholder="1027700132195" readOnly />
<FormField label="КПП" value={le.kpp ?? ''} placeholder="770801001" readOnly />
<FormField label="Адрес электронной почты" value={data.email ?? ''} type="email" icon="check" placeholder="org@mail.ru" readOnly />
</div>
</ProfileSection>
<ProfileSection title="Адреса">
<div className={styles.grid2}>
<FormField label="Юридический адрес" value={le.legal_address ?? ''} placeholder="г. Москва, ул. Тверская, д. 1" readOnly />
<FormField label="Фактический адрес" value={le.actual_address ?? ''} placeholder="г. Москва, ул. Тверская, д. 1" readOnly />
</div>
</ProfileSection>
<ProfileSection title="Контакты и верификация">
<div className={styles.grid2}>
<FormField label="Контактное лицо" value={le.contact_person ?? ''} placeholder="Иванов Иван Иванович" readOnly />
<FormField label="Контактный телефон" value={le.contact_phone ?? ''} type="tel" placeholder="+7 (999) 000-00-00" readOnly />
<FormField label="Статус" value={le.status ?? ''} placeholder="active" readOnly />
<FormField label="ID аккаунта" value={data.id ?? ''} readOnly icon="lock" placeholder="ECSA-00000000" />
</div>
</ProfileSection>
</>
)
}

View File

@@ -6,6 +6,8 @@ import { ROUTES } from '@shared/config/routes'
import { Button, FormField, Notification } from '@shared/ui'
import { WalletHeader } from '@widgets/wallet-header'
import { ProfileAvatar, ProfileSection } from '@widgets/profile'
import { IndividualFields } from './IndividualFields'
import { LegalEntityFields } from './LegalEntityFields'
import styles from './ProfilePage.module.css'
export function ProfilePage() {
@@ -44,11 +46,14 @@ export function ProfilePage() {
})
}
const capitalize = (s: string) => (s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : '')
const capitalize = (s: string | null) => (s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : '')
const fullName = data
? [data.last_name, data.first_name, data.middle_name].filter(Boolean).map(capitalize).join(' ')
: ''
const isLegal = !!data && data.account_type !== 'individual'
const displayName = isLegal ? (data?.legal_entity?.name ?? '') : fullName
const userBalance =
isPortfolioLoading || !portfolio || portfolio.totalUsd == null
? '$—'
@@ -66,28 +71,24 @@ export function ProfilePage() {
<div className={styles.profileTop}>
<ProfileAvatar />
<div className={styles.userInfo}>
<span className={styles.userName}>{fullName}</span>
<span className={styles.userName}>{displayName}</span>
<span className={styles.userBalance}>{userBalance}</span>
{/* <span className={styles.userBalanceRub}>≈ 22 340,50 ₽</span> */}
</div>
</div>
<div className={styles.sections}>
<ProfileSection title="Личные данные">
<div className={styles.grid2}>
<FormField label="Полное ФИО" value={fullName} placeholder="Например: Иванов Иван Иванович" readOnly />
<FormField label="Адрес электронной почты" value={data?.email ?? ''} type="email" icon="check" placeholder="example@mail.ru" readOnly />
<FormField label="Серия и номер паспорта" value={data?.passport_data ?? ''} placeholder="0000 000000" readOnly />
<FormField label="Номер телефона" value={phone} onChange={handlePhoneChange} onBlur={handlePhoneBlur} type="tel" placeholder="+7 (999) 000-00-00" />
</div>
</ProfileSection>
<ProfileSection title="Верификация">
<div className={styles.grid2}>
<FormField label="ИНН" value={data?.inn ?? ''} readOnly icon="lock" placeholder="000000000000" />
<FormField label="ID аккаунта" value={data?.id ?? ''} readOnly icon="lock" placeholder="ECSA-00000000" />
</div>
</ProfileSection>
{data && (isLegal ? (
<LegalEntityFields data={data} />
) : (
<IndividualFields
data={data}
fullName={fullName}
phone={phone}
onPhoneChange={handlePhoneChange}
onPhoneBlur={handlePhoneBlur}
/>
))}
<ProfileSection
title="Безопасность"

View File

@@ -13,7 +13,7 @@ import { Notification } from '@shared/ui'
const TICKER_SYMBOLS = ['BTC', 'ETH', 'SOL']
const capitalize = (s: string) => (s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : '')
const capitalize = (s: string | null) => (s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : '')
function formatPrice(value: number | null | undefined): string {
if (value == null) return '$—'

File diff suppressed because one or more lines are too long