From f73579b37fc2b1d9713148aae25b1cab8fe71569 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Mon, 2 Feb 2026 06:14:15 +0300 Subject: [PATCH] refactor: extract promo offers modals to separate pages - Create AdminPromoOfferTemplateEdit.tsx for template editing - Create AdminPromoOfferSend.tsx for sending offers with result screen - Add routes for /admin/promo-offers/templates/:id/edit and /admin/promo-offers/send - Simplify AdminPromoOffers.tsx by removing modals - Add i18n keys for new pages --- src/App.tsx | 22 + src/locales/en.json | 4 + src/locales/fa.json | 4 + src/locales/ru.json | 4 + src/locales/zh.json | 4 + src/pages/AdminPromoOfferSend.tsx | 382 +++++++++++++ src/pages/AdminPromoOfferTemplateEdit.tsx | 276 ++++++++++ src/pages/AdminPromoOffers.tsx | 622 +--------------------- 8 files changed, 710 insertions(+), 608 deletions(-) create mode 100644 src/pages/AdminPromoOfferSend.tsx create mode 100644 src/pages/AdminPromoOfferTemplateEdit.tsx diff --git a/src/App.tsx b/src/App.tsx index d5988a1..50482aa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -51,6 +51,8 @@ const AdminUsers = lazy(() => import('./pages/AdminUsers')); const AdminPayments = lazy(() => import('./pages/AdminPayments')); const AdminPaymentMethods = lazy(() => import('./pages/AdminPaymentMethods')); const AdminPromoOffers = lazy(() => import('./pages/AdminPromoOffers')); +const AdminPromoOfferTemplateEdit = lazy(() => import('./pages/AdminPromoOfferTemplateEdit')); +const AdminPromoOfferSend = lazy(() => import('./pages/AdminPromoOfferSend')); const AdminRemnawave = lazy(() => import('./pages/AdminRemnawave')); const AdminEmailTemplates = lazy(() => import('./pages/AdminEmailTemplates')); const AdminUserDetail = lazy(() => import('./pages/AdminUserDetail')); @@ -494,6 +496,26 @@ function App() { } /> + + + + + + } + /> + + + + + + } + /> ( + + + +); + +const CheckIcon = () => ( + + + +); + +const UsersIcon = () => ( + + + +); + +const UserIcon = () => ( + + + +); + +const getOfferTypeIcon = (offerType: string): string => { + return OFFER_TYPE_CONFIG[offerType as OfferType]?.icon || '🎁'; +}; + +export default function AdminPromoOfferSend() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const [selectedTemplateId, setSelectedTemplateId] = useState(null); + const [sendMode, setSendMode] = useState<'segment' | 'user'>('segment'); + const [selectedTarget, setSelectedTarget] = useState('active'); + const [userId, setUserId] = useState(''); + const [result, setResult] = useState<{ + title: string; + message: string; + isSuccess: boolean; + } | null>(null); + + // Query templates + const { data: templatesData, isLoading } = useQuery({ + queryKey: ['admin-promo-templates'], + queryFn: promoOffersApi.getTemplates, + }); + + const templates = templatesData?.items || []; + const activeTemplates = templates.filter((t) => t.is_active); + const selectedTemplate = templates.find((t) => t.id === selectedTemplateId); + + // Set default template when loaded + if (!selectedTemplateId && activeTemplates.length > 0) { + setSelectedTemplateId(activeTemplates[0].id); + } + + // Broadcast mutation + const broadcastMutation = useMutation({ + mutationFn: promoOffersApi.broadcastOffer, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['admin-promo-logs'] }); + + let message = t('admin.promoOffers.result.offersCreated', { count: data.created_offers }); + if (data.notifications_sent > 0 || data.notifications_failed > 0) { + message += + '\n' + + t('admin.promoOffers.result.notificationsSent', { count: data.notifications_sent }); + if (data.notifications_failed > 0) { + message += + ' ' + + t('admin.promoOffers.result.notificationsFailed', { + count: data.notifications_failed, + }); + } + } + + setResult({ + title: t('admin.promoOffers.result.sentTitle'), + message, + isSuccess: true, + }); + }, + onError: (error: unknown) => { + const axiosErr = error as { response?: { data?: { detail?: string } } }; + setResult({ + title: t('common.error'), + message: axiosErr.response?.data?.detail || t('admin.promoOffers.result.sendError'), + isSuccess: false, + }); + }, + }); + + const handleSubmit = () => { + if (!selectedTemplateId || !selectedTemplate) return; + + const data: PromoOfferBroadcastRequest = { + notification_type: selectedTemplate.offer_type, + valid_hours: selectedTemplate.valid_hours, + discount_percent: selectedTemplate.discount_percent, + effect_type: + selectedTemplate.offer_type === 'test_access' ? 'test_access' : 'percent_discount', + extra_data: { + template_id: selectedTemplate.id, + active_discount_hours: selectedTemplate.active_discount_hours, + test_duration_hours: selectedTemplate.test_duration_hours, + test_squad_uuids: selectedTemplate.test_squad_uuids, + }, + send_notification: true, + message_text: selectedTemplate.message_text, + button_text: selectedTemplate.button_text, + }; + + if (sendMode === 'user') { + const id = parseInt(userId); + if (!id) return; + data.telegram_id = id; + } else { + data.target = selectedTarget; + } + + broadcastMutation.mutate(data); + }; + + const isValid = () => { + if (!selectedTemplateId) return false; + if (sendMode === 'user' && !userId.trim()) return false; + return true; + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + // Result screen + if (result) { + return ( +
+
+
+ {result.isSuccess ? ( + + + + ) : ( + + + + )} +
+

