remove admin

This commit is contained in:
2026-06-10 17:24:36 +03:00
parent 266dea2c86
commit 6c4f9a97a6
45 changed files with 1 additions and 2234 deletions

View File

@@ -1 +0,0 @@
export { AdminOrganizationPage } from './ui/AdminOrganizationPage'

View File

@@ -1,90 +0,0 @@
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
}
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 ?? '',
}
}
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: '',
},
)
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()
const trimmedOrNull = (v: string) => (v.trim() ? v.trim() : null)
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),
}
mutation.mutate(payload, { onSuccess: () => onSaved?.() })
}
const error = mutation.isError ? extractErrorMessage(mutation.error) : null
return {
form,
setField,
handleSubmit,
isSaving: mutation.isPending,
error,
}
}

View File

@@ -1,154 +0,0 @@
.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;
}
.tabs {
max-width: 900px;
margin: 0 auto 24px;
display: flex;
gap: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.tab {
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--text-secondary, rgba(255, 255, 255, 0.6));
font-size: 15px;
font-weight: 600;
padding: 12px 16px;
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
}
.tab:hover {
color: var(--text-primary, #fff);
}
.tabActive {
color: var(--text-primary, #fff);
border-bottom-color: var(--interactive, #4a6dff);
}
.tabPanel {
max-width: 900px;
margin: 0 auto;
}
.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;
}
}

View File

@@ -1,150 +0,0 @@
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 { OrganizationDocuments } from '@widgets/organization-documents'
import { OrganizationPurchaseRequests } from '@widgets/organization-purchase-requests'
import { OrganizationWallets } from '@widgets/organization-wallets'
import { useOrganizationForm } from '../model/useOrganizationForm'
import styles from './AdminOrganizationPage.module.css'
type Tab = 'info' | 'wallets' | 'documents' | 'requests'
const TABS: { id: Tab; label: string }[] = [
{ id: 'info', label: 'Общая информация' },
{ id: 'wallets', label: 'Кошельки' },
{ id: 'documents', label: 'Документы' },
{ id: 'requests', label: 'Заявки' },
]
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 [activeTab, setActiveTab] = useState<Tab>('info')
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 && (
<div className={styles.tabs}>
{TABS.map((tab) => (
<button
key={tab.id}
type="button"
className={`${styles.tab} ${activeTab === tab.id ? styles.tabActive : ''}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>
)}
{org && activeTab === 'info' && (
<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')} placeholder="ООО «Ромашка»" required />
<FormField label="Краткое наименование" value={form.short_name} onChange={setField('short_name')} placeholder="Ромашка" />
<FormField label="ИНН" value={org.inn} readOnly icon="lock" />
<FormField label="ОГРН" value={form.ogrn} onChange={setField('ogrn')} placeholder="1027700132195" />
<FormField label="КПП" value={form.kpp} onChange={setField('kpp')} placeholder="770801001" />
<FormField label="Статус" value={form.status} onChange={setField('status')} placeholder="active" />
</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')} placeholder="г. Москва, ул. Тверская, д. 1" />
<FormField label="Фактический адрес" value={form.actual_address} onChange={setField('actual_address')} placeholder="г. Москва, ул. Тверская, д. 1" />
</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')} placeholder="Иванов Иван Иванович" />
<FormField label="Контактный телефон" type="tel" value={form.contact_phone} onChange={setField('contact_phone')} placeholder="+7 (999) 000-00-00" />
</div>
</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>
)}
{org && activeTab === 'wallets' && (
<div className={styles.tabPanel}>
<OrganizationWallets orgId={org.id} />
</div>
)}
{org && activeTab === 'documents' && (
<div className={styles.tabPanel}>
<OrganizationDocuments orgId={org.id} />
</div>
)}
{org && activeTab === 'requests' && (
<div className={styles.tabPanel}>
<OrganizationPurchaseRequests orgId={org.id} />
</div>
)}
{notice && (
<Notification
status="success"
message="Изменения сохранены"
onClose={() => setNotice(false)}
/>
)}
</div>
)
}

View File

@@ -1 +0,0 @@
export { AdminPage } from './ui/AdminPage'

View File

@@ -1,84 +0,0 @@
.page {
min-height: 100vh;
background: var(--bg-deep);
padding: 40px 48px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1200px;
margin: 0 auto 36px;
}
.greeting {
font-size: clamp(28px, 4vw, 40px);
font-weight: 700;
color: var(--text-primary, #fff);
margin: 0;
}
.logout {
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
color: var(--text-secondary, rgba(255, 255, 255, 0.7));
border-radius: 10px;
height: 40px;
padding: 0 18px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.logout:hover {
background: rgba(255, 90, 90, 0.12);
border-color: rgba(255, 90, 90, 0.3);
color: #ff5a5a;
}
.content {
max-width: 1200px;
margin: 0 auto;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.sectionTitle {
font-size: 20px;
font-weight: 700;
color: var(--text-primary, #fff);
margin: 0;
}
.addBtn {
background: linear-gradient(135deg, var(--grad-edge, #4a6dff), var(--grad-center, #6f4aff));
border: none;
color: #fff;
border-radius: 12px;
height: 44px;
padding: 0 20px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: filter 0.2s, box-shadow 0.2s;
}
.addBtn:hover {
filter: brightness(1.12);
box-shadow: 0 6px 20px rgba(74, 109, 255, 0.35);
}
@media (max-width: 768px) {
.page {
padding: 28px 20px;
}
}

View File

@@ -1,77 +0,0 @@
import { useState } from 'react'
import { useAdminAuth, useAdminLogout, useCreateOrganizationWallets } from '@features/admin'
import type { Organization } from '@features/admin'
import { Notification } from '@shared/ui'
import { AdminLoginForm } from '@widgets/admin-login-form'
import { LegalEntitiesTable } from '@widgets/legal-entities-table'
import { AddLegalEntityModal } from '@widgets/add-legal-entity-modal'
import styles from './AdminPage.module.css'
type NotificationState = { message: string; status: 'success' | 'error' | 'warning' }
export function AdminPage() {
const { isAuthenticated, isLoading } = useAdminAuth()
const logout = useAdminLogout()
const createWallets = useCreateOrganizationWallets()
const [modalOpen, setModalOpen] = useState(false)
const [notification, setNotification] = useState<NotificationState | null>(null)
// After a legal entity is created we immediately provision its wallets.
// The page stays mounted (unlike the modal), so these mutate callbacks fire reliably.
function handleCreated(organization: Organization) {
setNotification({ status: 'success', message: 'Юридическое лицо добавлено' })
createWallets.mutate(organization.id, {
onSuccess: (result) => {
setNotification({
status: 'success',
message: `Кошельки созданы (${result.wallets.length})`,
})
},
onError: () => {
setNotification({
status: 'warning',
message: 'Юридическое лицо создано, но кошельки создать не удалось',
})
},
})
}
if (isLoading) return null
if (!isAuthenticated) return <AdminLoginForm />
return (
<div className={styles.page}>
<header className={styles.header}>
<h1 className={styles.greeting}>Привет, Марк!</h1>
<button className={styles.logout} type="button" onClick={() => logout.mutate()}>
Выйти
</button>
</header>
<section className={styles.content}>
<div className={styles.toolbar}>
<h2 className={styles.sectionTitle}>Юридические лица</h2>
<button className={styles.addBtn} type="button" onClick={() => setModalOpen(true)}>
+ Добавить юридическое лицо
</button>
</div>
<LegalEntitiesTable />
</section>
<AddLegalEntityModal
open={modalOpen}
onClose={() => setModalOpen(false)}
onCreated={handleCreated}
/>
{notification && (
<Notification
status={notification.status}
message={notification.message}
onClose={() => setNotification(null)}
/>
)}
</div>
)
}