From 386acef1763ec86711272b6638c775810c2e9abe Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 18 Jan 2026 06:03:11 +0300 Subject: [PATCH] Add files via upload --- src/pages/Subscription.tsx | 471 +++++++++++++++++++++++++------------ 1 file changed, 321 insertions(+), 150 deletions(-) diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index 3617ec2..f1d37e5 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import { useLocation } from 'react-router-dom' import { AxiosError } from 'axios' import { subscriptionApi } from '../api/subscription' +import { promoApi } from '../api/promo' import type { PurchaseSelection, PeriodOption, Tariff, TariffPeriod, ClassicPurchaseOptions } from '../types' import ConnectionModal from '../components/ConnectionModal' import { useCurrency } from '../hooks/useCurrency' @@ -55,6 +56,24 @@ export default function Subscription() { // Helper to format price from kopeks const formatPrice = (kopeks: number) => `${formatAmount(kopeks / 100)} ${currencySymbol}` + // Helper to apply promo discount to a price + const applyPromoDiscount = (priceKopeks: number, hasExistingDiscount: boolean = false): { + price: number; + original: number | null; + percent: number | null + } => { + // Only apply promo discount if no existing discount (from promo group) and we have an active promo discount + if (!activeDiscount?.is_active || !activeDiscount.discount_percent || hasExistingDiscount) { + return { price: priceKopeks, original: null, percent: null } + } + const discountedPrice = Math.round(priceKopeks * (1 - activeDiscount.discount_percent / 100)) + return { + price: discountedPrice, + original: priceKopeks, + percent: activeDiscount.discount_percent + } + } + // Purchase state (classic mode) const [currentStep, setCurrentStep] = useState('period') const [selectedPeriod, setSelectedPeriod] = useState(null) @@ -91,6 +110,13 @@ export default function Subscription() { queryFn: subscriptionApi.getPurchaseOptions, }) + // Fetch active promo discount + const { data: activeDiscount } = useQuery({ + queryKey: ['active-discount'], + queryFn: promoApi.getActiveDiscount, + staleTime: 30000, + }) + // Check if in tariffs mode (moved up to be available for useEffect) const isTariffsMode = purchaseOptions?.sales_mode === 'tariffs' const classicOptions = !isTariffsMode ? purchaseOptions as ClassicPurchaseOptions : null @@ -1116,16 +1142,25 @@ export default function Subscription() { // Daily tariff price (is_daily + daily_price_kopeks) const dailyPrice = tariff.daily_price_kopeks ?? tariff.price_per_day_kopeks ?? 0 const originalDailyPrice = tariff.original_daily_price_kopeks || 0 + const hasExistingDailyDiscount = originalDailyPrice > dailyPrice if (dailyPrice > 0) { + // Apply promo discount if no existing discount + const promoDaily = applyPromoDiscount(dailyPrice, hasExistingDailyDiscount) return ( - {formatPrice(dailyPrice)} - {originalDailyPrice > dailyPrice && ( - {formatPrice(originalDailyPrice)} + {formatPrice(promoDaily.price)} + {/* Show original price from promo group or promo offer */} + {(hasExistingDailyDiscount || promoDaily.original) && ( + + {formatPrice(hasExistingDailyDiscount ? originalDailyPrice : promoDaily.original!)} + )} / день - {tariff.daily_discount_percent && tariff.daily_discount_percent > 0 && ( + {/* Show discount badge */} + {(tariff.daily_discount_percent && tariff.daily_discount_percent > 0) ? ( -{tariff.daily_discount_percent}% + ) : promoDaily.percent && ( + -{promoDaily.percent}% )} ) @@ -1133,18 +1168,24 @@ export default function Subscription() { // Period-based price if (tariff.periods.length > 0) { const firstPeriod = tariff.periods[0] - const hasDiscount = firstPeriod?.original_price_kopeks && firstPeriod.original_price_kopeks > firstPeriod.price_kopeks + const hasExistingDiscount = !!(firstPeriod?.original_price_kopeks && firstPeriod.original_price_kopeks > firstPeriod.price_kopeks) + // Apply promo discount if no existing discount + const promoPeriod = applyPromoDiscount(firstPeriod?.price_kopeks || 0, hasExistingDiscount) return ( {t('subscription.from')} - {formatPrice(firstPeriod?.price_kopeks || 0)} - {hasDiscount && ( - <> - {formatPrice(firstPeriod.original_price_kopeks!)} - {firstPeriod.discount_percent && ( - -{firstPeriod.discount_percent}% - )} - + {formatPrice(promoPeriod.price)} + {/* Show original price from promo group or promo offer */} + {(hasExistingDiscount || promoPeriod.original) && ( + + {formatPrice(hasExistingDiscount ? firstPeriod.original_price_kopeks! : promoPeriod.original!)} + + )} + {/* Show discount badge */} + {hasExistingDiscount && firstPeriod.discount_percent ? ( + -{firstPeriod.discount_percent}% + ) : promoPeriod.percent && ( + -{promoPeriod.percent}% )} ) @@ -1330,34 +1371,47 @@ export default function Subscription() { {/* Fixed periods */} {selectedTariff.periods.length > 0 && !useCustomDays && (
- {selectedTariff.periods.map((period) => ( -
-
{formatPrice(period.price_per_month_kopeks)}/{t('subscription.month')}
- - ))} +
{period.label}
+
+ {formatPrice(displayPrice)} + {displayOriginal && displayOriginal > displayPrice && ( + {formatPrice(displayOriginal)} + )} +
+
{formatPrice(displayPerMonth)}/{t('subscription.month')}
+ + ) + })} )} @@ -1400,10 +1454,25 @@ export default function Subscription() { className="w-20 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 text-center" /> -
- {customDays} дней × {formatPrice(selectedTariff.price_per_day_kopeks ?? 0)}/день - {formatPrice(customDays * (selectedTariff.price_per_day_kopeks ?? 0))} -
+ {(() => { + const basePrice = customDays * (selectedTariff.price_per_day_kopeks ?? 0) + const hasExistingDiscount = !!(selectedTariff.original_price_per_day_kopeks && selectedTariff.original_price_per_day_kopeks > (selectedTariff.price_per_day_kopeks ?? 0)) + const promoCustom = applyPromoDiscount(basePrice, hasExistingDiscount) + return ( +
+ {customDays} дней × {formatPrice(selectedTariff.price_per_day_kopeks ?? 0)}/день +
+ {formatPrice(promoCustom.price)} + {promoCustom.original && ( + <> + {formatPrice(promoCustom.original)} + -{promoCustom.percent}% + + )} +
+
+ ) + })()} )} @@ -1472,42 +1541,76 @@ export default function Subscription() { {/* Summary & Purchase */} {(selectedTariffPeriod || useCustomDays) && (
- {/* Price breakdown */} -
- {useCustomDays ? ( -
- Период: {customDays} дней - {formatPrice(customDays * (selectedTariff.price_per_day_kopeks ?? 0))} -
- ) : selectedTariffPeriod && ( -
- Период: {selectedTariffPeriod.label} - {formatPrice(selectedTariffPeriod.price_kopeks)} -
- )} - {useCustomTraffic && selectedTariff.custom_traffic_enabled && ( -
- Трафик: {customTrafficGb} ГБ - +{formatPrice(customTrafficGb * (selectedTariff.traffic_price_per_gb_kopeks ?? 0))} -
- )} -
- {(() => { - const periodPrice = useCustomDays + // Calculate prices with promo discount + const basePeriodPrice = useCustomDays ? customDays * (selectedTariff.price_per_day_kopeks ?? 0) : (selectedTariffPeriod?.price_kopeks || 0) + const hasExistingPeriodDiscount = !useCustomDays && selectedTariffPeriod?.original_price_kopeks + ? selectedTariffPeriod.original_price_kopeks > selectedTariffPeriod.price_kopeks + : false + const promoPeriod = applyPromoDiscount(basePeriodPrice, hasExistingPeriodDiscount) + const trafficPrice = useCustomTraffic && selectedTariff.custom_traffic_enabled ? customTrafficGb * (selectedTariff.traffic_price_per_gb_kopeks ?? 0) : 0 - const totalPrice = periodPrice + trafficPrice + + const totalPrice = promoPeriod.price + trafficPrice + const originalTotal = promoPeriod.original ? promoPeriod.original + trafficPrice : null const hasEnoughBalance = purchaseOptions && totalPrice <= purchaseOptions.balance_kopeks return ( <> + {/* Price breakdown */} +
+ {useCustomDays ? ( +
+ Период: {customDays} дней +
+ {formatPrice(promoPeriod.price)} + {promoPeriod.original && ( + {formatPrice(promoPeriod.original)} + )} +
+
+ ) : selectedTariffPeriod && ( +
+ Период: {selectedTariffPeriod.label} +
+ {formatPrice(promoPeriod.price)} + {(hasExistingPeriodDiscount || promoPeriod.original) && ( + + {formatPrice(hasExistingPeriodDiscount ? selectedTariffPeriod.original_price_kopeks! : promoPeriod.original!)} + + )} +
+
+ )} + {useCustomTraffic && selectedTariff.custom_traffic_enabled && ( +
+ Трафик: {customTrafficGb} ГБ + +{formatPrice(trafficPrice)} +
+ )} +
+ + {/* Promo discount info */} + {promoPeriod.percent && ( +
+ + Скидка -{promoPeriod.percent}% применена + +
+ )} +
{t('subscription.total')} - {formatPrice(totalPrice)} +
+ {formatPrice(totalPrice)} + {originalTotal && ( +
{formatPrice(originalTotal)}
+ )} +
{purchaseOptions && !hasEnoughBalance && ( @@ -1593,93 +1696,139 @@ export default function Subscription() { {/* Step: Period Selection */} {currentStep === 'period' && classicOptions && (
- {classicOptions.periods.map((period) => ( - - ))} + {classicOptions.periods.map((period) => { + const hasExistingDiscount = !!(period.discount_percent && period.discount_percent > 0) + const promoPeriod = applyPromoDiscount(period.price_kopeks, hasExistingDiscount) + const displayDiscount = hasExistingDiscount ? period.discount_percent : promoPeriod.percent + const displayOriginal = hasExistingDiscount ? period.original_price_kopeks : promoPeriod.original + + return ( + + ) + })}
)} {/* Step: Traffic Selection */} {currentStep === 'traffic' && selectedPeriod?.traffic.options && (
- {selectedPeriod.traffic.options.map((option) => ( - - ))} + {selectedPeriod.traffic.options.map((option) => { + const hasExistingDiscount = !!(option.discount_percent && option.discount_percent > 0) + const promoTraffic = applyPromoDiscount(option.price_kopeks, hasExistingDiscount) + + return ( + + ) + })}
)} {/* Step: Server Selection */} {currentStep === 'servers' && selectedPeriod?.servers.options && (
- {selectedPeriod.servers.options.map((server) => ( -
- - ))} + + ) + })}
)} @@ -1723,6 +1872,18 @@ export default function Subscription() { ) : preview ? (
+ {/* Active promo discount banner */} + {activeDiscount?.is_active && activeDiscount.discount_percent && !preview.original_price_kopeks && ( +
+ + + + + Скидка -{activeDiscount.discount_percent}% применена + +
+ )} + {preview.breakdown.map((item, idx) => (
{item.label} @@ -1730,15 +1891,25 @@ export default function Subscription() {
))} -
- {t('subscription.total')} -
-
{formatPrice(preview.total_price_kopeks)}
- {preview.original_price_kopeks && ( -
{formatPrice(preview.original_price_kopeks)}
- )} -
-
+ {(() => { + // Apply promo discount if not already applied by server + const hasServerDiscount = !!preview.original_price_kopeks + const promoTotal = applyPromoDiscount(preview.total_price_kopeks, hasServerDiscount) + const displayTotal = promoTotal.price + const displayOriginal = hasServerDiscount ? preview.original_price_kopeks : promoTotal.original + + return ( +
+ {t('subscription.total')} +
+
{formatPrice(displayTotal)}
+ {displayOriginal && ( +
{formatPrice(displayOriginal)}
+ )} +
+
+ ) + })()} {preview.discount_label && (
{preview.discount_label}