Implement billing status feature in Telegram bot
- Added a new command `/billing` and a corresponding menu option for users with billing roles to check their payment status. - Implemented `HandleBillingStatusAsync` method to retrieve and display billing information, including the payment expiration date and remaining time in a user-friendly format. - Updated the main menu to conditionally show the billing status option based on the user's role. - Enhanced the `PaidUntilBadge` component to format and display the remaining time until the next payment in both days and hours/minutes. - Updated documentation to reflect the new billing status feature and its usage in the Telegram bot.
This commit is contained in:
@@ -1,15 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { formatRemaining, getDaysRemaining } from './remaining'
|
||||
|
||||
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 }) {
|
||||
@@ -17,16 +12,12 @@ export function PaidUntilBadge({ paidUntil }: { paidUntil: string | null }) {
|
||||
|
||||
if (!paidUntil) return <Badge variant="destructive">{t('billing.neverPaidShort')}</Badge>
|
||||
|
||||
const days = daysRemaining(paidUntil)
|
||||
const days = getDaysRemaining(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 (
|
||||
<Badge variant={variant} title={new Date(paidUntil).toLocaleDateString()}>
|
||||
{label}
|
||||
<Badge variant={variant} title={new Date(paidUntil).toLocaleString()}>
|
||||
{formatRemaining(paidUntil, t)}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
const DAY_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
type TranslateFn = (key: string, options?: Record<string, unknown>) => string
|
||||
|
||||
/** Дней до paidUntil (дробное число, для цветовых порогов — не округлять до отображения). */
|
||||
export function getDaysRemaining(paidUntil: string): number {
|
||||
return (new Date(paidUntil).getTime() - Date.now()) / DAY_MS
|
||||
}
|
||||
|
||||
/** Человекочитаемый остаток: "N дн." если ≥ суток, иначе "N ч M мин"/"M мин", "истекло" если уже
|
||||
* прошло. То же форматирование, что в боте (см. FormatRemaining в PnvBotUpdateHandler.cs). */
|
||||
export function formatRemaining(paidUntil: string, t: TranslateFn): string {
|
||||
const diffMs = new Date(paidUntil).getTime() - Date.now()
|
||||
if (diffMs <= 0) return t('billing.expiredShort')
|
||||
|
||||
if (diffMs < DAY_MS) {
|
||||
const totalMinutes = Math.ceil(diffMs / 60000)
|
||||
const hours = Math.floor(totalMinutes / 60)
|
||||
const minutes = totalMinutes % 60
|
||||
return hours > 0
|
||||
? t('billing.hoursMinutesRemaining', { hours, minutes })
|
||||
: t('billing.minutesRemaining', { count: minutes })
|
||||
}
|
||||
|
||||
const days = Math.ceil(diffMs / DAY_MS)
|
||||
return t('billing.daysRemaining', { count: days })
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/shared/ui/card'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { getMyBillingStatus } from '@/features/billing/api'
|
||||
import { PaidUntilBadge } from '@/features/billing/PaidUntilBadge'
|
||||
import { PaymentRequestPanel } from '@/features/billing/PaymentRequestPanel'
|
||||
|
||||
export const Route = createFileRoute('/billing')({ component: BillingPage })
|
||||
@@ -56,12 +57,13 @@ function BillingContent() {
|
||||
<CardTitle className="text-base">{t('billing.status')}</CardTitle>
|
||||
{data.suspended && <Badge variant="destructive">{t('billing.suspended')}</Badge>}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{data.paidUntil
|
||||
? t('billing.paidUntil', { date: new Date(data.paidUntil).toLocaleDateString() })
|
||||
: t('billing.neverPaid')}
|
||||
</p>
|
||||
{data.paidUntil && <PaidUntilBadge paidUntil={data.paidUntil} />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -112,6 +112,8 @@ const resources = {
|
||||
neverPaidShort: 'Не оплачено',
|
||||
expiredShort: 'Истекло',
|
||||
daysRemaining: '{{count}} дн.',
|
||||
hoursMinutesRemaining: '{{hours}} ч {{minutes}} мин',
|
||||
minutesRemaining: '{{count}} мин',
|
||||
newRequest: 'Оформить оплату',
|
||||
period: 'Период',
|
||||
periods: {
|
||||
@@ -626,6 +628,8 @@ const resources = {
|
||||
neverPaidShort: 'Not paid',
|
||||
expiredShort: 'Expired',
|
||||
daysRemaining: '{{count}}d',
|
||||
hoursMinutesRemaining: '{{hours}}h {{minutes}}m',
|
||||
minutesRemaining: '{{count}}m',
|
||||
newRequest: 'Set up payment',
|
||||
period: 'Period',
|
||||
periods: {
|
||||
|
||||
Reference in New Issue
Block a user