From 2920ca45e870d55e97f821b90bb32efa6266c798 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Mon, 2 Feb 2026 06:05:15 +0300 Subject: [PATCH] refactor: convert promocode stats modal to separate page - Create AdminPromocodeStats.tsx with responsive mobile layout - Add route /admin/promocodes/:id/stats - Fix mobile layout in AdminPromocodes.tsx (badges and buttons no longer overlap) - Remove PromocodeStatsModal component --- src/App.tsx | 11 ++ src/locales/en.json | 2 + src/locales/fa.json | 2 + src/locales/ru.json | 2 + src/locales/zh.json | 2 + src/pages/AdminPromocodeStats.tsx | 294 ++++++++++++++++++++++++++++++ src/pages/AdminPromocodes.tsx | 276 ++-------------------------- 7 files changed, 328 insertions(+), 261 deletions(-) create mode 100644 src/pages/AdminPromocodeStats.tsx diff --git a/src/App.tsx b/src/App.tsx index cdd0d8b..d5988a1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -42,6 +42,7 @@ const AdminBroadcasts = lazy(() => import('./pages/AdminBroadcasts')); const AdminBroadcastCreate = lazy(() => import('./pages/AdminBroadcastCreate')); const AdminPromocodes = lazy(() => import('./pages/AdminPromocodes')); const AdminPromocodeCreate = lazy(() => import('./pages/AdminPromocodeCreate')); +const AdminPromocodeStats = lazy(() => import('./pages/AdminPromocodeStats')); const AdminPromoGroups = lazy(() => import('./pages/AdminPromoGroups')); const AdminPromoGroupCreate = lazy(() => import('./pages/AdminPromoGroupCreate')); const AdminCampaigns = lazy(() => import('./pages/AdminCampaigns')); @@ -393,6 +394,16 @@ function App() { } /> + + + + + + } + /> ( + + + +); + +const ClockIcon = () => ( + + + +); + +const UserIcon = () => ( + + + +); + +// Helper functions +const getTypeLabel = (type: PromoCodeType): string => { + const labels: Record = { + balance: i18n.t('admin.promocodes.type.balance'), + subscription_days: i18n.t('admin.promocodes.type.subscriptionDays'), + trial_subscription: i18n.t('admin.promocodes.type.trialSubscription'), + promo_group: i18n.t('admin.promocodes.type.promoGroup'), + discount: i18n.t('admin.promocodes.type.discount'), + }; + return labels[type] || type; +}; + +const getTypeColor = (type: PromoCodeType): string => { + const colors: Record = { + balance: 'bg-success-500/20 text-success-400', + subscription_days: 'bg-accent-500/20 text-accent-400', + trial_subscription: 'bg-accent-500/20 text-accent-400', + promo_group: 'bg-warning-500/20 text-warning-400', + discount: 'bg-pink-500/20 text-pink-400', + }; + return colors[type] || 'bg-dark-600 text-dark-300'; +}; + +const formatDate = (date: string | null): string => { + if (!date) return '-'; + const localeMap: Record = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' }; + const locale = localeMap[i18n.language] || 'ru-RU'; + return new Date(date).toLocaleDateString(locale, { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }); +}; + +const formatDateTime = (date: string | null): string => { + if (!date) return '-'; + const localeMap: Record = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' }; + const locale = localeMap[i18n.language] || 'ru-RU'; + return new Date(date).toLocaleString(locale, { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +}; + +export default function AdminPromocodeStats() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + + const { + data: promocode, + isLoading, + error, + } = useQuery({ + queryKey: ['admin-promocode', id], + queryFn: () => promocodesApi.getPromocode(Number(id)), + enabled: !!id, + }); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (error || !promocode) { + return ( +
+
+ +

+ {t('admin.promocodes.stats.title')} +

+
+
+

{t('admin.promocodes.stats.notFound')}

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+
+ {promocode.code} +
+ + {getTypeLabel(promocode.type)} + + {!promocode.is_active && ( + + {t('admin.promocodes.stats.inactive')} + + )} +
+
+ +
+ +
+ {/* Stats Cards */} +
+
+
{promocode.total_uses}
+
{t('admin.promocodes.stats.totalUses')}
+
+
+
{promocode.today_uses}
+
{t('admin.promocodes.stats.today')}
+
+
+
+ {promocode.max_uses === 0 ? '∞' : promocode.uses_left} +
+
{t('admin.promocodes.stats.remaining')}
+
+
+ + {/* Details */} +
+

