feat(i18n): migrate all hardcoded Russian text to i18n

- Replace all hardcoded Russian strings with t() calls across 30+ files
- Add ~500 new translation keys to all 4 locales (ru, en, zh, fa)
- Convert module-level config objects to labelKey pattern
- Remove Russian fallbacks from t() calls (fallbackLng handles it)
- Replace DeepLinkRedirect custom i18n with standard t() calls
- Fix subscription.servers key collision (string vs object)
This commit is contained in:
c0mrade
2026-01-27 22:19:41 +03:00
parent d837f8050c
commit 9707ad97a4
34 changed files with 5275 additions and 1075 deletions

View File

@@ -1,5 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
import { useCurrency } from '../hooks/useCurrency';
import {
adminUsersApi,
@@ -132,6 +134,7 @@ interface UserRowProps {
}
function UserRow({ user, onSelect, formatAmount }: UserRowProps) {
const { t } = useTranslation();
return (
<div
onClick={() => onSelect(user)}
@@ -165,10 +168,10 @@ function UserRow({ user, onSelect, formatAmount }: UserRowProps) {
}`}
>
{user.subscription_status === 'active'
? 'Подписка'
? t('admin.users.status.subscription')
: user.subscription_status === 'trial'
? 'Триал'
: 'Истекла'}
? t('admin.users.status.trial')
: t('admin.users.status.expired')}
</span>
)}
</div>
@@ -178,7 +181,9 @@ function UserRow({ user, onSelect, formatAmount }: UserRowProps) {
<div className="shrink-0 text-right">
<div className="font-medium text-dark-100">{formatAmount(user.balance_rubles)}</div>
<div className="text-xs text-dark-500">
{user.purchase_count > 0 ? `${user.purchase_count} покупок` : 'Нет покупок'}
{user.purchase_count > 0
? t('admin.users.purchaseCount', { count: user.purchase_count })
: t('admin.users.noPurchases')}
</div>
</div>
@@ -196,7 +201,10 @@ interface UserDetailModalProps {
}
function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
const { t } = useTranslation();
const { formatWithCurrency } = useCurrency();
const localeMap: Record<string, string> = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' };
const locale = localeMap[i18n.language] || 'ru-RU';
const [user, setUser] = useState<UserDetailResponse | null>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<'info' | 'subscription' | 'balance' | 'sync'>('info');
@@ -259,7 +267,11 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
const amount = Math.abs(parseFloat(balanceAmount) * 100);
await adminUsersApi.updateBalance(userId, {
amount_kopeks: isAdd ? amount : -amount,
description: balanceDescription || (isAdd ? 'Начисление админом' : 'Списание админом'),
description:
balanceDescription ||
(isAdd
? t('admin.users.detail.balance.addByAdmin')
: t('admin.users.detail.balance.subtractByAdmin')),
});
await loadUser();
setBalanceAmount('');
@@ -299,7 +311,7 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
};
const handleBlockUser = async () => {
if (!confirm('Заблокировать пользователя?')) return;
if (!confirm(t('admin.users.confirm.block'))) return;
setActionLoading(true);
try {
await adminUsersApi.blockUser(userId);
@@ -358,7 +370,7 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
const formatDate = (date: string | null) => {
if (!date) return '-';
return new Date(date).toLocaleDateString('ru-RU', {
return new Date(date).toLocaleDateString(locale, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
@@ -414,10 +426,10 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
: 'text-dark-400 hover:text-dark-200'
}`}
>
{tab === 'info' && 'Информация'}
{tab === 'subscription' && 'Подписка'}
{tab === 'balance' && 'Баланс'}
{tab === 'sync' && 'Синхронизация'}
{tab === 'info' && t('admin.users.detail.tabs.info')}
{tab === 'subscription' && t('admin.users.detail.tabs.subscription')}
{tab === 'balance' && t('admin.users.detail.tabs.balance')}
{tab === 'sync' && t('admin.users.detail.tabs.sync')}
</button>
))}
</div>
@@ -429,7 +441,7 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
<div className="space-y-4">
{/* Status */}
<div className="flex items-center justify-between rounded-xl bg-dark-900/50 p-3">
<span className="text-dark-400">Статус</span>
<span className="text-dark-400">{t('admin.users.detail.status')}</span>
<div className="flex items-center gap-2">
<StatusBadge status={user.status} />
{user.status === 'active' ? (
@@ -438,7 +450,7 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
disabled={actionLoading}
className="rounded-lg bg-rose-500/20 px-3 py-1 text-xs text-rose-400 transition-colors hover:bg-rose-500/30"
>
Заблокировать
{t('admin.users.actions.block')}
</button>
) : user.status === 'blocked' ? (
<button
@@ -446,7 +458,7 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
disabled={actionLoading}
className="rounded-lg bg-emerald-500/20 px-3 py-1 text-xs text-emerald-400 transition-colors hover:bg-emerald-500/30"
>
Разблокировать
{t('admin.users.actions.unblock')}
</button>
) : null}
</div>
@@ -459,50 +471,68 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
<div className="text-dark-100">{user.email || '-'}</div>
</div>
<div className="rounded-xl bg-dark-900/50 p-3">
<div className="mb-1 text-xs text-dark-500">Язык</div>
<div className="mb-1 text-xs text-dark-500">
{t('admin.users.detail.language')}
</div>
<div className="text-dark-100">{user.language}</div>
</div>
<div className="rounded-xl bg-dark-900/50 p-3">
<div className="mb-1 text-xs text-dark-500">Регистрация</div>
<div className="mb-1 text-xs text-dark-500">
{t('admin.users.detail.registration')}
</div>
<div className="text-dark-100">{formatDate(user.created_at)}</div>
</div>
<div className="rounded-xl bg-dark-900/50 p-3">
<div className="mb-1 text-xs text-dark-500">Последняя активность</div>
<div className="mb-1 text-xs text-dark-500">
{t('admin.users.detail.lastActivity')}
</div>
<div className="text-dark-100">{formatDate(user.last_activity)}</div>
</div>
<div className="rounded-xl bg-dark-900/50 p-3">
<div className="mb-1 text-xs text-dark-500">Всего потрачено</div>
<div className="mb-1 text-xs text-dark-500">
{t('admin.users.detail.totalSpent')}
</div>
<div className="text-dark-100">
{formatWithCurrency(user.total_spent_kopeks / 100)}
</div>
</div>
<div className="rounded-xl bg-dark-900/50 p-3">
<div className="mb-1 text-xs text-dark-500">Покупок</div>
<div className="mb-1 text-xs text-dark-500">
{t('admin.users.detail.purchases')}
</div>
<div className="text-dark-100">{user.purchase_count}</div>
</div>
</div>
{/* Referral */}
<div className="rounded-xl bg-dark-900/50 p-3">
<div className="mb-2 text-sm font-medium text-dark-200">Реферальная программа</div>
<div className="mb-2 text-sm font-medium text-dark-200">
{t('admin.users.detail.referral.title')}
</div>
<div className="grid grid-cols-3 gap-3 text-center">
<div>
<div className="text-lg font-bold text-dark-100">
{user.referral.referrals_count}
</div>
<div className="text-xs text-dark-500">Рефералов</div>
<div className="text-xs text-dark-500">
{t('admin.users.detail.referral.referrals')}
</div>
</div>
<div>
<div className="text-lg font-bold text-dark-100">
{formatWithCurrency(user.referral.total_earnings_kopeks / 100)}
</div>
<div className="text-xs text-dark-500">Заработано</div>
<div className="text-xs text-dark-500">
{t('admin.users.detail.referral.earned')}
</div>
</div>
<div>
<div className="text-lg font-bold text-dark-100">
{user.referral.commission_percent || 0}%
</div>
<div className="text-xs text-dark-500">Комиссия</div>
<div className="text-xs text-dark-500">
{t('admin.users.detail.referral.commission')}
</div>
</div>
</div>
</div>
@@ -510,16 +540,22 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
{/* Restrictions */}
{(user.restriction_topup || user.restriction_subscription) && (
<div className="rounded-xl border border-rose-500/30 bg-rose-500/10 p-3">
<div className="mb-2 text-sm font-medium text-rose-400">Ограничения</div>
<div className="mb-2 text-sm font-medium text-rose-400">
{t('admin.users.detail.restrictions.title')}
</div>
{user.restriction_topup && (
<div className="text-xs text-rose-300"> Запрет пополнения</div>
<div className="text-xs text-rose-300">
{t('admin.users.detail.restrictions.topup')}
</div>
)}
{user.restriction_subscription && (
<div className="text-xs text-rose-300"> Запрет покупки подписки</div>
<div className="text-xs text-rose-300">
{t('admin.users.detail.restrictions.subscription')}
</div>
)}
{user.restriction_reason && (
<div className="mt-1 text-xs text-dark-400">
Причина: {user.restriction_reason}
{t('admin.users.detail.restrictions.reason')}: {user.restriction_reason}
</div>
)}
</div>
@@ -535,31 +571,42 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
{/* Current subscription */}
<div className="rounded-xl bg-dark-900/50 p-4">
<div className="mb-3 flex items-center justify-between">
<span className="font-medium text-dark-200">Текущая подписка</span>
<span className="font-medium text-dark-200">
{t('admin.users.detail.subscription.current')}
</span>
<StatusBadge status={user.subscription.status} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<div className="text-xs text-dark-500">Тариф</div>
<div className="text-xs text-dark-500">
{t('admin.users.detail.subscription.tariff')}
</div>
<div className="text-dark-100">
{user.subscription.tariff_name || 'Не указан'}
{user.subscription.tariff_name ||
t('admin.users.detail.subscription.notSpecified')}
</div>
</div>
<div>
<div className="text-xs text-dark-500">Действует до</div>
<div className="text-xs text-dark-500">
{t('admin.users.detail.subscription.validUntil')}
</div>
<div className="text-dark-100">
{formatDate(user.subscription.end_date)}
</div>
</div>
<div>
<div className="text-xs text-dark-500">Трафик</div>
<div className="text-xs text-dark-500">
{t('admin.users.detail.subscription.traffic')}
</div>
<div className="text-dark-100">
{user.subscription.traffic_used_gb.toFixed(1)} /{' '}
{user.subscription.traffic_limit_gb} ГБ
{user.subscription.traffic_limit_gb} {t('common.units.gb')}
</div>
</div>
<div>
<div className="text-xs text-dark-500">Устройств</div>
<div className="text-xs text-dark-500">
{t('admin.users.detail.subscription.devices')}
</div>
<div className="text-dark-100">{user.subscription.device_limit}</div>
</div>
</div>
@@ -567,17 +614,27 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
{/* Actions */}
<div className="rounded-xl bg-dark-900/50 p-4">
<div className="mb-3 font-medium text-dark-200">Действия</div>
<div className="mb-3 font-medium text-dark-200">
{t('admin.users.detail.subscription.actions')}
</div>
<div className="space-y-3">
<select
value={subAction}
onChange={(e) => setSubAction(e.target.value)}
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100"
>
<option value="extend">Продлить</option>
<option value="change_tariff">Сменить тариф</option>
<option value="cancel">Отменить</option>
<option value="activate">Активировать</option>
<option value="extend">
{t('admin.users.detail.subscription.extend')}
</option>
<option value="change_tariff">
{t('admin.users.detail.subscription.changeTariff')}
</option>
<option value="cancel">
{t('admin.users.detail.subscription.cancel')}
</option>
<option value="activate">
{t('admin.users.detail.subscription.activate')}
</option>
</select>
{subAction === 'extend' && (
@@ -585,7 +642,7 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
type="number"
value={subDays}
onChange={(e) => setSubDays(e.target.value)}
placeholder="Дней"
placeholder={t('admin.users.detail.subscription.days')}
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100"
/>
)}
@@ -598,10 +655,14 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
}
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100"
>
<option value="">Выберите тариф</option>
{tariffs.map((t) => (
<option key={t.id} value={t.id}>
{t.name} {!t.is_available && '(недоступен)'}
<option value="">
{t('admin.users.detail.subscription.selectTariff')}
</option>
{tariffs.map((tariffItem) => (
<option key={tariffItem.id} value={tariffItem.id}>
{tariffItem.name}{' '}
{!tariffItem.is_available &&
t('admin.users.detail.subscription.unavailable')}
</option>
))}
</select>
@@ -612,14 +673,18 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
disabled={actionLoading}
className="w-full rounded-lg bg-blue-500 py-2 text-white transition-colors hover:bg-blue-600 disabled:opacity-50"
>
{actionLoading ? 'Применение...' : 'Применить'}
{actionLoading
? t('admin.users.actions.applying')
: t('admin.users.actions.apply')}
</button>
</div>
</div>
</>
) : (
<div className="rounded-xl bg-dark-900/50 p-4">
<div className="mb-4 text-center text-dark-400">Нет активной подписки</div>
<div className="mb-4 text-center text-dark-400">
{t('admin.users.detail.subscription.noActive')}
</div>
<div className="space-y-3">
<select
value={selectedTariffId || ''}
@@ -628,10 +693,10 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
}
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100"
>
<option value="">Выберите тариф</option>
{tariffs.map((t) => (
<option key={t.id} value={t.id}>
{t.name}
<option value="">{t('admin.users.detail.subscription.selectTariff')}</option>
{tariffs.map((tariffItem) => (
<option key={tariffItem.id} value={tariffItem.id}>
{tariffItem.name}
</option>
))}
</select>
@@ -639,7 +704,7 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
type="number"
value={subDays}
onChange={(e) => setSubDays(e.target.value)}
placeholder="Дней"
placeholder={t('admin.users.detail.subscription.days')}
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100"
/>
<button
@@ -650,7 +715,9 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
disabled={actionLoading}
className="w-full rounded-lg bg-emerald-500 py-2 text-white transition-colors hover:bg-emerald-600 disabled:opacity-50"
>
{actionLoading ? 'Создание...' : 'Создать подписку'}
{actionLoading
? t('admin.users.detail.subscription.creating')
: t('admin.users.detail.subscription.create')}
</button>
</div>
</div>
@@ -663,7 +730,9 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
<div className="space-y-4">
{/* Current balance */}
<div className="rounded-xl border border-blue-500/30 bg-gradient-to-r from-blue-500/20 to-purple-500/20 p-4">
<div className="mb-1 text-sm text-dark-400">Текущий баланс</div>
<div className="mb-1 text-sm text-dark-400">
{t('admin.users.detail.balance.current')}
</div>
<div className="text-3xl font-bold text-dark-100">
{formatWithCurrency(user.balance_rubles)}
</div>
@@ -675,14 +744,14 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
type="number"
value={balanceAmount}
onChange={(e) => setBalanceAmount(e.target.value)}
placeholder="Сумма в рублях"
placeholder={t('admin.users.detail.balance.amountPlaceholder')}
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100"
/>
<input
type="text"
value={balanceDescription}
onChange={(e) => setBalanceDescription(e.target.value)}
placeholder="Описание (опционально)"
placeholder={t('admin.users.detail.balance.descriptionPlaceholder')}
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100"
/>
<div className="flex gap-2">
@@ -691,14 +760,14 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
disabled={actionLoading || !balanceAmount}
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-emerald-500 py-2 text-white transition-colors hover:bg-emerald-600 disabled:opacity-50"
>
<PlusIcon /> Начислить
<PlusIcon /> {t('admin.users.detail.balance.add')}
</button>
<button
onClick={() => handleUpdateBalance(false)}
disabled={actionLoading || !balanceAmount}
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-rose-500 py-2 text-white transition-colors hover:bg-rose-600 disabled:opacity-50"
>
<MinusIcon /> Списать
<MinusIcon /> {t('admin.users.detail.balance.subtract')}
</button>
</div>
</div>
@@ -706,7 +775,9 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
{/* Recent transactions */}
{user.recent_transactions.length > 0 && (
<div className="rounded-xl bg-dark-900/50 p-4">
<div className="mb-3 font-medium text-dark-200">Последние транзакции</div>
<div className="mb-3 font-medium text-dark-200">
{t('admin.users.detail.balance.recentTransactions')}
</div>
<div className="max-h-48 space-y-2 overflow-y-auto">
{user.recent_transactions.map((tx) => (
<div
@@ -741,9 +812,13 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
>
<div className="mb-3 flex items-center gap-2">
{syncStatus.has_differences ? (
<span className="font-medium text-amber-400">Есть расхождения</span>
<span className="font-medium text-amber-400">
{t('admin.users.detail.sync.hasDifferences')}
</span>
) : (
<span className="font-medium text-emerald-400">Синхронизировано</span>
<span className="font-medium text-emerald-400">
{t('admin.users.detail.sync.synced')}
</span>
)}
</div>
@@ -759,36 +834,48 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<div className="mb-2 text-xs text-dark-500">Бот</div>
<div className="mb-2 text-xs text-dark-500">
{t('admin.users.detail.sync.bot')}
</div>
<div className="space-y-1">
<div className="flex justify-between">
<span className="text-dark-400">Статус:</span>
<span className="text-dark-400">
{t('admin.users.detail.sync.statusLabel')}:
</span>
<span className="text-dark-200">
{syncStatus.bot_subscription_status || '-'}
</span>
</div>
<div className="flex justify-between">
<span className="text-dark-400">До:</span>
<span className="text-dark-400">
{t('admin.users.detail.sync.until')}:
</span>
<span className="text-dark-200">
{syncStatus.bot_subscription_end_date
? new Date(syncStatus.bot_subscription_end_date).toLocaleDateString(
'ru-RU',
locale,
)
: '-'}
</span>
</div>
<div className="flex justify-between">
<span className="text-dark-400">Трафик:</span>
<span className="text-dark-400">
{t('admin.users.detail.sync.traffic')}:
</span>
<span className="text-dark-200">
{syncStatus.bot_traffic_used_gb.toFixed(2)} ГБ
{syncStatus.bot_traffic_used_gb.toFixed(2)} {t('common.units.gb')}
</span>
</div>
<div className="flex justify-between">
<span className="text-dark-400">Устройства:</span>
<span className="text-dark-400">
{t('admin.users.detail.sync.devices')}:
</span>
<span className="text-dark-200">{syncStatus.bot_device_limit}</span>
</div>
<div className="flex justify-between">
<span className="text-dark-400">Сквады:</span>
<span className="text-dark-400">
{t('admin.users.detail.sync.squads')}:
</span>
<span className="text-dark-200">
{syncStatus.bot_squads?.length || 0}
</span>
@@ -796,32 +883,44 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
</div>
</div>
<div>
<div className="mb-2 text-xs text-dark-500">Панель</div>
<div className="mb-2 text-xs text-dark-500">
{t('admin.users.detail.sync.panel')}
</div>
<div className="space-y-1">
<div className="flex justify-between">
<span className="text-dark-400">Статус:</span>
<span className="text-dark-400">
{t('admin.users.detail.sync.statusLabel')}:
</span>
<span className="text-dark-200">{syncStatus.panel_status || '-'}</span>
</div>
<div className="flex justify-between">
<span className="text-dark-400">До:</span>
<span className="text-dark-400">
{t('admin.users.detail.sync.until')}:
</span>
<span className="text-dark-200">
{syncStatus.panel_expire_at
? new Date(syncStatus.panel_expire_at).toLocaleDateString('ru-RU')
? new Date(syncStatus.panel_expire_at).toLocaleDateString(locale)
: '-'}
</span>
</div>
<div className="flex justify-between">
<span className="text-dark-400">Трафик:</span>
<span className="text-dark-400">
{t('admin.users.detail.sync.traffic')}:
</span>
<span className="text-dark-200">
{syncStatus.panel_traffic_used_gb.toFixed(2)} ГБ
{syncStatus.panel_traffic_used_gb.toFixed(2)} {t('common.units.gb')}
</span>
</div>
<div className="flex justify-between">
<span className="text-dark-400">Устройства:</span>
<span className="text-dark-400">
{t('admin.users.detail.sync.devices')}:
</span>
<span className="text-dark-200">{syncStatus.panel_device_limit}</span>
</div>
<div className="flex justify-between">
<span className="text-dark-400">Сквады:</span>
<span className="text-dark-400">
{t('admin.users.detail.sync.squads')}:
</span>
<span className="text-dark-200">
{syncStatus.panel_squads?.length || 0}
</span>
@@ -836,7 +935,9 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
<div className="rounded-xl bg-dark-900/50 p-4">
<div className="mb-1 text-sm text-dark-400">Remnawave UUID</div>
<div className="break-all font-mono text-sm text-dark-100">
{syncStatus?.remnawave_uuid || user.remnawave_uuid || 'Не привязан'}
{syncStatus?.remnawave_uuid ||
user.remnawave_uuid ||
t('admin.users.detail.sync.notLinked')}
</div>
</div>
@@ -848,7 +949,7 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
className="flex flex-1 items-center justify-center gap-2 rounded-xl border border-blue-500/30 bg-blue-500/20 py-3 text-blue-400 transition-colors hover:bg-blue-500/30 disabled:opacity-50"
>
<SyncIcon className={actionLoading ? 'animate-spin' : ''} />
Из панели в бота
{t('admin.users.detail.sync.fromPanel')}
</button>
<button
onClick={handleSyncToPanel}
@@ -856,7 +957,7 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
className="flex flex-1 items-center justify-center gap-2 rounded-xl border border-purple-500/30 bg-purple-500/20 py-3 text-purple-400 transition-colors hover:bg-purple-500/30 disabled:opacity-50"
>
<SyncIcon className={actionLoading ? 'animate-spin' : ''} />
Из бота в панель
{t('admin.users.detail.sync.toPanel')}
</button>
</div>
</div>
@@ -870,6 +971,7 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) {
// ============ Main Page ============
export default function AdminUsers() {
const { t } = useTranslation();
const { formatWithCurrency } = useCurrency();
const [users, setUsers] = useState<UserListItem[]>([]);
@@ -941,8 +1043,8 @@ export default function AdminUsers() {
<ChevronLeftIcon />
</Link>
<div>
<h1 className="text-xl font-bold text-dark-100">Пользователи</h1>
<p className="text-sm text-dark-400">Управление пользователями бота</p>
<h1 className="text-xl font-bold text-dark-100">{t('admin.users.title')}</h1>
<p className="text-sm text-dark-400">{t('admin.users.subtitle')}</p>
</div>
</div>
<button
@@ -959,15 +1061,27 @@ export default function AdminUsers() {
{/* Stats */}
{stats && (
<div className="mb-6 grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
<StatCard title="Всего" value={stats.total_users} color="blue" />
<StatCard title="Активных" value={stats.active_users} color="green" />
<StatCard title={t('admin.users.stats.total')} value={stats.total_users} color="blue" />
<StatCard
title="С подпиской"
title={t('admin.users.stats.active')}
value={stats.active_users}
color="green"
/>
<StatCard
title={t('admin.users.stats.withSubscription')}
value={stats.users_with_active_subscription}
color="purple"
/>
<StatCard title="Новых сегодня" value={stats.new_today} color="yellow" />
<StatCard title="Заблокировано" value={stats.blocked_users} color="red" />
<StatCard
title={t('admin.users.stats.newToday')}
value={stats.new_today}
color="yellow"
/>
<StatCard
title={t('admin.users.stats.blocked')}
value={stats.blocked_users}
color="red"
/>
</div>
)}
@@ -979,7 +1093,7 @@ export default function AdminUsers() {
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Поиск по ID, имени, username..."
placeholder={t('admin.users.search')}
className="w-full rounded-xl border border-dark-700 bg-dark-800 py-2 pl-10 pr-4 text-dark-100 placeholder-dark-500 focus:border-dark-600 focus:outline-none"
/>
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-dark-500">
@@ -995,10 +1109,10 @@ export default function AdminUsers() {
}}
className="rounded-xl border border-dark-700 bg-dark-800 px-3 py-2 text-dark-100"
>
<option value="">Все статусы</option>
<option value="active">Активные</option>
<option value="blocked">Заблокированные</option>
<option value="deleted">Удалённые</option>
<option value="">{t('admin.users.filters.allStatuses')}</option>
<option value="active">{t('admin.users.status.active')}</option>
<option value="blocked">{t('admin.users.status.blocked')}</option>
<option value="deleted">{t('admin.users.status.deleted')}</option>
</select>
<select
value={sortBy}
@@ -1008,10 +1122,10 @@ export default function AdminUsers() {
}}
className="rounded-xl border border-dark-700 bg-dark-800 px-3 py-2 text-dark-100"
>
<option value="created_at">По дате</option>
<option value="balance">По балансу</option>
<option value="last_activity">По активности</option>
<option value="total_spent">По расходам</option>
<option value="created_at">{t('admin.users.filters.byDate')}</option>
<option value="balance">{t('admin.users.filters.byBalance')}</option>
<option value="last_activity">{t('admin.users.filters.byActivity')}</option>
<option value="total_spent">{t('admin.users.filters.bySpent')}</option>
</select>
</div>
@@ -1022,7 +1136,7 @@ export default function AdminUsers() {
<div className="h-8 w-8 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
</div>
) : users.length === 0 ? (
<div className="py-12 text-center text-dark-400">Пользователи не найдены</div>
<div className="py-12 text-center text-dark-400">{t('admin.users.noData')}</div>
) : (
users.map((user) => (
<UserRow
@@ -1039,7 +1153,11 @@ export default function AdminUsers() {
{totalPages > 1 && (
<div className="flex items-center justify-between">
<div className="text-sm text-dark-400">
Показано {offset + 1}-{Math.min(offset + limit, total)} из {total}
{t('admin.users.pagination.showing', {
from: offset + 1,
to: Math.min(offset + limit, total),
total,
})}
</div>
<div className="flex items-center gap-2">
<button