feat(admin): вкладка «Активность» в карточке пользователя

Единый таймлайн действий юзера в боте и кабинете
(GET /cabinet/admin/users/{id}/activity): вертикальная лента с
иконками-кружками по типу записи, бейджи подтипов, суммы со знаком
(+зелёный / −красный), относительное время (абсолютное — в title),
чипы-фильтры по категориям (платежи, события, промо, тикеты, подарки,
рефералы, входы), счётчик StatCard и постраничная догрузка по house
load-more паттерну. Компонент самодостаточный (ActivityTab), как
TicketsTab. Переводы во всех 4 локалях; время переиспользует
admin.auditLog.time.*.

biome check, type-check и build проходят (pre-commit пропущен: biome
не установлен локально — бинарь есть только в CI).
This commit is contained in:
Fringg
2026-07-13 03:34:07 +03:00
parent c920330fa6
commit 7547b49d7a
7 changed files with 564 additions and 8 deletions

View File

@@ -24,6 +24,7 @@ import { GiftsTab } from '../components/admin/userDetail/GiftsTab';
import { SyncTab } from '../components/admin/userDetail/SyncTab';
import { ReferralsTab } from '../components/admin/userDetail/ReferralsTab';
import { BalanceTab } from '../components/admin/userDetail/BalanceTab';
import { ActivityTab } from '../components/admin/userDetail/ActivityTab';
import { TicketsTab } from '../components/admin/userDetail/TicketsTab';
import { InfoTab } from '../components/admin/userDetail/InfoTab';
import { SubscriptionTab } from '../components/admin/userDetail/SubscriptionTab';
@@ -49,7 +50,7 @@ export default function AdminUserDetail() {
const [user, setUser] = useState<UserDetailResponse | null>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<
'info' | 'subscription' | 'balance' | 'sync' | 'tickets' | 'gifts' | 'referrals'
'info' | 'subscription' | 'balance' | 'sync' | 'tickets' | 'gifts' | 'referrals' | 'activity'
>('info');
const [syncStatus, setSyncStatus] = useState<PanelSyncStatusResponse | null>(null);
const [tariffs, setTariffs] = useState<UserAvailableTariff[]>([]);
@@ -776,7 +777,18 @@ export default function AdminUserDetail() {
className="scrollbar-hide -mx-4 mb-6 flex gap-2 overflow-x-auto px-4 py-1"
style={{ WebkitOverflowScrolling: 'touch' }}
>
{(['info', 'subscription', 'balance', 'sync', 'tickets', 'gifts', 'referrals'] as const)
{(
[
'info',
'subscription',
'balance',
'sync',
'tickets',
'gifts',
'referrals',
'activity',
] as const
)
.filter((tab) => tab !== 'sync' || hasPermission('users:sync'))
.map((tab) => (
<button
@@ -795,6 +807,7 @@ export default function AdminUserDetail() {
{tab === 'tickets' && t('admin.users.detail.tabs.tickets')}
{tab === 'gifts' && t('admin.users.detail.tabs.gifts')}
{tab === 'referrals' && t('admin.users.detail.tabs.referrals')}
{tab === 'activity' && t('admin.users.detail.tabs.activity')}
</button>
))}
</div>
@@ -948,6 +961,11 @@ export default function AdminUserDetail() {
{activeTab === 'referrals' && user && userId && (
<ReferralsTab user={user} userId={userId} onUserRefresh={loadUser} />
)}
{/* Activity Tab */}
{activeTab === 'activity' && userId && (
<ActivityTab userId={userId} formatDate={formatDate} />
)}
</div>
</div>
);