Enhance billing status handling in PlanContent component
CI / Backend (build + test) (push) Failing after 1m23s
CI / Frontend (lint + typecheck + build) (push) Successful in 44s

- Integrated billing status query to determine if payment is required for users with active plans.
- Added conditional rendering for a payment notice card, guiding users to the billing page if payment is needed.
- Updated i18n resource files with new translations for payment-related messages, improving user clarity on billing status.
This commit is contained in:
Leonid Pershin
2026-07-24 02:43:49 +03:00
parent 694683455b
commit 4e1b63645f
2 changed files with 50 additions and 13 deletions
+48 -13
View File
@@ -13,6 +13,8 @@ import { applyDiscount, resolveDiscountPercent } from '@/shared/lib/pricing'
import { getMyConfigs } from '@/features/configs/api'
import { getSupportPricing } from '@/features/support/api'
import { changePlan, getMyPlanStatus, listPlans } from '@/features/plans/api'
import { getMyBillingStatus } from '@/features/billing/api'
import { getDaysRemaining } from '@/features/billing/remaining'
export const Route = createFileRoute('/plan')({ component: PlanPage })
@@ -35,6 +37,17 @@ function PlanContent() {
const statusQuery = useQuery({ queryKey: ['my-plan-status'], queryFn: getMyPlanStatus })
const pricingQuery = useQuery({ queryKey: ['support-pricing'], queryFn: getSupportPricing })
const configsQuery = useQuery({ queryKey: ['my-configs'], queryFn: getMyConfigs })
const billingStatusQuery = useQuery({ queryKey: ['my-billing-status'], queryFn: getMyBillingStatus })
// Если оплата включена для роли, но сейчас нет действующего оплаченного периода (ни разу не
// платил, либо срок истёк) — не ждём, пока это всплывёт как 403 при попытке создать конфиг:
// сразу предлагаем оформить оплату прямо на странице тарифа. Не показываем, если заявка уже
// отправлена и ждёт решения админа (activeRequest) — тогда достаточно билинг-страницы.
const billingData = billingStatusQuery.data
const needsPayment =
!!billingData?.billingEnabled &&
!billingData.activeRequest &&
(!billingData.paidUntil || getDaysRemaining(billingData.paidUntil) <= 0)
const [selection, setSelection] = useState<string>('')
const [customCount, setCustomCount] = useState('3')
@@ -104,6 +117,19 @@ function PlanContent() {
<p className="mt-1 text-xs text-muted-foreground">{t('plan.pageHint')}</p>
</div>
{needsPayment && (
<Card className="border-destructive/50">
<CardHeader>
<CardTitle className="text-base">{t('plan.paymentRequiredNotice')}</CardTitle>
</CardHeader>
<CardContent>
<Link to="/billing" className="font-medium text-foreground underline underline-offset-2">
{t('plan.goToBilling')}
</Link>
</CardContent>
</Card>
)}
<div className="flex flex-col gap-3">
{plansQuery.data?.map((p) => (
<label
@@ -123,19 +149,28 @@ function PlanContent() {
</label>
))}
<label className="flex cursor-pointer items-center gap-3 rounded-md border border-border p-3 text-sm has-[:checked]:border-foreground">
<input type="radio" name="plan" checked={selection === 'custom'} onChange={() => setSelection('custom')} />
<span className="font-medium">{t('plan.custom')}</span>
<Input
type="number"
min={3}
className="w-24"
value={customCount}
onChange={(e) => {
setCustomCount(e.target.value)
setSelection('custom')
}}
/>
<label className="flex cursor-pointer items-center justify-between gap-3 rounded-md border border-border p-3 text-sm has-[:checked]:border-foreground">
<span className="flex items-center gap-3">
<input type="radio" name="plan" checked={selection === 'custom'} onChange={() => setSelection('custom')} />
<span className="font-medium">{t('plan.custom')}</span>
<Input
type="number"
min={3}
className="w-24"
value={customCount}
onChange={(e) => {
setCustomCount(e.target.value)
setSelection('custom')
}}
/>
</span>
{selection === 'custom' && (
<span className="flex flex-col items-end text-xs text-muted-foreground">
<span>{t('plan.perQuarter', { price: priceFor(pricingQuery.data?.pricePerConfigPerQuarter, targetCount, 3) })}</span>
<span>{t('plan.perHalfYear', { price: priceFor(pricingQuery.data?.pricePerConfigPerHalfYear, targetCount, 6) })}</span>
<span>{t('plan.perYear', { price: priceFor(pricingQuery.data?.pricePerConfigPerYear, targetCount, 12) })}</span>
</span>
)}
</label>
{selection === 'custom' && <p className="text-xs text-muted-foreground">{t('plan.customHint')}</p>}
</div>