{t('admin.promocodes.stats.details')}

+
+
+ {t('admin.promocodes.stats.type')}: + {getTypeLabel(promocode.type)} +
+ {promocode.type === 'balance' && ( +
+ {t('admin.promocodes.stats.bonus')}: + + +{promocode.balance_bonus_rubles} {t('admin.promocodes.form.rub')} + +
+ )} + {(promocode.type === 'subscription_days' || + promocode.type === 'trial_subscription') && ( +
+ {t('admin.promocodes.stats.daysLabel')}: + +{promocode.subscription_days} +
+ )} + {promocode.type === 'discount' && ( + <> +
+ + {t('admin.promocodes.stats.discountLabel')}: + + -{promocode.balance_bonus_kopeks}% +
+
+ {t('admin.promocodes.stats.validFor')}: + + {t('admin.promocodes.stats.hoursValue', { + count: promocode.subscription_days, + })} + +
+ + )} +
+ {t('admin.promocodes.stats.limit')}: + + {promocode.current_uses}/{promocode.max_uses === 0 ? '∞' : promocode.max_uses} + +
+
+ {t('admin.promocodes.stats.status')}: + + {promocode.is_valid + ? t('admin.promocodes.stats.active') + : t('admin.promocodes.stats.inactive')} + +
+
+ {t('admin.promocodes.stats.created')}: + {formatDateTime(promocode.created_at)} +
+
+ {t('admin.promocodes.stats.validUntil')}: + + {promocode.valid_until + ? formatDate(promocode.valid_until) + : t('admin.promocodes.stats.unlimited')} + +
+ {promocode.first_purchase_only && ( +
+ {t('admin.promocodes.stats.restriction')}: + + {t('admin.promocodes.stats.firstPurchaseOnly')} + +
+ )} +
+
+ + {/* Usage History */} +
+

+ + {t('admin.promocodes.stats.usageHistory')} +

