admin page
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
DocumentResponse,
|
||||
Organization,
|
||||
OrganizationListResponse,
|
||||
PurchaseRequestListResponse,
|
||||
UpdateOrganizationRequest,
|
||||
WalletResponse,
|
||||
} from '../model/types'
|
||||
@@ -120,18 +121,15 @@ export function createOrganizationWallets(id: string): Promise<WalletResponse[]>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getDocuments(orgId: string): Promise<DocumentResponse[]> {
|
||||
const data = await doAdminRequest<DocumentResponse[]>(
|
||||
export function getDocuments(orgId: string): Promise<DocumentResponse[]> {
|
||||
return doAdminRequest<DocumentResponse[]>(
|
||||
`/v1/organizations/${orgId}/documents`,
|
||||
{},
|
||||
true,
|
||||
)
|
||||
// TEMP: inspect the real backend shape (download_url presence, fields).
|
||||
console.log('[documents] list response:', data)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function uploadDocument(
|
||||
export function uploadDocument(
|
||||
orgId: string,
|
||||
documentType: string,
|
||||
file: File,
|
||||
@@ -140,27 +138,32 @@ export async function uploadDocument(
|
||||
body.append('document_type', documentType)
|
||||
body.append('file', file)
|
||||
|
||||
const data = await doAdminRequest<DocumentResponse>(
|
||||
return doAdminRequest<DocumentResponse>(
|
||||
`/v1/organizations/${orgId}/documents`,
|
||||
{ method: 'POST', body },
|
||||
true,
|
||||
)
|
||||
// TEMP: inspect the real backend shape after upload.
|
||||
console.log('[documents] upload response:', data)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getDocument(
|
||||
orgId: string,
|
||||
documentId: string,
|
||||
): Promise<DocumentResponse> {
|
||||
const data = await doAdminRequest<DocumentResponse>(
|
||||
`/v1/organizations/${orgId}/documents/${documentId}`,
|
||||
export async function getPurchaseRequests(params: {
|
||||
organizationId?: string
|
||||
status?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<PurchaseRequestListResponse> {
|
||||
const query = new URLSearchParams()
|
||||
if (params.organizationId) query.set('organization_id', params.organizationId)
|
||||
if (params.status) query.set('status', params.status)
|
||||
query.set('limit', String(params.limit ?? 50))
|
||||
query.set('offset', String(params.offset ?? 0))
|
||||
|
||||
const data = await doAdminRequest<PurchaseRequestListResponse>(
|
||||
`/v1/purchase-requests?${query.toString()}`,
|
||||
{},
|
||||
true,
|
||||
)
|
||||
// TEMP: inspect single-document shape (this is where download_url should appear).
|
||||
console.log('[documents] get-one response:', data)
|
||||
// TEMP: inspect real backend shape — especially which `status` values appear.
|
||||
console.log('[purchase-requests] list response:', data)
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { getDocument } from '../api/adminApi'
|
||||
import type { DocumentResponse } from '../model/types'
|
||||
|
||||
/**
|
||||
* Resolves a document's download URL and opens it. The list endpoint may not
|
||||
* include a fresh `download_url`, so we fall back to fetching the single
|
||||
* document (which is where the presigned URL is expected to appear).
|
||||
*/
|
||||
export function useDownloadDocument(orgId: string) {
|
||||
const [downloadingId, setDownloadingId] = useState<string | null>(null)
|
||||
|
||||
async function download(doc: DocumentResponse) {
|
||||
setDownloadingId(doc.id)
|
||||
try {
|
||||
let url = doc.download_url
|
||||
if (!url) {
|
||||
const fresh = await getDocument(orgId, doc.id)
|
||||
url = fresh.download_url
|
||||
}
|
||||
if (url) {
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
} else {
|
||||
throw new Error('Сервер не вернул ссылку для скачивания')
|
||||
}
|
||||
} finally {
|
||||
setDownloadingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
return { download, downloadingId }
|
||||
}
|
||||
15
src/features/admin/hooks/usePurchaseRequests.ts
Normal file
15
src/features/admin/hooks/usePurchaseRequests.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getPurchaseRequests } from '../api/adminApi'
|
||||
|
||||
export const PURCHASE_REQUESTS_QUERY_KEY = (orgId: string) => [
|
||||
'admin-purchase-requests',
|
||||
orgId,
|
||||
]
|
||||
|
||||
export function usePurchaseRequests(orgId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: PURCHASE_REQUESTS_QUERY_KEY(orgId ?? ''),
|
||||
queryFn: () => getPurchaseRequests({ organizationId: orgId }),
|
||||
enabled: !!orgId,
|
||||
})
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export {
|
||||
updateOrganization,
|
||||
getDocuments,
|
||||
uploadDocument,
|
||||
getDocument,
|
||||
getPurchaseRequests,
|
||||
refreshAdminToken,
|
||||
adminTokenStore,
|
||||
} from './api/adminApi'
|
||||
@@ -23,6 +23,8 @@ export type {
|
||||
UpdateOrganizationRequest,
|
||||
WalletResponse,
|
||||
DocumentResponse,
|
||||
PurchaseRequestResponse,
|
||||
PurchaseRequestListResponse,
|
||||
BankDetails,
|
||||
} from './model/types'
|
||||
export { useAdminAuth, ADMIN_AUTH_QUERY_KEY } from './hooks/useAdminAuth'
|
||||
@@ -35,4 +37,4 @@ export { useCreateOrganizationWallets } from './hooks/useCreateOrganizationWalle
|
||||
export { useUpdateOrganization } from './hooks/useUpdateOrganization'
|
||||
export { useDocuments, DOCUMENTS_QUERY_KEY } from './hooks/useDocuments'
|
||||
export { useUploadDocument } from './hooks/useUploadDocument'
|
||||
export { useDownloadDocument } from './hooks/useDownloadDocument'
|
||||
export { usePurchaseRequests, PURCHASE_REQUESTS_QUERY_KEY } from './hooks/usePurchaseRequests'
|
||||
|
||||
@@ -70,6 +70,31 @@ export interface DocumentResponse {
|
||||
download_url: string | null
|
||||
}
|
||||
|
||||
// Monetary fields are strings to preserve decimal precision — do not coerce to number.
|
||||
export interface PurchaseRequestResponse {
|
||||
id: string
|
||||
organization_id: string
|
||||
status: string
|
||||
usdt_amount: string
|
||||
rub_amount: string | null
|
||||
exchange_rate: string | null
|
||||
service_fee_percent: 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
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
completed_at: string | null
|
||||
}
|
||||
|
||||
export interface PurchaseRequestListResponse {
|
||||
items: PurchaseRequestResponse[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface UpdateOrganizationRequest {
|
||||
name?: string | null
|
||||
short_name?: string | null
|
||||
|
||||
@@ -39,9 +39,38 @@
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.documents {
|
||||
.tabs {
|
||||
max-width: 900px;
|
||||
margin: 24px auto 0;
|
||||
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 {
|
||||
|
||||
@@ -5,9 +5,18 @@ 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 { useOrganizationForm } from '../model/useOrganizationForm'
|
||||
import styles from './AdminOrganizationPage.module.css'
|
||||
|
||||
type Tab = 'info' | 'documents' | 'requests'
|
||||
|
||||
const TABS: { id: Tab; label: string }[] = [
|
||||
{ id: 'info', label: 'Общая информация' },
|
||||
{ id: 'documents', label: 'Документы' },
|
||||
{ id: 'requests', label: 'Заявки' },
|
||||
]
|
||||
|
||||
function formatDateTime(value: string | null): string {
|
||||
if (!value) return '—'
|
||||
const d = new Date(value)
|
||||
@@ -21,6 +30,7 @@ export function AdminOrganizationPage() {
|
||||
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 ?? '',
|
||||
@@ -43,32 +53,47 @@ export function AdminOrganizationPage() {
|
||||
{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')} required />
|
||||
<FormField label="Краткое наименование" value={form.short_name} onChange={setField('short_name')} />
|
||||
<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')} />
|
||||
<FormField label="КПП" value={form.kpp} onChange={setField('kpp')} />
|
||||
<FormField label="Статус" value={form.status} onChange={setField('status')} />
|
||||
<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')} />
|
||||
<FormField label="Фактический адрес" value={form.actual_address} onChange={setField('actual_address')} />
|
||||
<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')} />
|
||||
<FormField label="Контактный телефон" type="tel" value={form.contact_phone} onChange={setField('contact_phone')} />
|
||||
<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>
|
||||
|
||||
@@ -93,12 +118,18 @@ export function AdminOrganizationPage() {
|
||||
</form>
|
||||
)}
|
||||
|
||||
{org && (
|
||||
<div className={styles.documents}>
|
||||
{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"
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
}
|
||||
|
||||
.downloadBtn {
|
||||
display: inline-block;
|
||||
background: rgba(74, 109, 255, 0.12);
|
||||
border: 1px solid rgba(74, 109, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
@@ -118,17 +119,17 @@
|
||||
font-weight: 600;
|
||||
padding: 7px 14px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.downloadBtn:hover:not(:disabled) {
|
||||
.downloadBtn:hover {
|
||||
background: rgba(74, 109, 255, 0.22);
|
||||
}
|
||||
|
||||
.downloadBtn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
.muted {
|
||||
color: var(--text-secondary, rgba(255, 255, 255, 0.4));
|
||||
}
|
||||
|
||||
.state {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import {
|
||||
useDocuments,
|
||||
useUploadDocument,
|
||||
useDownloadDocument,
|
||||
} from '@features/admin'
|
||||
import type { DocumentResponse } from '@features/admin'
|
||||
import { useDocuments, useUploadDocument } from '@features/admin'
|
||||
import styles from './OrganizationDocuments.module.css'
|
||||
|
||||
interface Props {
|
||||
@@ -37,18 +32,16 @@ function extractErrorMessage(error: unknown): string {
|
||||
export function OrganizationDocuments({ orgId }: Props) {
|
||||
const { data: documents, isLoading, isError } = useDocuments(orgId)
|
||||
const upload = useUploadDocument(orgId)
|
||||
const { download, downloadingId } = useDownloadDocument(orgId)
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [documentType, setDocumentType] = useState('')
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const [downloadError, setDownloadError] = useState<string | null>(null)
|
||||
|
||||
function handleUpload(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (!file || !documentType.trim()) return
|
||||
if (!file) return
|
||||
upload.mutate(
|
||||
{ documentType: documentType.trim(), file },
|
||||
{ documentType: documentType.trim() || 'other', file },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setDocumentType('')
|
||||
@@ -59,15 +52,6 @@ export function OrganizationDocuments({ orgId }: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
async function handleDownload(doc: DocumentResponse) {
|
||||
setDownloadError(null)
|
||||
try {
|
||||
await download(doc)
|
||||
} catch (err) {
|
||||
setDownloadError(extractErrorMessage(err))
|
||||
}
|
||||
}
|
||||
|
||||
const uploadError = upload.isError ? extractErrorMessage(upload.error) : null
|
||||
|
||||
return (
|
||||
@@ -78,7 +62,7 @@ export function OrganizationDocuments({ orgId }: Props) {
|
||||
<input
|
||||
className={styles.input}
|
||||
type="text"
|
||||
placeholder="Тип документа (например, charter)"
|
||||
placeholder="Тип документа (необязательно)"
|
||||
value={documentType}
|
||||
onChange={(e) => setDocumentType(e.target.value)}
|
||||
/>
|
||||
@@ -91,14 +75,13 @@ export function OrganizationDocuments({ orgId }: Props) {
|
||||
<button
|
||||
className={styles.uploadBtn}
|
||||
type="submit"
|
||||
disabled={!file || !documentType.trim() || upload.isPending}
|
||||
disabled={!file || upload.isPending}
|
||||
>
|
||||
{upload.isPending ? 'Загрузка...' : 'Загрузить'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{uploadError && <p className={styles.error}>{uploadError}</p>}
|
||||
{downloadError && <p className={styles.error}>{downloadError}</p>}
|
||||
|
||||
{isLoading && <div className={styles.state}>Загрузка...</div>}
|
||||
{isError && <div className={styles.state}>Не удалось загрузить документы</div>}
|
||||
@@ -126,14 +109,18 @@ export function OrganizationDocuments({ orgId }: Props) {
|
||||
<td className={styles.mono}>{formatSize(doc.file_size_bytes)}</td>
|
||||
<td>{formatDate(doc.created_at)}</td>
|
||||
<td>
|
||||
<button
|
||||
className={styles.downloadBtn}
|
||||
type="button"
|
||||
onClick={() => handleDownload(doc)}
|
||||
disabled={downloadingId === doc.id}
|
||||
>
|
||||
{downloadingId === doc.id ? '...' : 'Скачать'}
|
||||
</button>
|
||||
{doc.download_url ? (
|
||||
<a
|
||||
className={styles.downloadBtn}
|
||||
href={doc.download_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Скачать
|
||||
</a>
|
||||
) : (
|
||||
<span className={styles.muted}>—</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
1
src/widgets/organization-purchase-requests/index.ts
Normal file
1
src/widgets/organization-purchase-requests/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { OrganizationPurchaseRequests } from './ui/OrganizationPurchaseRequests'
|
||||
@@ -0,0 +1,50 @@
|
||||
.tableWrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th {
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1.5px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
padding: 0 16px 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 14px 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: rgba(74, 109, 255, 0.12);
|
||||
color: #7c95ff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.state {
|
||||
padding: 32px 16px;
|
||||
text-align: center;
|
||||
color: var(--text-secondary, rgba(255, 255, 255, 0.6));
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { usePurchaseRequests } from '@features/admin'
|
||||
import styles from './OrganizationPurchaseRequests.module.css'
|
||||
|
||||
interface Props {
|
||||
orgId: string
|
||||
}
|
||||
|
||||
function formatAmount(value: string | null, suffix: string): string {
|
||||
if (!value) return '—'
|
||||
return `${value} ${suffix}`
|
||||
}
|
||||
|
||||
function formatDate(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 OrganizationPurchaseRequests({ orgId }: Props) {
|
||||
const { data, isLoading, isError } = usePurchaseRequests(orgId)
|
||||
|
||||
if (isLoading) {
|
||||
return <div className={styles.state}>Загрузка...</div>
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <div className={styles.state}>Не удалось загрузить заявки</div>
|
||||
}
|
||||
|
||||
if (!data || data.items.length === 0) {
|
||||
return <div className={styles.state}>Заявок пока нет</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>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user