diff --git a/src/components/admin/userDetail/GiftsTab.tsx b/src/components/admin/userDetail/GiftsTab.tsx new file mode 100644 index 0000000..2247089 --- /dev/null +++ b/src/components/admin/userDetail/GiftsTab.tsx @@ -0,0 +1,301 @@ +import { useTranslation } from 'react-i18next'; +import { useCurrency } from '../../../hooks/useCurrency'; +import type { AdminUserGiftItem, AdminUserGiftsResponse } from '../../../api/adminUsers'; + +// ────────────────────────────────────────────────────────────────── +// Status badge +// ────────────────────────────────────────────────────────────────── + +function GiftStatusBadge({ status }: { status: string }) { + const { t } = useTranslation(); + const styles: Record = { + pending: 'bg-warning-500/20 text-warning-400 border-warning-500/30', + paid: 'bg-accent-500/20 text-accent-400 border-accent-500/30', + delivered: 'bg-success-500/20 text-success-400 border-success-500/30', + pending_activation: 'bg-accent-500/20 text-accent-400 border-accent-500/30', + failed: 'bg-error-500/20 text-error-400 border-error-500/30', + expired: 'bg-dark-600 text-dark-400 border-dark-500', + }; + const fallback = 'bg-dark-600 text-dark-400 border-dark-500'; + + const label = t(`admin.users.detail.gifts.status.${status}`, { defaultValue: '' }) || status; + + return ( + + {label} + + ); +} + +// ────────────────────────────────────────────────────────────────── +// Single gift card +// ────────────────────────────────────────────────────────────────── + +function GiftCard({ + gift, + direction, + locale, + onNavigateToUser, +}: { + gift: AdminUserGiftItem; + direction: 'sent' | 'received'; + locale: string; + onNavigateToUser: (userId: number) => void; +}) { + const { t } = useTranslation(); + const { formatWithCurrency } = useCurrency(); + const isSent = direction === 'sent'; + + const otherPartyLabel = isSent + ? t('admin.users.detail.gifts.recipient') + : t('admin.users.detail.gifts.sender'); + const otherPartyName = isSent + ? gift.receiver_username + ? `@${gift.receiver_username}` + : gift.gift_recipient_value || t('admin.users.detail.gifts.codeOnly') + : gift.buyer_username + ? `@${gift.buyer_username}` + : gift.buyer_full_name || t('admin.users.detail.gifts.unknownUser'); + const otherPartyId = isSent ? gift.receiver_user_id : gift.buyer_user_id; + + const dateOpts: Intl.DateTimeFormatOptions = { + day: '2-digit', + month: '2-digit', + year: '2-digit', + hour: '2-digit', + minute: '2-digit', + }; + + return ( +
+ {/* Header */} +
+
+
+ + + +
+
+
{gift.tariff_name || '—'}
+
+ {gift.period_days} {t('admin.users.detail.gifts.days')} · {gift.device_limit}{' '} + {t('admin.users.detail.gifts.devices')} +
+
+
+ +
+ + {/* Details grid */} +
+
+ {otherPartyLabel}:{' '} + {otherPartyName} + {otherPartyId && ( + + )} +
+
+ {t('admin.users.detail.gifts.amount')}:{' '} + {formatWithCurrency(gift.amount_kopeks / 100)} +
+
+ {t('admin.users.detail.gifts.paymentMethod')}:{' '} + {gift.payment_method || '—'} +
+
+ {t('admin.users.detail.gifts.createdAt')}:{' '} + + {gift.created_at ? new Date(gift.created_at).toLocaleString(locale, dateOpts) : '—'} + +
+ {gift.paid_at && ( +
+ {t('admin.users.detail.gifts.paidAt')}:{' '} + + {new Date(gift.paid_at).toLocaleString(locale, dateOpts)} + +
+ )} + {gift.delivered_at && ( +
+ {t('admin.users.detail.gifts.deliveredAt')}:{' '} + + {new Date(gift.delivered_at).toLocaleString(locale, dateOpts)} + +
+ )} +
+ + {/* Gift message */} + {gift.gift_message && ( +
+ “{gift.gift_message}” +
+ )} + + {/* Token */} +
GIFT-{gift.token}
+
+ ); +} + +// ────────────────────────────────────────────────────────────────── +// Tab — gifts list with sent + received sections +// ────────────────────────────────────────────────────────────────── + +export interface GiftsTabProps { + giftsLoading: boolean; + giftsData: AdminUserGiftsResponse | null; + locale: string; + onNavigateToUser: (userId: number) => void; +} + +export function GiftsTab({ giftsLoading, giftsData, locale, onNavigateToUser }: GiftsTabProps) { + const { t } = useTranslation(); + + if (giftsLoading) { + return ( +
+
+
+ ); + } + + if (!giftsData || (giftsData.sent.length === 0 && giftsData.received.length === 0)) { + return ( +
+ + + +

{t('admin.users.detail.gifts.noGifts')}

+
+ ); + } + + return ( +
+ {/* Summary counters */} +
+
+
+ {t('admin.users.detail.gifts.totalSent')} +
+
{giftsData.sent_total}
+
+
+
+ {t('admin.users.detail.gifts.totalReceived')} +
+
{giftsData.received_total}
+
+
+ + {/* Sent Gifts */} +
+

+ + + + {t('admin.users.detail.gifts.sentTitle')} + ({giftsData.sent_total}) +

+ {giftsData.sent.length === 0 ? ( +
+ {t('admin.users.detail.gifts.noSent')} +
+ ) : ( +
+ {giftsData.sent.map((gift) => ( + + ))} +
+ )} +
+ + {/* Received Gifts */} +
+

+ + + + {t('admin.users.detail.gifts.receivedTitle')} + ({giftsData.received_total}) +

+ {giftsData.received.length === 0 ? ( +
+ {t('admin.users.detail.gifts.noReceived')} +
+ ) : ( +
+ {giftsData.received.map((gift) => ( + + ))} +
+ )} +
+
+ ); +} diff --git a/src/components/admin/userDetail/SyncTab.tsx b/src/components/admin/userDetail/SyncTab.tsx new file mode 100644 index 0000000..c543966 --- /dev/null +++ b/src/components/admin/userDetail/SyncTab.tsx @@ -0,0 +1,207 @@ +import { useTranslation } from 'react-i18next'; +import type { + UserDetailResponse, + UserSubscriptionInfo, + PanelSyncStatusResponse, +} from '../../../api/adminUsers'; + +// ────────────────────────────────────────────────────────────────── +// Icons (sync-tab-local — not used outside this view) +// ────────────────────────────────────────────────────────────────── + +const ArrowDownIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( + + + +); + +const ArrowUpIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( + + + +); + +// ────────────────────────────────────────────────────────────────── +// Sync tab — compares bot DB vs panel data, offers a 2-way push +// ────────────────────────────────────────────────────────────────── + +export interface SyncTabProps { + user: UserDetailResponse; + syncStatus: PanelSyncStatusResponse | null; + userSubscriptions: UserSubscriptionInfo[]; + activeSubscriptionId: number | null; + onActiveSubscriptionChange: (id: number | null) => void; + actionLoading: boolean; + onSyncFromPanel: () => void; + onSyncToPanel: () => void; + locale: string; +} + +export function SyncTab({ + user, + syncStatus, + userSubscriptions, + activeSubscriptionId, + onActiveSubscriptionChange, + actionLoading, + onSyncFromPanel, + onSyncToPanel, + locale, +}: SyncTabProps) { + const { t } = useTranslation(); + + return ( +
+ {/* Subscription selector for multi-tariff */} + {userSubscriptions.length > 1 && ( +
+
+ {t('admin.users.detail.sync.selectSubscription')} +
+ +
+ )} + + {/* Sync status */} + {syncStatus && ( +
+
+ {syncStatus.has_differences ? ( + + {t('admin.users.detail.sync.hasDifferences')} + + ) : ( + + {t('admin.users.detail.sync.synced')} + + )} +
+ + {syncStatus.differences.length > 0 && ( +
+ {syncStatus.differences.map((diff, i) => ( +
+ • {diff} +
+ ))} +
+ )} + +
+
+
{t('admin.users.detail.sync.bot')}
+
+
+ {t('admin.users.detail.sync.statusLabel')}: + {syncStatus.bot_subscription_status || '-'} +
+
+ {t('admin.users.detail.sync.until')}: + + {syncStatus.bot_subscription_end_date + ? new Date(syncStatus.bot_subscription_end_date).toLocaleDateString(locale) + : '-'} + +
+
+ {t('admin.users.detail.sync.traffic')}: + + {syncStatus.bot_traffic_used_gb.toFixed(2)} {t('common.units.gb')} + +
+
+ {t('admin.users.detail.sync.devices')}: + {syncStatus.bot_device_limit} +
+
+ {t('admin.users.detail.sync.squads')}: + {syncStatus.bot_squads?.length || 0} +
+
+
+
+
{t('admin.users.detail.sync.panel')}
+
+
+ {t('admin.users.detail.sync.statusLabel')}: + {syncStatus.panel_status || '-'} +
+
+ {t('admin.users.detail.sync.until')}: + + {syncStatus.panel_expire_at + ? new Date(syncStatus.panel_expire_at).toLocaleDateString(locale) + : '-'} + +
+
+ {t('admin.users.detail.sync.traffic')}: + + {syncStatus.panel_traffic_used_gb.toFixed(2)} {t('common.units.gb')} + +
+
+ {t('admin.users.detail.sync.devices')}: + {syncStatus.panel_device_limit} +
+
+ {t('admin.users.detail.sync.squads')}: + {syncStatus.panel_squads?.length || 0} +
+
+
+
+
+ )} + + {/* UUID info */} +
+ {syncStatus?.subscription_tariff_name && ( +
{syncStatus.subscription_tariff_name}
+ )} +
Remnawave UUID
+
+ {syncStatus?.remnawave_uuid || + user.remnawave_uuid || + t('admin.users.detail.sync.notLinked')} +
+
+ + {/* Sync buttons */} +
+ + +
+
+ ); +} diff --git a/src/pages/AdminUserDetail.tsx b/src/pages/AdminUserDetail.tsx index 226c6e7..5c72d18 100644 --- a/src/pages/AdminUserDetail.tsx +++ b/src/pages/AdminUserDetail.tsx @@ -23,6 +23,8 @@ import { adminApi, type AdminTicket, type AdminTicketDetail } from '../api/admin import { promocodesApi, type PromoGroup } from '../api/promocodes'; import { promoOffersApi } from '../api/promoOffers'; import { AdminBackButton } from '../components/admin'; +import { GiftsTab } from '../components/admin/userDetail/GiftsTab'; +import { SyncTab } from '../components/admin/userDetail/SyncTab'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; import { usePermissionStore } from '../store/permissions'; import { MessageMediaGrid } from '../components/tickets/MessageMediaGrid'; @@ -60,17 +62,7 @@ const MinusIcon = () => ( ); -const ArrowDownIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( - - - -); - -const ArrowUpIcon = ({ className = 'w-5 h-5' }: { className?: string }) => ( - - - -); +// (ArrowDownIcon / ArrowUpIcon moved to components/admin/userDetail/SyncTab.tsx) const TelegramIcon = () => ( @@ -98,155 +90,7 @@ function StatusBadge({ status }: { status: string }) { ); } -function GiftStatusBadge({ status }: { status: string }) { - const { t } = useTranslation(); - const styles: Record = { - pending: 'bg-warning-500/20 text-warning-400 border-warning-500/30', - paid: 'bg-accent-500/20 text-accent-400 border-accent-500/30', - delivered: 'bg-success-500/20 text-success-400 border-success-500/30', - pending_activation: 'bg-accent-500/20 text-accent-400 border-accent-500/30', - failed: 'bg-error-500/20 text-error-400 border-error-500/30', - expired: 'bg-dark-600 text-dark-400 border-dark-500', - }; - const fallback = 'bg-dark-600 text-dark-400 border-dark-500'; - - const label = t(`admin.users.detail.gifts.status.${status}`, { defaultValue: '' }) || status; - - return ( - - {label} - - ); -} - -function GiftCard({ - gift, - direction, - locale, - onNavigateToUser, -}: { - gift: import('../api/adminUsers').AdminUserGiftItem; - direction: 'sent' | 'received'; - locale: string; - onNavigateToUser: (userId: number) => void; -}) { - const { t } = useTranslation(); - const { formatWithCurrency } = useCurrency(); - const isSent = direction === 'sent'; - - const otherPartyLabel = isSent - ? t('admin.users.detail.gifts.recipient') - : t('admin.users.detail.gifts.sender'); - const otherPartyName = isSent - ? gift.receiver_username - ? `@${gift.receiver_username}` - : gift.gift_recipient_value || t('admin.users.detail.gifts.codeOnly') - : gift.buyer_username - ? `@${gift.buyer_username}` - : gift.buyer_full_name || t('admin.users.detail.gifts.unknownUser'); - const otherPartyId = isSent ? gift.receiver_user_id : gift.buyer_user_id; - - const dateOpts: Intl.DateTimeFormatOptions = { - day: '2-digit', - month: '2-digit', - year: '2-digit', - hour: '2-digit', - minute: '2-digit', - }; - - return ( -
- {/* Header */} -
-
-
- - - -
-
-
{gift.tariff_name || '—'}
-
- {gift.period_days} {t('admin.users.detail.gifts.days')} · {gift.device_limit}{' '} - {t('admin.users.detail.gifts.devices')} -
-
-
- -
- - {/* Details grid */} -
-
- {otherPartyLabel}:{' '} - {otherPartyName} - {otherPartyId && ( - - )} -
-
- {t('admin.users.detail.gifts.amount')}:{' '} - {formatWithCurrency(gift.amount_kopeks / 100)} -
-
- {t('admin.users.detail.gifts.paymentMethod')}:{' '} - {gift.payment_method || '—'} -
-
- {t('admin.users.detail.gifts.createdAt')}:{' '} - - {gift.created_at ? new Date(gift.created_at).toLocaleString(locale, dateOpts) : '—'} - -
- {gift.paid_at && ( -
- {t('admin.users.detail.gifts.paidAt')}:{' '} - - {new Date(gift.paid_at).toLocaleString(locale, dateOpts)} - -
- )} - {gift.delivered_at && ( -
- {t('admin.users.detail.gifts.deliveredAt')}:{' '} - - {new Date(gift.delivered_at).toLocaleString(locale, dateOpts)} - -
- )} -
- - {/* Gift message */} - {gift.gift_message && ( -
- “{gift.gift_message}” -
- )} - - {/* Token */} -
GIFT-{gift.token}
-
- ); -} +// (GiftStatusBadge / GiftCard / GiftsTab JSX moved to components/admin/userDetail/GiftsTab.tsx) // ============ Main Page ============ @@ -2992,186 +2836,17 @@ export default function AdminUserDetail() { {/* Sync Tab */} {activeTab === 'sync' && ( -
- {/* Subscription selector for multi-tariff */} - {userSubscriptions.length > 1 && ( -
-
- {t('admin.users.detail.sync.selectSubscription')} -
- -
- )} - - {/* Sync status */} - {syncStatus && ( -
-
- {syncStatus.has_differences ? ( - - {t('admin.users.detail.sync.hasDifferences')} - - ) : ( - - {t('admin.users.detail.sync.synced')} - - )} -
- - {syncStatus.differences.length > 0 && ( -
- {syncStatus.differences.map((diff, i) => ( -
- • {diff} -
- ))} -
- )} - -
-
-
- {t('admin.users.detail.sync.bot')} -
-
-
- - {t('admin.users.detail.sync.statusLabel')}: - - - {syncStatus.bot_subscription_status || '-'} - -
-
- {t('admin.users.detail.sync.until')}: - - {syncStatus.bot_subscription_end_date - ? new Date(syncStatus.bot_subscription_end_date).toLocaleDateString( - locale, - ) - : '-'} - -
-
- - {t('admin.users.detail.sync.traffic')}: - - - {syncStatus.bot_traffic_used_gb.toFixed(2)} {t('common.units.gb')} - -
-
- - {t('admin.users.detail.sync.devices')}: - - {syncStatus.bot_device_limit} -
-
- - {t('admin.users.detail.sync.squads')}: - - {syncStatus.bot_squads?.length || 0} -
-
-
-
-
- {t('admin.users.detail.sync.panel')} -
-
-
- - {t('admin.users.detail.sync.statusLabel')}: - - {syncStatus.panel_status || '-'} -
-
- {t('admin.users.detail.sync.until')}: - - {syncStatus.panel_expire_at - ? new Date(syncStatus.panel_expire_at).toLocaleDateString(locale) - : '-'} - -
-
- - {t('admin.users.detail.sync.traffic')}: - - - {syncStatus.panel_traffic_used_gb.toFixed(2)} {t('common.units.gb')} - -
-
- - {t('admin.users.detail.sync.devices')}: - - {syncStatus.panel_device_limit} -
-
- - {t('admin.users.detail.sync.squads')}: - - - {syncStatus.panel_squads?.length || 0} - -
-
-
-
-
- )} - - {/* UUID info */} -
- {syncStatus?.subscription_tariff_name && ( -
- {syncStatus.subscription_tariff_name} -
- )} -
Remnawave UUID
-
- {syncStatus?.remnawave_uuid || - user.remnawave_uuid || - t('admin.users.detail.sync.notLinked')} -
-
- - {/* Sync buttons */} -
- - -
-
+ )} {/* Tickets Tab */} @@ -3399,126 +3074,12 @@ export default function AdminUserDetail() { {/* Gifts Tab */} {activeTab === 'gifts' && ( -
- {giftsLoading ? ( -
-
-
- ) : !giftsData || (giftsData.sent.length === 0 && giftsData.received.length === 0) ? ( -
- - - -

{t('admin.users.detail.gifts.noGifts')}

-
- ) : ( - <> - {/* Summary counters */} -
-
-
- {t('admin.users.detail.gifts.totalSent')} -
-
{giftsData.sent_total}
-
-
-
- {t('admin.users.detail.gifts.totalReceived')} -
-
- {giftsData.received_total} -
-
-
- - {/* Sent Gifts */} -
-

- - - - {t('admin.users.detail.gifts.sentTitle')} - ({giftsData.sent_total}) -

- {giftsData.sent.length === 0 ? ( -
- {t('admin.users.detail.gifts.noSent')} -
- ) : ( -
- {giftsData.sent.map((gift) => ( - navigate(`/admin/users/${id}`)} - /> - ))} -
- )} -
- - {/* Received Gifts */} -
-

- - - - {t('admin.users.detail.gifts.receivedTitle')} - ({giftsData.received_total}) -

- {giftsData.received.length === 0 ? ( -
- {t('admin.users.detail.gifts.noReceived')} -
- ) : ( -
- {giftsData.received.map((gift) => ( - navigate(`/admin/users/${id}`)} - /> - ))} -
- )} -
- - )} -
+ navigate(`/admin/users/${id}`)} + /> )} {/* Referrals Tab */}