+ {promocode.recent_uses.length === 0 ? ( +

+ {t('admin.promocodes.stats.noUsages')} +

+ ) : ( +
+ {promocode.recent_uses.map((use) => ( +
+
+
+ +
+
+
+ {use.user_full_name || use.user_username || `User #${use.user_id}`} +
+ {use.user_username && ( +
@{use.user_username}
+ )} +
+
+
+ {formatDateTime(use.used_at)} +
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/src/pages/AdminPromocodes.tsx b/src/pages/AdminPromocodes.tsx index 6839b52..f119175 100644 --- a/src/pages/AdminPromocodes.tsx +++ b/src/pages/AdminPromocodes.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import i18n from '../i18n'; -import { promocodesApi, PromoCode, PromoCodeDetail, PromoCodeType } from '../api/promocodes'; +import { promocodesApi, PromoCode, PromoCodeType } from '../api/promocodes'; import { AdminBackButton } from '../components/admin'; // Icons @@ -40,12 +40,6 @@ const CheckIcon = () => ( ); -const XIcon = () => ( - - - -); - const CopyIcon = () => ( ( ); -const ClockIcon = () => ( - - - -); - -const UserIcon = () => ( - - - -); - // Helper functions const getTypeLabel = (type: PromoCodeType): string => { const labels: Record = { @@ -120,218 +94,11 @@ const formatDate = (date: string | null): string => { }); }; -const formatDateTime = (date: string | null): string => { - if (!date) return '-'; - const localeMap: Record = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' }; - const locale = localeMap[i18n.language] || 'ru-RU'; - return new Date(date).toLocaleString(locale, { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); -}; - -// Promocode Stats Modal -interface PromocodeStatsModalProps { - promocode: PromoCodeDetail; - onClose: () => void; - onEdit: () => void; -} - -function PromocodeStatsModal({ promocode, onClose, onEdit }: PromocodeStatsModalProps) { - const { t } = useTranslation(); - return ( -
-
- {/* Header */} -
-
-
- {promocode.code} -
- - {getTypeLabel(promocode.type)} - - {!promocode.is_active && ( - - {t('admin.promocodes.stats.inactive')} - - )} -
- -
- - {/* Content */} -
- {/* Stats Cards */} -
-
-
{promocode.total_uses}
-
{t('admin.promocodes.stats.totalUses')}
-
-
-
{promocode.today_uses}
-
{t('admin.promocodes.stats.today')}
-
-
-
- {promocode.max_uses === 0 ? '∞' : promocode.uses_left} -
-
{t('admin.promocodes.stats.remaining')}
-
-
- - {/* Details */} -
-

- {t('admin.promocodes.stats.details')} -

-
-
- {t('admin.promocodes.stats.type')}: - {getTypeLabel(promocode.type)} -
- {promocode.type === 'balance' && ( -
- {t('admin.promocodes.stats.bonus')}: - - +{promocode.balance_bonus_rubles} {t('admin.promocodes.form.rub')} - -
- )} - {(promocode.type === 'subscription_days' || - promocode.type === 'trial_subscription') && ( -
- {t('admin.promocodes.stats.daysLabel')}: - +{promocode.subscription_days} -
- )} - {promocode.type === 'discount' && ( - <> -
- - {t('admin.promocodes.stats.discountLabel')}: - - -{promocode.balance_bonus_kopeks}% -
-
- {t('admin.promocodes.stats.validFor')}: - - {t('admin.promocodes.stats.hoursValue', { - count: promocode.subscription_days, - })} - -
- - )} -
- {t('admin.promocodes.stats.limit')}: - - {promocode.current_uses}/{promocode.max_uses === 0 ? '∞' : promocode.max_uses} - -
-
- {t('admin.promocodes.stats.status')}: - - {promocode.is_valid - ? t('admin.promocodes.stats.active') - : t('admin.promocodes.stats.inactive')} - -
-
- {t('admin.promocodes.stats.created')}: - {formatDateTime(promocode.created_at)} -
-
- {t('admin.promocodes.stats.validUntil')}: - - {promocode.valid_until - ? formatDate(promocode.valid_until) - : t('admin.promocodes.stats.unlimited')} - -
- {promocode.first_purchase_only && ( -
- {t('admin.promocodes.stats.restriction')}: - - {t('admin.promocodes.stats.firstPurchaseOnly')} - -
- )} -
-
- - {/* Usage History */} -
-

- - {t('admin.promocodes.stats.usageHistory')} -

- {promocode.recent_uses.length === 0 ? ( -

- {t('admin.promocodes.stats.noUsages')} -

- ) : ( -
- {promocode.recent_uses.map((use) => ( -
-
-
- -
-
-
- {use.user_full_name || use.user_username || `User #${use.user_id}`} -
- {use.user_username && ( -
@{use.user_username}
- )} -
-
-
{formatDateTime(use.used_at)}
-
- ))} -
- )} -
-
- - {/* Footer */} -
- - -
-
-
- ); -} - export default function AdminPromocodes() { const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); - const [viewingPromocode, setViewingPromocode] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState(null); const [copiedCode, setCopiedCode] = useState(null); @@ -350,15 +117,6 @@ export default function AdminPromocodes() { }, }); - const handleViewStats = async (id: number) => { - try { - const detail = await promocodesApi.getPromocode(id); - setViewingPromocode(detail); - } catch (error) { - console.error('Failed to load promocode stats:', error); - } - }; - const handleCopyCode = (code: string) => { navigator.clipboard.writeText(code); setCopiedCode(code); @@ -435,9 +193,11 @@ export default function AdminPromocodes() { promo.is_active ? 'border-dark-700' : 'border-dark-700/50 opacity-60' }`} > -
+ {/* Mobile: stacked layout, Desktop: row layout */} +
-
+ {/* Code with copy button */} +
+
+ {/* Badges - wrap on mobile */} +
{getTypeLabel(promo.type)} @@ -459,6 +222,7 @@ export default function AdminPromocodes() { )}
+ {/* Info line */}
{promo.type === 'balance' && ( @@ -491,24 +255,25 @@ export default function AdminPromocodes() {
-
+ {/* Action buttons - full width on mobile */} +
)} - {/* Promocode Stats Modal */} - {viewingPromocode && ( - setViewingPromocode(null)} - onEdit={() => { - navigate(`/admin/promocodes/${viewingPromocode.id}/edit`); - }} - /> - )} - {/* Delete Confirmation */} {deleteConfirm && (