deploy
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||
import { AdminPage } from '@pages/admin'
|
||||
import { AdminOrganizationPage } from '@pages/admin-organization'
|
||||
import { AdminCreatePurchaseRequestPage } from '@pages/admin-create-purchase-request'
|
||||
import { ROUTES } from '@shared/config/routes'
|
||||
import { ScrollToTop } from './ScrollToTop'
|
||||
|
||||
@@ -12,6 +13,10 @@ export function RouterProvider() {
|
||||
{/* Admin panel — own auth gate, independent of the user auth system */}
|
||||
<Route path={ROUTES.ADMIN} element={<AdminPage />} />
|
||||
<Route path={ROUTES.ADMIN_ORGANIZATION} element={<AdminOrganizationPage />} />
|
||||
<Route
|
||||
path={ROUTES.ADMIN_ORGANIZATION_CREATE_REQUEST}
|
||||
element={<AdminCreatePurchaseRequestPage />}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
@@ -4,12 +4,14 @@ import type {
|
||||
AdminMeResponse,
|
||||
AdminRefreshResponse,
|
||||
CreateOrganizationRequest,
|
||||
CreatePurchaseRequestRequest,
|
||||
CreateWalletsResponse,
|
||||
WalletResponse,
|
||||
DocumentResponse,
|
||||
DocumentTypeSlug,
|
||||
Organization,
|
||||
OrganizationListResponse,
|
||||
PurchaseRequestResponse,
|
||||
PurchaseRequestListResponse,
|
||||
UpdateOrganizationRequest,
|
||||
} from '../model/types'
|
||||
@@ -207,6 +209,16 @@ export async function getPurchaseRequests(params: {
|
||||
return data
|
||||
}
|
||||
|
||||
export function createPurchaseRequest(
|
||||
payload: CreatePurchaseRequestRequest,
|
||||
): Promise<PurchaseRequestResponse> {
|
||||
return doAdminRequest<PurchaseRequestResponse>(
|
||||
'/v1/purchase-requests',
|
||||
{ method: 'POST', body: JSON.stringify(payload) },
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
export function updateOrganization(
|
||||
id: string,
|
||||
payload: UpdateOrganizationRequest,
|
||||
|
||||
13
src/features/admin/hooks/useCreatePurchaseRequest.ts
Normal file
13
src/features/admin/hooks/useCreatePurchaseRequest.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { createPurchaseRequest } from '../api/adminApi'
|
||||
import { PURCHASE_REQUESTS_QUERY_KEY } from './usePurchaseRequests'
|
||||
|
||||
export function useCreatePurchaseRequest(orgId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: createPurchaseRequest,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: PURCHASE_REQUESTS_QUERY_KEY(orgId) })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export {
|
||||
getDocuments,
|
||||
uploadDocument,
|
||||
getPurchaseRequests,
|
||||
createPurchaseRequest,
|
||||
refreshAdminToken,
|
||||
adminTokenStore,
|
||||
} from './api/adminApi'
|
||||
@@ -21,6 +22,7 @@ export type {
|
||||
Organization,
|
||||
OrganizationListResponse,
|
||||
CreateOrganizationRequest,
|
||||
CreatePurchaseRequestRequest,
|
||||
UpdateOrganizationRequest,
|
||||
WalletResponse,
|
||||
DocumentResponse,
|
||||
@@ -41,3 +43,4 @@ export { useOrganizationWallets, WALLETS_QUERY_KEY } from './hooks/useOrganizati
|
||||
export { useDocuments, DOCUMENTS_QUERY_KEY } from './hooks/useDocuments'
|
||||
export { useUploadDocument } from './hooks/useUploadDocument'
|
||||
export { usePurchaseRequests, PURCHASE_REQUESTS_QUERY_KEY } from './hooks/usePurchaseRequests'
|
||||
export { useCreatePurchaseRequest } from './hooks/useCreatePurchaseRequest'
|
||||
|
||||
@@ -121,6 +121,24 @@ export interface PurchaseRequestListResponse {
|
||||
total: number
|
||||
}
|
||||
|
||||
// Body of POST /v1/purchase-requests. Only organization_id and usdt_amount are
|
||||
// required; the rest are optional and sent only when filled. Monetary fields
|
||||
// accept number or string (the backend coerces to its decimal type).
|
||||
export interface CreatePurchaseRequestRequest {
|
||||
organization_id: string
|
||||
usdt_amount: number | string
|
||||
status?: string
|
||||
rub_amount?: number | string | null
|
||||
exchange_rate?: number | string | null
|
||||
service_fee_percent?: number | string | null
|
||||
comment?: string | null
|
||||
admin_comment?: string | null
|
||||
target_wallet_chain?: string | null
|
||||
target_wallet_address?: string | null
|
||||
tx_hash?: string | null
|
||||
assigned_to?: string | null
|
||||
}
|
||||
|
||||
export interface UpdateOrganizationRequest {
|
||||
name?: string | null
|
||||
short_name?: string | null
|
||||
|
||||
1
src/pages/admin-create-purchase-request/index.ts
Normal file
1
src/pages/admin-create-purchase-request/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { AdminCreatePurchaseRequestPage } from './ui/AdminCreatePurchaseRequestPage'
|
||||
@@ -0,0 +1,170 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useCreatePurchaseRequest } from '@features/admin'
|
||||
import type { CreatePurchaseRequestRequest, Organization } from '@features/admin'
|
||||
import { adminOrganizationPath } from '@shared/config/routes'
|
||||
|
||||
export const MIN_ORDER = 500_000
|
||||
|
||||
// Чем дольше клиент готов ждать, тем ниже комиссия сервиса. Скопировано с
|
||||
// страницы конвертации для юр. лиц (LegalConverterPage), чтобы условия совпадали.
|
||||
export const TERM_OPTIONS = [
|
||||
{ days: 3, rate: 0.05 },
|
||||
{ days: 4, rate: 0.04636 },
|
||||
{ days: 5, rate: 0.04273 },
|
||||
{ days: 6, rate: 0.03909 },
|
||||
{ days: 7, rate: 0.03545 },
|
||||
{ days: 8, rate: 0.03182 },
|
||||
{ days: 9, rate: 0.02818 },
|
||||
{ days: 10, rate: 0.02455 },
|
||||
{ days: 11, rate: 0.02091 },
|
||||
{ days: 12, rate: 0.01727 },
|
||||
{ days: 13, rate: 0.01364 },
|
||||
{ days: 14, rate: 0.01 },
|
||||
] as const
|
||||
|
||||
export const ru = (n: number) => n.toLocaleString('ru-RU', { maximumFractionDigits: 0 })
|
||||
|
||||
export const dayLabel = (days: number) => {
|
||||
const mod10 = days % 10
|
||||
const mod100 = days % 100
|
||||
if (mod10 === 1 && mod100 !== 11) return `${days} день`
|
||||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return `${days} дня`
|
||||
return `${days} дней`
|
||||
}
|
||||
|
||||
interface FormState {
|
||||
amount: string
|
||||
name: string
|
||||
contact: string
|
||||
status: string
|
||||
rubAmount: string
|
||||
exchangeRate: string
|
||||
serviceFee: string
|
||||
targetChain: string
|
||||
targetAddress: string
|
||||
txHash: string
|
||||
assignedTo: string
|
||||
adminComment: string
|
||||
}
|
||||
|
||||
const INITIAL: FormState = {
|
||||
amount: '',
|
||||
name: '',
|
||||
contact: '',
|
||||
status: 'submitted',
|
||||
rubAmount: '',
|
||||
exchangeRate: '',
|
||||
serviceFee: (TERM_OPTIONS[0].rate * 100).toFixed(3),
|
||||
targetChain: '',
|
||||
targetAddress: '',
|
||||
txHash: '',
|
||||
assignedTo: '',
|
||||
adminComment: '',
|
||||
}
|
||||
|
||||
export function useCreatePurchaseRequestForm(
|
||||
org: Organization | undefined,
|
||||
orgId: string,
|
||||
) {
|
||||
const navigate = useNavigate()
|
||||
const [form, setForm] = useState<FormState>(INITIAL)
|
||||
const [days, setDaysState] = useState<number>(TERM_OPTIONS[0].days)
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
const { mutate, isPending } = useCreatePurchaseRequest(orgId)
|
||||
|
||||
// Префилл имени/контакта из реквизитов организации — один раз, когда данные
|
||||
// подгрузились. Эти поля можно изменить вручную.
|
||||
const [prefilled, setPrefilled] = useState(false)
|
||||
useEffect(() => {
|
||||
if (prefilled || !org) return
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
name: org.contact_person ?? prev.name,
|
||||
contact: org.contact_phone ?? prev.contact,
|
||||
}))
|
||||
setPrefilled(true)
|
||||
}, [prefilled, org])
|
||||
|
||||
const setField = (key: keyof FormState) => (value: string) =>
|
||||
setForm((prev) => ({ ...prev, [key]: value }))
|
||||
|
||||
// Объём вводится цифрами и форматируется как в конвертере (число → usdt_amount).
|
||||
const setAmount = (value: string) => {
|
||||
const digits = value.replace(/\D/g, '')
|
||||
setForm((prev) => ({ ...prev, amount: digits ? ru(Number(digits)) : '' }))
|
||||
}
|
||||
|
||||
// Смена срока подставляет соответствующую ставку комиссии (её можно переопределить).
|
||||
const setDays = (value: number) => {
|
||||
setDaysState(value)
|
||||
const r = TERM_OPTIONS.find((o) => o.days === value)?.rate ?? TERM_OPTIONS[0].rate
|
||||
setForm((prev) => ({ ...prev, serviceFee: (r * 100).toFixed(3) }))
|
||||
}
|
||||
|
||||
const numAmount = Number(form.amount.replace(/\D/g, '')) || 0
|
||||
const belowMin = numAmount > 0 && numAmount < MIN_ORDER
|
||||
const rate = TERM_OPTIONS.find((o) => o.days === days)?.rate ?? TERM_OPTIONS[0].rate
|
||||
const commission = numAmount * rate
|
||||
const total = numAmount + commission
|
||||
const canSubmit = numAmount >= MIN_ORDER && !isPending
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (numAmount === 0 || belowMin || isPending) return
|
||||
|
||||
// Отдельных полей под имя/контакт/срок/сумму в ₽ исторически нет — собираем
|
||||
// человекочитаемый comment ровно как на странице конвертации.
|
||||
const comment = [
|
||||
form.name && `Имя: ${form.name}`,
|
||||
form.contact && `Контакт: ${form.contact}`,
|
||||
`Срок ожидания: ${dayLabel(days)}`,
|
||||
`Сумма: ${ru(numAmount)} ₽`,
|
||||
`Комиссия: ≈${ru(commission)} ₽`,
|
||||
`Итого: ≈${ru(total)} ₽`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('; ')
|
||||
|
||||
const payload: CreatePurchaseRequestRequest = {
|
||||
organization_id: orgId,
|
||||
usdt_amount: numAmount,
|
||||
status: form.status.trim() || 'submitted',
|
||||
comment,
|
||||
}
|
||||
|
||||
if (form.serviceFee.trim()) payload.service_fee_percent = form.serviceFee.trim()
|
||||
if (form.rubAmount.trim()) payload.rub_amount = form.rubAmount.replace(/\D/g, '')
|
||||
if (form.exchangeRate.trim()) payload.exchange_rate = form.exchangeRate.trim()
|
||||
if (form.targetChain.trim()) payload.target_wallet_chain = form.targetChain.trim()
|
||||
if (form.targetAddress.trim()) payload.target_wallet_address = form.targetAddress.trim()
|
||||
if (form.txHash.trim()) payload.tx_hash = form.txHash.trim()
|
||||
if (form.assignedTo.trim()) payload.assigned_to = form.assignedTo.trim()
|
||||
if (form.adminComment.trim()) payload.admin_comment = form.adminComment.trim()
|
||||
|
||||
mutate(payload, {
|
||||
onSuccess: () =>
|
||||
navigate(adminOrganizationPath(orgId), { state: { tab: 'requests' } }),
|
||||
onError: () => setError(true),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
setField,
|
||||
setAmount,
|
||||
days,
|
||||
setDays,
|
||||
numAmount,
|
||||
belowMin,
|
||||
rate,
|
||||
commission,
|
||||
total,
|
||||
canSubmit,
|
||||
isPending,
|
||||
handleSubmit,
|
||||
error,
|
||||
clearError: () => setError(false),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-deep);
|
||||
padding: 40px 48px;
|
||||
}
|
||||
|
||||
.header {
|
||||
max-width: 760px;
|
||||
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(22px, 3.2vw, 30px);
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #fff);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form {
|
||||
max-width: 760px;
|
||||
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;
|
||||
}
|
||||
|
||||
.amountField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #ff8a5a;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.summary {
|
||||
margin-top: 20px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.summaryRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.summaryLabel {
|
||||
color: var(--text-secondary, rgba(255, 255, 255, 0.6));
|
||||
}
|
||||
|
||||
.summaryValue {
|
||||
color: var(--text-primary, #fff);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.summaryRow[data-accent] .summaryValue {
|
||||
color: var(--interactive, #4a6dff);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.state {
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 16px;
|
||||
text-align: center;
|
||||
color: var(--text-secondary, rgba(255, 255, 255, 0.6));
|
||||
}
|
||||
|
||||
.actions {
|
||||
max-width: 320px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page {
|
||||
padding: 28px 20px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { useAdminAuth, useOrganization } from '@features/admin'
|
||||
import { adminOrganizationPath } from '@shared/config/routes'
|
||||
import { FormField, Notification, PrimaryButton, Select } from '@shared/ui'
|
||||
import { AdminLoginForm } from '@widgets/admin-login-form'
|
||||
import {
|
||||
TERM_OPTIONS,
|
||||
dayLabel,
|
||||
ru,
|
||||
useCreatePurchaseRequestForm,
|
||||
} from '../model/useCreatePurchaseRequestForm'
|
||||
import styles from './AdminCreatePurchaseRequestPage.module.css'
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'submitted', label: 'submitted' },
|
||||
{ value: 'quoted', label: 'quoted' },
|
||||
{ value: 'paid', label: 'paid' },
|
||||
{ value: 'processing', label: 'processing' },
|
||||
{ value: 'completed', label: 'completed' },
|
||||
{ value: 'cancelled', label: 'cancelled' },
|
||||
]
|
||||
|
||||
const CHAIN_OPTIONS = [
|
||||
{ value: '', label: '— не выбрано —' },
|
||||
{ value: 'ETH', label: 'ETH' },
|
||||
{ value: 'TRON', label: 'TRON' },
|
||||
{ value: 'BSC', label: 'BSC' },
|
||||
{ value: 'POLYGON', label: 'POLYGON' },
|
||||
]
|
||||
|
||||
export function AdminCreatePurchaseRequestPage() {
|
||||
const { isAuthenticated, isLoading: isAuthLoading } = useAdminAuth()
|
||||
const { organizationId } = useParams<{ organizationId: string }>()
|
||||
const navigate = useNavigate()
|
||||
const orgId = organizationId ?? ''
|
||||
const { data: org, isLoading, isError } = useOrganization(orgId)
|
||||
|
||||
const {
|
||||
form,
|
||||
setField,
|
||||
setAmount,
|
||||
days,
|
||||
setDays,
|
||||
numAmount,
|
||||
belowMin,
|
||||
rate,
|
||||
commission,
|
||||
total,
|
||||
canSubmit,
|
||||
isPending,
|
||||
handleSubmit,
|
||||
error,
|
||||
clearError,
|
||||
} = useCreatePurchaseRequestForm(org, orgId)
|
||||
|
||||
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(adminOrganizationPath(orgId), { state: { tab: 'requests' } })}
|
||||
>
|
||||
← Назад к организации
|
||||
</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}>
|
||||
<div className={styles.amountField}>
|
||||
<FormField
|
||||
label="Объём заявки, ₽"
|
||||
type="text"
|
||||
value={form.amount}
|
||||
onChange={setAmount}
|
||||
placeholder="от 500 000"
|
||||
required
|
||||
/>
|
||||
{belowMin && (
|
||||
<p className={styles.hint}>Минимальный объём заявки — {ru(500_000)} ₽</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Select
|
||||
id="term"
|
||||
label="Срок ожидания операции"
|
||||
value={days}
|
||||
onChange={setDays}
|
||||
options={TERM_OPTIONS.map((o) => ({
|
||||
value: o.days,
|
||||
label: `${dayLabel(o.days)} — комиссия ${(o.rate * 100).toFixed(3)} %`,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
label="Имя"
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={setField('name')}
|
||||
placeholder="Имя (опционально)"
|
||||
filter="letters"
|
||||
/>
|
||||
|
||||
<FormField
|
||||
label="Email или телефон для связи"
|
||||
type="text"
|
||||
value={form.contact}
|
||||
onChange={setField('contact')}
|
||||
placeholder="example@mail.ru / +7 900 000-00-00 (опционально)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.summary}>
|
||||
<div className={styles.summaryRow}>
|
||||
<span className={styles.summaryLabel}>Ставка комиссии</span>
|
||||
<span className={styles.summaryValue}>{(rate * 100).toFixed(3)} %</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
<span className={styles.summaryLabel}>Сумма комиссии</span>
|
||||
<span className={styles.summaryValue}>
|
||||
{numAmount > 0 ? `≈ ${ru(commission)} ₽` : '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow} data-accent>
|
||||
<span className={styles.summaryLabel}>Итого</span>
|
||||
<span className={styles.summaryValue}>
|
||||
{numAmount > 0 ? `≈ ${ru(total)} ₽` : '—'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Дополнительно (опционально)</h2>
|
||||
<div className={styles.grid}>
|
||||
<Select
|
||||
id="status"
|
||||
label="Статус"
|
||||
value={form.status}
|
||||
onChange={setField('status')}
|
||||
options={STATUS_OPTIONS}
|
||||
/>
|
||||
<FormField
|
||||
label="Комиссия, %"
|
||||
type="text"
|
||||
value={form.serviceFee}
|
||||
onChange={setField('serviceFee')}
|
||||
placeholder="напр. 5.000 (опционально)"
|
||||
/>
|
||||
<FormField
|
||||
label="Сумма, ₽"
|
||||
type="text"
|
||||
value={form.rubAmount}
|
||||
onChange={setField('rubAmount')}
|
||||
placeholder="Сумма в рублях (опционально)"
|
||||
filter="digits"
|
||||
/>
|
||||
<FormField
|
||||
label="Курс"
|
||||
type="text"
|
||||
value={form.exchangeRate}
|
||||
onChange={setField('exchangeRate')}
|
||||
placeholder="напр. 95.5 (опционально)"
|
||||
/>
|
||||
<Select
|
||||
id="chain"
|
||||
label="Сеть кошелька"
|
||||
value={form.targetChain}
|
||||
onChange={setField('targetChain')}
|
||||
options={CHAIN_OPTIONS}
|
||||
/>
|
||||
<FormField
|
||||
label="Адрес кошелька"
|
||||
type="text"
|
||||
value={form.targetAddress}
|
||||
onChange={setField('targetAddress')}
|
||||
placeholder="Адрес кошелька (опционально)"
|
||||
/>
|
||||
<FormField
|
||||
label="Хеш транзакции"
|
||||
type="text"
|
||||
value={form.txHash}
|
||||
onChange={setField('txHash')}
|
||||
placeholder="Хеш транзакции (опционально)"
|
||||
/>
|
||||
<FormField
|
||||
label="Назначить (ID менеджера)"
|
||||
type="text"
|
||||
value={form.assignedTo}
|
||||
onChange={setField('assignedTo')}
|
||||
placeholder="ID менеджера (опционально)"
|
||||
/>
|
||||
<FormField
|
||||
label="Внутренний комментарий"
|
||||
type="text"
|
||||
value={form.adminComment}
|
||||
onChange={setField('adminComment')}
|
||||
placeholder="Внутренний комментарий (опционально)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<PrimaryButton
|
||||
label={isPending ? 'Добавляем...' : 'Добавить операцию'}
|
||||
disabled={!canSubmit}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Notification
|
||||
status="error"
|
||||
message="Не удалось создать заявку"
|
||||
onClose={clearError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { useLocation, 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'
|
||||
@@ -30,9 +30,13 @@ export function AdminOrganizationPage() {
|
||||
const { isAuthenticated, isLoading: isAuthLoading } = useAdminAuth()
|
||||
const { organizationId } = useParams<{ organizationId: string }>()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { data: org, isLoading, isError } = useOrganization(organizationId)
|
||||
const [notice, setNotice] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<Tab>('info')
|
||||
// После создания заявки страница открывается сразу на вкладке «Заявки»
|
||||
// (вкладка передаётся через location.state при навигации назад).
|
||||
const initialTab = (location.state as { tab?: Tab } | null)?.tab ?? 'info'
|
||||
const [activeTab, setActiveTab] = useState<Tab>(initialTab)
|
||||
const { form, setField, handleSubmit, isSaving, error } = useOrganizationForm(
|
||||
org,
|
||||
organizationId ?? '',
|
||||
@@ -77,8 +81,8 @@ export function AdminOrganizationPage() {
|
||||
<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.ogrn} onChange={setField('ogrn')} placeholder="1027700132195" filter="digits" maxLength={13} />
|
||||
<FormField label="КПП" value={form.kpp} onChange={setField('kpp')} placeholder="770801001" filter="digits" maxLength={9} />
|
||||
<FormField label="Статус" value={form.status} onChange={setField('status')} placeholder="active" />
|
||||
</div>
|
||||
</section>
|
||||
@@ -94,8 +98,8 @@ export function AdminOrganizationPage() {
|
||||
<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" />
|
||||
<FormField label="Контактное лицо" value={form.contact_person} onChange={setField('contact_person')} placeholder="Иванов Иван Иванович" filter="letters" />
|
||||
<FormField label="Контактный телефон" type="tel" value={form.contact_phone} onChange={setField('contact_phone')} placeholder="+7 (999) 000-00-00" filter="phone" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
export const ROUTES = {
|
||||
ADMIN: '/sys-c7f29a4e-d81b-4630-ops-console',
|
||||
ADMIN_ORGANIZATION: '/sys-c7f29a4e-d81b-4630-ops-console/organizations/:organizationId',
|
||||
ADMIN_ORGANIZATION_CREATE_REQUEST:
|
||||
'/sys-c7f29a4e-d81b-4630-ops-console/organizations/:organizationId/purchase-requests/new',
|
||||
} as const
|
||||
|
||||
export const adminOrganizationPath = (id: string) =>
|
||||
`/sys-c7f29a4e-d81b-4630-ops-console/organizations/${id}`
|
||||
|
||||
export const adminOrganizationCreateRequestPath = (id: string) =>
|
||||
`/sys-c7f29a4e-d81b-4630-ops-console/organizations/${id}/purchase-requests/new`
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import styles from './FormField.module.css'
|
||||
import { applyFilter, type FieldFilter } from './filters'
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
@@ -11,9 +12,11 @@ interface Props {
|
||||
readOnly?: boolean
|
||||
required?: boolean
|
||||
icon?: 'check' | 'lock'
|
||||
filter?: FieldFilter
|
||||
maxLength?: number
|
||||
}
|
||||
|
||||
export function FormField({ label, value, placeholder, type = 'text', onChange, onBlur, readOnly, required, icon }: Props) {
|
||||
export function FormField({ label, value, placeholder, type = 'text', onChange, onBlur, readOnly, required, icon, filter, maxLength }: Props) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
|
||||
@@ -36,12 +39,13 @@ export function FormField({ label, value, placeholder, type = 'text', onChange,
|
||||
className={`${styles.input} ${isPassword ? styles.withToggle : ''} ${readOnly ? styles.readonly : ''} ${copied ? styles.copied : ''}`}
|
||||
type={inputType}
|
||||
{...(onChange
|
||||
? { value, onChange: (e) => onChange(e.target.value) }
|
||||
? { value, onChange: (e) => onChange(applyFilter(e.target.value, filter)) }
|
||||
: { defaultValue: value }
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
required={required}
|
||||
maxLength={maxLength}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{isPassword && (
|
||||
|
||||
18
src/shared/ui/FormField/filters.ts
Normal file
18
src/shared/ui/FormField/filters.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export type FieldFilter = 'digits' | 'letters' | 'phone' | 'email'
|
||||
|
||||
// Ограничивает символы, которые можно ввести в поле, в зависимости от типа данных.
|
||||
const FILTERS: Record<FieldFilter, RegExp> = {
|
||||
// Только цифры: ИНН, ОГРН, КПП, БИК, счета.
|
||||
digits: /[^0-9]/g,
|
||||
// Только буквы (рус./лат.), пробел и дефис: ФИО, наименования.
|
||||
letters: /[^a-zA-Zа-яА-ЯёЁ\s-]/g,
|
||||
// Телефон: цифры, плюс, скобки, пробел, дефис.
|
||||
phone: /[^0-9+()\s-]/g,
|
||||
// Email: латиница, цифры и допустимые символы адреса.
|
||||
email: /[^a-zA-Z0-9@._+-]/g,
|
||||
}
|
||||
|
||||
export function applyFilter(value: string, filter?: FieldFilter): string {
|
||||
if (!filter) return value
|
||||
return value.replace(FILTERS[filter], '')
|
||||
}
|
||||
101
src/shared/ui/Select/Select.module.css
Normal file
101
src/shared/ui/Select/Select.module.css
Normal file
@@ -0,0 +1,101 @@
|
||||
.field {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-family: var(--font-sans);
|
||||
text-align: left;
|
||||
padding: 0 16px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.trigger:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.trigger:focus-visible,
|
||||
.triggerOpen {
|
||||
border-color: var(--interactive);
|
||||
box-shadow: 0 0 0 3px rgba(74, 109, 255, 0.15);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.arrowOpen {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
list-style: none;
|
||||
background: var(--bg-mid);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 11px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.optionSelected {
|
||||
background: rgba(91, 61, 184, 0.35);
|
||||
}
|
||||
|
||||
.optionSelected:hover {
|
||||
background: rgba(91, 61, 184, 0.45);
|
||||
}
|
||||
92
src/shared/ui/Select/Select.tsx
Normal file
92
src/shared/ui/Select/Select.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import styles from './Select.module.css'
|
||||
|
||||
export interface SelectOption<T extends string | number = string> {
|
||||
value: T
|
||||
label: string
|
||||
}
|
||||
|
||||
interface Props<T extends string | number> {
|
||||
value: T
|
||||
options: SelectOption<T>[]
|
||||
onChange: (value: T) => void
|
||||
label?: string
|
||||
placeholder?: string
|
||||
id?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function Select<T extends string | number>({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
label,
|
||||
placeholder = 'Выберите',
|
||||
id,
|
||||
disabled,
|
||||
}: Props<T>) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const selected = options.find((o) => o.value === value)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const handlePointer = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
||||
}
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', handlePointer)
|
||||
document.addEventListener('keydown', handleKey)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handlePointer)
|
||||
document.removeEventListener('keydown', handleKey)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
<div className={styles.field} ref={ref}>
|
||||
{label && (
|
||||
<label className={styles.label} htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
id={id}
|
||||
className={`${styles.trigger} ${open ? styles.triggerOpen : ''}`}
|
||||
onClick={() => !disabled && setOpen((v) => !v)}
|
||||
disabled={disabled}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span className={selected ? styles.value : styles.placeholder}>
|
||||
{selected ? selected.label : placeholder}
|
||||
</span>
|
||||
<span className={`${styles.arrow} ${open ? styles.arrowOpen : ''}`} aria-hidden>
|
||||
▾
|
||||
</span>
|
||||
</button>
|
||||
{open && (
|
||||
<ul className={styles.dropdown} role="listbox">
|
||||
{options.map((o) => (
|
||||
<li
|
||||
key={String(o.value)}
|
||||
role="option"
|
||||
aria-selected={o.value === value}
|
||||
className={`${styles.option} ${o.value === value ? styles.optionSelected : ''}`}
|
||||
onClick={() => {
|
||||
onChange(o.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{o.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
2
src/shared/ui/Select/index.ts
Normal file
2
src/shared/ui/Select/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Select } from './Select'
|
||||
export type { SelectOption } from './Select'
|
||||
@@ -2,3 +2,5 @@ export { Button } from './Button'
|
||||
export { FormField } from './FormField'
|
||||
export { Notification } from './Notification'
|
||||
export { PrimaryButton } from './PrimaryButton'
|
||||
export { Select } from './Select'
|
||||
export type { SelectOption } from './Select'
|
||||
|
||||
@@ -42,30 +42,30 @@ export function AddLegalEntityModal({ open, onClose, onCreated }: Props) {
|
||||
<form className={styles.body} onSubmit={handleSubmit}>
|
||||
<p className={styles.groupLabel}>Обязательные поля</p>
|
||||
<div className={styles.grid}>
|
||||
<FormField label="Email" type="email" value={form.email} onChange={setField('email')} placeholder="org@mail.ru" required />
|
||||
<FormField label="Email" type="email" value={form.email} onChange={setField('email')} placeholder="org@mail.ru" filter="email" required />
|
||||
<FormField label="Пароль" type="password" value={form.password} onChange={setField('password')} placeholder="Минимум 8 символов" required />
|
||||
<FormField label="Наименование" value={form.name} onChange={setField('name')} placeholder="ООО «Ромашка»" required />
|
||||
<FormField label="ИНН" value={form.inn} onChange={setField('inn')} placeholder="10–12 цифр" required />
|
||||
<FormField label="ИНН" value={form.inn} onChange={setField('inn')} placeholder="10–12 цифр" filter="digits" maxLength={12} required />
|
||||
</div>
|
||||
|
||||
<p className={styles.groupLabel}>Дополнительные поля</p>
|
||||
<div className={styles.grid}>
|
||||
<FormField label="Краткое наименование" value={form.short_name} onChange={setField('short_name')} placeholder="Ромашка" />
|
||||
<FormField label="ОГРН" value={form.ogrn} onChange={setField('ogrn')} placeholder="—" />
|
||||
<FormField label="КПП" value={form.kpp} onChange={setField('kpp')} placeholder="—" />
|
||||
<FormField label="ОГРН" value={form.ogrn} onChange={setField('ogrn')} placeholder="—" filter="digits" maxLength={13} />
|
||||
<FormField label="КПП" value={form.kpp} onChange={setField('kpp')} placeholder="—" filter="digits" maxLength={9} />
|
||||
<FormField label="Статус" value={form.status} onChange={setField('status')} placeholder="active" />
|
||||
<FormField label="Юридический адрес" value={form.legal_address} onChange={setField('legal_address')} placeholder="—" />
|
||||
<FormField label="Фактический адрес" value={form.actual_address} onChange={setField('actual_address')} placeholder="—" />
|
||||
<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" />
|
||||
<FormField label="Контактное лицо" value={form.contact_person} onChange={setField('contact_person')} placeholder="—" filter="letters" />
|
||||
<FormField label="Контактный телефон" type="tel" value={form.contact_phone} onChange={setField('contact_phone')} placeholder="+7 (999) 000-00-00" filter="phone" />
|
||||
</div>
|
||||
|
||||
<p className={styles.groupLabel}>Банковские реквизиты</p>
|
||||
<div className={styles.grid}>
|
||||
<FormField label="Банк" value={form.bank_name} onChange={setField('bank_name')} placeholder="—" />
|
||||
<FormField label="БИК" value={form.bik} onChange={setField('bik')} placeholder="—" />
|
||||
<FormField label="Расчётный счёт" value={form.account} onChange={setField('account')} placeholder="—" />
|
||||
<FormField label="Корр. счёт" value={form.corr_account} onChange={setField('corr_account')} placeholder="—" />
|
||||
<FormField label="БИК" value={form.bik} onChange={setField('bik')} placeholder="—" filter="digits" maxLength={9} />
|
||||
<FormField label="Расчётный счёт" value={form.account} onChange={setField('account')} placeholder="—" filter="digits" maxLength={20} />
|
||||
<FormField label="Корр. счёт" value={form.corr_account} onChange={setField('corr_account')} placeholder="—" filter="digits" maxLength={20} />
|
||||
</div>
|
||||
|
||||
{error && <p className={styles.error}>{error}</p>}
|
||||
|
||||
@@ -48,3 +48,9 @@
|
||||
color: var(--text-secondary, rgba(255, 255, 255, 0.6));
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { usePurchaseRequests } from '@features/admin'
|
||||
import { adminOrganizationCreateRequestPath } from '@shared/config/routes'
|
||||
import { Button } from '@shared/ui'
|
||||
import styles from './OrganizationPurchaseRequests.module.css'
|
||||
|
||||
interface Props {
|
||||
@@ -19,6 +22,18 @@ function formatDate(value: string | null): string {
|
||||
|
||||
export function OrganizationPurchaseRequests({ orgId }: Props) {
|
||||
const { data, isLoading, isError } = usePurchaseRequests(orgId)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const createButton = (
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => navigate(adminOrganizationCreateRequestPath(orgId))}
|
||||
>
|
||||
Создать заявку
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return <div className={styles.state}>Загрузка...</div>
|
||||
@@ -29,35 +44,43 @@ export function OrganizationPurchaseRequests({ orgId }: Props) {
|
||||
}
|
||||
|
||||
if (!data || data.items.length === 0) {
|
||||
return <div className={styles.state}>Заявок пока нет</div>
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.state}>Заявок пока нет</div>
|
||||
{createButton}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.tableWrap}>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>USDT</th>
|
||||
<th>Сумма ₽</th>
|
||||
<th>Курс</th>
|
||||
<th>Статус</th>
|
||||
<th>Создана</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((req) => (
|
||||
<tr key={req.id}>
|
||||
<td className={styles.mono}>{formatAmount(req.usdt_amount, 'USDT')}</td>
|
||||
<td className={styles.mono}>{formatAmount(req.rub_amount, '₽')}</td>
|
||||
<td className={styles.mono}>{req.exchange_rate ?? '—'}</td>
|
||||
<td>
|
||||
<span className={styles.status}>{req.status}</span>
|
||||
</td>
|
||||
<td>{formatDate(req.created_at)}</td>
|
||||
<div>
|
||||
<div className={styles.tableWrap}>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>USDT</th>
|
||||
<th>Сумма ₽</th>
|
||||
<th>Курс</th>
|
||||
<th>Статус</th>
|
||||
<th>Создана</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((req) => (
|
||||
<tr key={req.id}>
|
||||
<td className={styles.mono}>{formatAmount(req.usdt_amount, 'USDT')}</td>
|
||||
<td className={styles.mono}>{formatAmount(req.rub_amount, '₽')}</td>
|
||||
<td className={styles.mono}>{req.exchange_rate ?? '—'}</td>
|
||||
<td>
|
||||
<span className={styles.status}>{req.status}</span>
|
||||
</td>
|
||||
<td>{formatDate(req.created_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{createButton}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/app.tsx","./src/app/providers/queryprovider.tsx","./src/app/providers/routerprovider.tsx","./src/app/providers/scrolltotop.tsx","./src/app/providers/index.ts","./src/features/admin/index.ts","./src/features/admin/api/adminapi.ts","./src/features/admin/hooks/useadminauth.ts","./src/features/admin/hooks/useadminlogin.ts","./src/features/admin/hooks/useadminlogout.ts","./src/features/admin/hooks/usecreateorganization.ts","./src/features/admin/hooks/usecreateorganizationwallets.ts","./src/features/admin/hooks/usedocuments.ts","./src/features/admin/hooks/useorganization.ts","./src/features/admin/hooks/useorganizationwallets.ts","./src/features/admin/hooks/useorganizations.ts","./src/features/admin/hooks/usepurchaserequests.ts","./src/features/admin/hooks/useupdateorganization.ts","./src/features/admin/hooks/useuploaddocument.ts","./src/features/admin/model/types.ts","./src/pages/admin/index.ts","./src/pages/admin/ui/adminpage.tsx","./src/pages/admin-organization/index.ts","./src/pages/admin-organization/model/useorganizationform.ts","./src/pages/admin-organization/ui/adminorganizationpage.tsx","./src/shared/config/routes.ts","./src/shared/ui/index.ts","./src/shared/ui/button/button.tsx","./src/shared/ui/button/index.ts","./src/shared/ui/formfield/formfield.tsx","./src/shared/ui/formfield/index.ts","./src/shared/ui/notification/notification.tsx","./src/shared/ui/notification/index.ts","./src/shared/ui/primarybutton/primarybutton.tsx","./src/shared/ui/primarybutton/index.ts","./src/widgets/add-legal-entity-modal/index.ts","./src/widgets/add-legal-entity-modal/model/useaddlegalentityform.ts","./src/widgets/add-legal-entity-modal/ui/addlegalentitymodal.tsx","./src/widgets/admin-login-form/index.ts","./src/widgets/admin-login-form/model/useadminloginform.ts","./src/widgets/admin-login-form/ui/adminloginform.tsx","./src/widgets/legal-entities-table/index.ts","./src/widgets/legal-entities-table/ui/legalentitiestable.tsx","./src/widgets/organization-documents/index.ts","./src/widgets/organization-documents/ui/organizationdocuments.tsx","./src/widgets/organization-purchase-requests/index.ts","./src/widgets/organization-purchase-requests/ui/organizationpurchaserequests.tsx","./src/widgets/organization-wallets/index.ts","./src/widgets/organization-wallets/ui/organizationwallets.tsx"],"version":"5.6.3"}
|
||||
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/app/app.tsx","./src/app/providers/queryprovider.tsx","./src/app/providers/routerprovider.tsx","./src/app/providers/scrolltotop.tsx","./src/app/providers/index.ts","./src/features/admin/index.ts","./src/features/admin/api/adminapi.ts","./src/features/admin/hooks/useadminauth.ts","./src/features/admin/hooks/useadminlogin.ts","./src/features/admin/hooks/useadminlogout.ts","./src/features/admin/hooks/usecreateorganization.ts","./src/features/admin/hooks/usecreateorganizationwallets.ts","./src/features/admin/hooks/usecreatepurchaserequest.ts","./src/features/admin/hooks/usedocuments.ts","./src/features/admin/hooks/useorganization.ts","./src/features/admin/hooks/useorganizationwallets.ts","./src/features/admin/hooks/useorganizations.ts","./src/features/admin/hooks/usepurchaserequests.ts","./src/features/admin/hooks/useupdateorganization.ts","./src/features/admin/hooks/useuploaddocument.ts","./src/features/admin/model/types.ts","./src/pages/admin/index.ts","./src/pages/admin/ui/adminpage.tsx","./src/pages/admin-create-purchase-request/index.ts","./src/pages/admin-create-purchase-request/model/usecreatepurchaserequestform.ts","./src/pages/admin-create-purchase-request/ui/admincreatepurchaserequestpage.tsx","./src/pages/admin-organization/index.ts","./src/pages/admin-organization/model/useorganizationform.ts","./src/pages/admin-organization/ui/adminorganizationpage.tsx","./src/shared/config/routes.ts","./src/shared/ui/index.ts","./src/shared/ui/button/button.tsx","./src/shared/ui/button/index.ts","./src/shared/ui/formfield/formfield.tsx","./src/shared/ui/formfield/filters.ts","./src/shared/ui/formfield/index.ts","./src/shared/ui/notification/notification.tsx","./src/shared/ui/notification/index.ts","./src/shared/ui/primarybutton/primarybutton.tsx","./src/shared/ui/primarybutton/index.ts","./src/shared/ui/select/select.tsx","./src/shared/ui/select/index.ts","./src/widgets/add-legal-entity-modal/index.ts","./src/widgets/add-legal-entity-modal/model/useaddlegalentityform.ts","./src/widgets/add-legal-entity-modal/ui/addlegalentitymodal.tsx","./src/widgets/admin-login-form/index.ts","./src/widgets/admin-login-form/model/useadminloginform.ts","./src/widgets/admin-login-form/ui/adminloginform.tsx","./src/widgets/legal-entities-table/index.ts","./src/widgets/legal-entities-table/ui/legalentitiestable.tsx","./src/widgets/organization-documents/index.ts","./src/widgets/organization-documents/ui/organizationdocuments.tsx","./src/widgets/organization-purchase-requests/index.ts","./src/widgets/organization-purchase-requests/ui/organizationpurchaserequests.tsx","./src/widgets/organization-wallets/index.ts","./src/widgets/organization-wallets/ui/organizationwallets.tsx"],"version":"5.6.3"}
|
||||
Reference in New Issue
Block a user