diff --git a/backend/src/PnvPanel.Application/Common/Interfaces/IIdentityService.cs b/backend/src/PnvPanel.Application/Common/Interfaces/IIdentityService.cs index a571406..2c7c068 100644 --- a/backend/src/PnvPanel.Application/Common/Interfaces/IIdentityService.cs +++ b/backend/src/PnvPanel.Application/Common/Interfaces/IIdentityService.cs @@ -34,7 +34,9 @@ public sealed record UserSummaryDto( string Role, bool IsActivated, bool IsBlocked, - DateTimeOffset? ActivatedAt + DateTimeOffset? ActivatedAt, + bool BillingEnabled, + DateTimeOffset? BillingPaidUntil ); public sealed record UserStatsDto(int Total, int Activated); diff --git a/backend/src/PnvPanel.Infrastructure/Identity/IdentityService.cs b/backend/src/PnvPanel.Infrastructure/Identity/IdentityService.cs index 3cec77e..abfbdfe 100644 --- a/backend/src/PnvPanel.Infrastructure/Identity/IdentityService.cs +++ b/backend/src/PnvPanel.Infrastructure/Identity/IdentityService.cs @@ -269,15 +269,17 @@ internal sealed class IdentityService( var items = new List(users.Count); foreach (var user in users) { - var roleName = await GetPrimaryRoleNameAsync(user); + var role = await GetPrimaryRoleAsync(user); items.Add( new UserSummaryDto( user.Id, user.UserName!, - roleName, + role.Name!, user.IsActivated, user.IsBlocked, - user.ActivatedAt + user.ActivatedAt, + role.BillingEnabled, + user.BillingPaidUntil ) ); } diff --git a/frontend/src/features/billing/PaidUntilBadge.tsx b/frontend/src/features/billing/PaidUntilBadge.tsx new file mode 100644 index 0000000..87a0a53 --- /dev/null +++ b/frontend/src/features/billing/PaidUntilBadge.tsx @@ -0,0 +1,32 @@ +import { useTranslation } from 'react-i18next' +import { Badge } from '@/shared/ui/badge' + +const RED_THRESHOLD_DAYS = 3 +const YELLOW_THRESHOLD_DAYS = 7 + +/** Дней до paidUntil, округление вверх — «меньше суток» всё равно считается как 1 день, а не 0. */ +function daysRemaining(paidUntil: string): number { + const diffMs = new Date(paidUntil).getTime() - Date.now() + return Math.ceil(diffMs / (24 * 60 * 60 * 1000)) +} + +/** Цветовая индикация оплаченного периода: зелёный — обычный запас, жёлтый — ≤7 дней, красный — + * ≤3 дней или уже истекло. Общий компонент для дашборда пользователя и списка пользователей в админке. */ +export function PaidUntilBadge({ paidUntil }: { paidUntil: string | null }) { + const { t } = useTranslation() + + if (!paidUntil) return {t('billing.neverPaidShort')} + + const days = daysRemaining(paidUntil) + const variant = days <= RED_THRESHOLD_DAYS ? 'destructive' : days <= YELLOW_THRESHOLD_DAYS ? 'warning' : 'success' + const label = + days <= 0 + ? t('billing.expiredShort') + : t('billing.daysRemaining', { count: days }) + + return ( + + {label} + + ) +} diff --git a/frontend/src/routes/admin/users.tsx b/frontend/src/routes/admin/users.tsx index 9041dee..0cc02b7 100644 --- a/frontend/src/routes/admin/users.tsx +++ b/frontend/src/routes/admin/users.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' import { Input } from '@/shared/ui/input' import { Button } from '@/shared/ui/button' import { Badge } from '@/shared/ui/badge' +import { PaidUntilBadge } from '@/features/billing/PaidUntilBadge' import { listUsers } from '@/features/admin/users/api' import { UserManageDialog } from '@/features/admin/users/UserManageDialog' import type { UserSummaryDto } from '@/shared/api/types' @@ -56,6 +57,7 @@ function AdminUsersPage() { {t('admin.users.userName')} {t('admin.users.role')} {t('admin.users.statusLabel')} + {t('admin.users.billingLabel')} @@ -73,6 +75,13 @@ function AdminUsersPage() { : t('admin.users.status.pending')} + + {user.billingEnabled ? ( + + ) : ( + + )} +