{result.title}

+

{result.message}

+
+ + {result.isSuccess && ( + + )} +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+ +
+
+ +
+

+ {t('admin.promoOffers.send.title')} +

+
+
+ + {activeTemplates.length === 0 ? ( +
+

{t('admin.promoOffers.noActiveTemplates')}

+
+ ) : ( +
+ {/* Template Selection */} +
+ +
+ {activeTemplates.map((template) => ( + + ))} +
+
+ + {/* Send Mode */} +
+ +
+ + +
+ + {sendMode === 'segment' ? ( + + ) : ( + setUserId(e.target.value)} + placeholder={t('admin.promoOffers.send.userIdPlaceholder')} + className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2.5 text-dark-100 focus:border-accent-500 focus:outline-none" + /> + )} +
+ + {/* Preview */} + {selectedTemplate && ( +
+

+ {t('admin.promoOffers.send.preview')} +

+
+
+ {selectedTemplate.message_text} +
+
+ + {selectedTemplate.button_text} + +
+
+
+ )} + + {/* Actions */} +
+ + +
+
+ )} +
+ ); +} diff --git a/src/pages/AdminPromoOfferTemplateEdit.tsx b/src/pages/AdminPromoOfferTemplateEdit.tsx new file mode 100644 index 0000000..78c984f --- /dev/null +++ b/src/pages/AdminPromoOfferTemplateEdit.tsx @@ -0,0 +1,276 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { + promoOffersApi, + PromoOfferTemplateUpdateRequest, + OFFER_TYPE_CONFIG, + OfferType, +} from '../api/promoOffers'; +import { AdminBackButton } from '../components/admin'; +import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; + +const getOfferTypeIcon = (offerType: string): string => { + return OFFER_TYPE_CONFIG[offerType as OfferType]?.icon || '🎁'; +}; + +export default function AdminPromoOfferTemplateEdit() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const queryClient = useQueryClient(); + + // Form state + const [name, setName] = useState(''); + const [messageText, setMessageText] = useState(''); + const [buttonText, setButtonText] = useState(''); + const [validHours, setValidHours] = useState(24); + const [discountPercent, setDiscountPercent] = useState(0); + const [activeDiscountHours, setActiveDiscountHours] = useState(0); + const [testDurationHours, setTestDurationHours] = useState(0); + const [isActive, setIsActive] = useState(true); + const [isTestAccess, setIsTestAccess] = useState(false); + + // Query template + const { data: templatesData, isLoading } = useQuery({ + queryKey: ['admin-promo-templates'], + queryFn: promoOffersApi.getTemplates, + }); + + const template = templatesData?.items.find((t) => t.id === Number(id)); + + // Populate form when template loads + useEffect(() => { + if (template) { + setName(template.name); + setMessageText(template.message_text); + setButtonText(template.button_text); + setValidHours(template.valid_hours); + setDiscountPercent(template.discount_percent); + setActiveDiscountHours(template.active_discount_hours || 0); + setTestDurationHours(template.test_duration_hours || 0); + setIsActive(template.is_active); + setIsTestAccess(template.offer_type === 'test_access'); + } + }, [template]); + + // Update mutation + const updateMutation = useMutation({ + mutationFn: (data: PromoOfferTemplateUpdateRequest) => + promoOffersApi.updateTemplate(Number(id), data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin-promo-templates'] }); + navigate('/admin/promo-offers'); + }, + }); + + const handleSubmit = () => { + const data: PromoOfferTemplateUpdateRequest = { + name, + message_text: messageText, + button_text: buttonText, + valid_hours: toNumber(validHours, 1), + discount_percent: toNumber(discountPercent), + is_active: isActive, + }; + const testHours = toNumber(testDurationHours); + const discountHours = toNumber(activeDiscountHours); + if (isTestAccess) { + data.test_duration_hours = testHours > 0 ? testHours : undefined; + } else { + data.active_discount_hours = discountHours > 0 ? discountHours : undefined; + } + updateMutation.mutate(data); + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!template) { + return ( +
+
+ +

+ {t('admin.promoOffers.form.editTemplate')} +

+
+
+

{t('admin.promoOffers.notFound')}

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+ +
+ {getOfferTypeIcon(template.offer_type)} +

+ {t('admin.promoOffers.form.editTemplate')} +

+
+
+ + {/* Form */} +
+
+
+
+ + setName(e.target.value)} + className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-accent-500 focus:outline-none" + /> +
+ +
+ +