import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router'; import { useCurrency } from '../../../hooks/useCurrency'; import { createNumberInputHandler } from '../../../utils/inputHelpers'; import type { UserDetailResponse, UserListItem, UserPanelInfo, UserSubscriptionInfo, } from '../../../api/adminUsers'; import type { PromoGroup } from '../../../api/promocodes'; import { ServerIcon } from '@/components/icons'; // ────────────────────────────────────────────────────────────────── // Local status badge (parent has its own — duplicating here to keep // this file self-contained for the tab's smallest unit of meaning). // ────────────────────────────────────────────────────────────────── function StatusBadge({ status }: { status: string }) { const { t } = useTranslation(); const styles: Record = { active: 'bg-success-500/20 text-success-400 border-success-500/30', blocked: 'bg-error-500/20 text-error-400 border-error-500/30', deleted: 'bg-dark-600 text-dark-400 border-dark-500', }; return ( {t(`admin.users.status.${status}`, status)} ); } // ────────────────────────────────────────────────────────────────── // Info tab — user metadata + VPN connection + promo group + referral // + restrictions + 4 destructive actions (reset/disable/delete). // // The tab is a "view facade" — it reads parent state (panelInfo, // promoGroups, etc. live up there because subscription tab also // consumes them) and delegates every mutation to a parent handler. // Local UI state is parent-owned for the same reason: the parent // already maintains the inline-confirm armer for actions shared // with the subscription tab. // ────────────────────────────────────────────────────────────────── export interface InfoTabProps { user: UserDetailResponse; hasPermission: (perm: string) => boolean; formatDate: (date: string | null) => string; locale: string; // Subscription / panel info (also used by Subscription tab in parent) panelInfo: UserPanelInfo | null; panelInfoLoading: boolean; userSubscriptions: UserSubscriptionInfo[]; activeSubscriptionId: number | null; onActiveSubscriptionChange: (id: number) => void; // Promo-group editor promoGroups: PromoGroup[]; editingPromoGroup: boolean; onToggleEditingPromoGroup: () => void; onChangePromoGroup: (groupId: number | null) => void; // Referral commission editor editingReferralCommission: boolean; referralCommissionValue: number | ''; onSetReferralCommissionValue: (value: number | '') => void; onToggleEditingReferralCommission: () => void; onUpdateReferralCommission: () => void; // Referrals mini-list referrals: UserListItem[]; referralsLoading: boolean; // Status block/unblock actionLoading: boolean; onBlockUser: () => void; onUnblockUser: () => void; // Destructive actions + inline-confirm armer confirmingAction: string | null; onInlineConfirm: (actionKey: string, executeFn: () => Promise) => void; onResetTrial: () => Promise; onResetSubscription: () => Promise; onDisableUser: () => Promise; onFullDeleteUser: () => Promise; } export function InfoTab(props: InfoTabProps) { const { t } = useTranslation(); const { formatWithCurrency } = useCurrency(); const navigate = useNavigate(); const { user, hasPermission, formatDate, locale, panelInfo, panelInfoLoading, userSubscriptions, activeSubscriptionId, onActiveSubscriptionChange, promoGroups, editingPromoGroup, onToggleEditingPromoGroup, onChangePromoGroup, editingReferralCommission, referralCommissionValue, onSetReferralCommissionValue, onToggleEditingReferralCommission, onUpdateReferralCommission, referrals, referralsLoading, actionLoading, onBlockUser, onUnblockUser, confirmingAction, onInlineConfirm, onResetTrial, onResetSubscription, onDisableUser, onFullDeleteUser, } = props; return (
{/* Status */}
{t('admin.users.detail.status')}
{user.status === 'active' ? ( ) : user.status === 'blocked' ? ( ) : null}
{/* Details grid */}
Email
{user.email || '-'}
{t('admin.users.detail.language')}
{user.language}
{t('admin.users.detail.registration')}
{formatDate(user.created_at)}
{t('admin.users.detail.botActivity')}
{formatDate(user.last_activity)}
{t('admin.users.detail.cabinetLastLogin')}
{formatDate(user.cabinet_last_login)}
{t('admin.users.detail.totalSpent')}
{formatWithCurrency(user.total_spent_kopeks / 100)}
{t('admin.users.detail.purchases')}
{user.purchase_count}
{/* VPN Connection Info */} {(panelInfo || userSubscriptions.length > 0) && (
{t('admin.users.detail.vpnConnection')} {userSubscriptions.length > 1 && ( )}
{panelInfoLoading && !panelInfo?.found && (
)} {panelInfo?.found && (
{t('admin.users.detail.lastConnection')}
{panelInfo.online_at && (() => { const onlineDate = new Date(panelInfo.online_at); const isRecent = Date.now() - onlineDate.getTime() < 5 * 60 * 1000; return ( <> {isRecent ? t('admin.users.detail.online') : formatDate(panelInfo.online_at)} ); })()} {!panelInfo.online_at && -}
{t('admin.users.detail.firstConnection')}
{panelInfo.first_connected_at ? new Date(panelInfo.first_connected_at).toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric', }) : '-'}
{panelInfo.last_connected_node_name && (
{t('admin.users.detail.lastNode')}
{panelInfo.last_connected_node_name}
)}
)} {!panelInfoLoading && !panelInfo?.found && userSubscriptions.length > 0 && (
{t('admin.users.detail.noVpnData')}
)}
)} {/* Campaign */} {user.campaign_name && (
{t('admin.users.detail.campaign')}
{user.campaign_name}
)} {/* Promo Group */}
{t('admin.users.detail.promoGroup')} {hasPermission('users:promo_group') && ( )}
{editingPromoGroup ? (
{user.promo_group && ( )}
) : (
{user.promo_group?.name || ( {t('admin.users.detail.noPromoGroup')} )}
)}
{/* Referral */}
{t('admin.users.detail.referral.title')} {hasPermission('users:referral') && ( )}
{user.referral.referrals_count}
{t('admin.users.detail.referral.referrals')}
{formatWithCurrency(user.referral.total_earnings_kopeks / 100)}
{t('admin.users.detail.referral.earned')}
{editingReferralCommission ? (
) : ( <>
{user.referral.commission_percent != null ? `${user.referral.commission_percent}%` : t('admin.users.detail.referral.default')}
{t('admin.users.detail.referral.commission')}
)}
{/* Referrals list */} {user.referral.referrals_count > 0 && (
{t('admin.users.detail.referralsList')}
{referralsLoading ? (
) : referrals.length === 0 ? (
{t('admin.users.detail.noReferrals')}
) : (
{referrals.map((ref) => ( ))}
)}
)} {/* Restrictions */} {(user.restriction_topup || user.restriction_subscription) && (
{t('admin.users.detail.restrictions.title')}
{user.restriction_topup && (
{t('admin.users.detail.restrictions.topup')}
)} {user.restriction_subscription && (
{t('admin.users.detail.restrictions.subscription')}
)} {user.restriction_reason && (
{t('admin.users.detail.restrictions.reason')}: {user.restriction_reason}
)}
)} {/* Actions */}
{t('admin.users.detail.actions.title')}
); }