const { useEffect, useMemo, useRef, useState } = React; const API_BASE = window.__SHOP_API_BASE__ || localStorage.getItem("shop-api-base") || (window.location.protocol === "file:" ? "http://127.0.0.1:8000/api" : "/api"); const SESSION_TOKEN_KEY = "shop-session-token"; const navItems = [ { id: "home", label: "Главная", icon: "icon-home-fill" }, { id: "favorites", label: "Избранное", icon: "icon-heart-fill" }, { id: "cart", label: "Корзина", icon: "icon-bag-fill" }, { id: "profile", label: "Профиль", icon: "icon-user-fill" }, ]; const sortModes = [ { id: "featured", label: "По популярности" }, { id: "priceAsc", label: "Цена: ниже" }, { id: "priceDesc", label: "Цена: выше" }, ]; const emptyPromoForm = { code: "", discount_type: "percent", amount: "10", description: "", usage_limit: "", is_active: true, }; const emptyProductForm = { title: "", price: "2990", category: "ФУТБОЛКИ", sort_order: "0", is_active: true, imagesText: "", sizesText: "S\nM\nL\nXL", descriptionText: "", }; let cdekWidgetScriptPromise = null; function ensureCdekWidgetScript() { if (window.CDEKWidget) { return Promise.resolve(window.CDEKWidget); } if (cdekWidgetScriptPromise) { return cdekWidgetScriptPromise; } cdekWidgetScriptPromise = new Promise((resolve, reject) => { const existing = document.querySelector('script[data-cdek-widget="1"]'); if (existing) { existing.addEventListener("load", () => resolve(window.CDEKWidget)); existing.addEventListener("error", () => reject(new Error("Не удалось загрузить виджет СДЭК"))); return; } const script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/@cdek-it/widget@3"; script.async = true; script.dataset.cdekWidget = "1"; script.onload = () => { if (window.CDEKWidget) { resolve(window.CDEKWidget); } else { reject(new Error("Виджет СДЭК загрузился некорректно")); } }; script.onerror = () => reject(new Error("Не удалось загрузить виджет СДЭК")); document.head.appendChild(script); }); return cdekWidgetScriptPromise; } function rub(value) { return `${new Intl.NumberFormat("ru-RU").format(value)} \u20bd`; } function getStoredToken() { return localStorage.getItem(SESSION_TOKEN_KEY) || ""; } function setStoredToken(token) { localStorage.setItem(SESSION_TOKEN_KEY, token); } function clearStoredToken() { localStorage.removeItem(SESSION_TOKEN_KEY); } function getTelegramInitData() { return window.Telegram?.WebApp?.initData || ""; } function isLocalPreview() { return ( window.location.protocol === "file:" || window.location.hostname === "127.0.0.1" || window.location.hostname === "localhost" ); } function parseTimestamp(value) { if (!value) { return null; } if (typeof value === "number") { return new Date(value * 1000); } return new Date(`${value.replace(" ", "T")}Z`); } function formatOrderDate(value) { const date = parseTimestamp(value); if (!date || Number.isNaN(date.getTime())) { return "\u2014"; } return new Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", }).format(date); } function getDisplayName(user) { if (!user) { return "Покупатель"; } return ( user.display_name || [user.first_name, user.last_name].filter(Boolean).join(" ").trim() || (user.username ? `@${user.username}` : "Покупатель") ); } function getInitials(user) { const name = getDisplayName(user).replace("@", "").trim(); const parts = name.split(/\s+/).filter(Boolean); const letters = parts.slice(0, 2).map((part) => part[0]?.toUpperCase() || ""); return letters.join("") || "TG"; } async function copyText(text) { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return; } const input = document.createElement("textarea"); input.value = text; input.setAttribute("readonly", ""); input.style.position = "absolute"; input.style.left = "-9999px"; document.body.appendChild(input); input.select(); document.execCommand("copy"); document.body.removeChild(input); } async function apiRequest(path, options = {}) { const headers = { "Content-Type": "application/json", ...(options.headers || {}), }; if (!options.skipAuth) { const token = getStoredToken(); if (token) { headers.Authorization = `Bearer ${token}`; } } const response = await fetch(`${API_BASE}${path}`, { ...options, headers, }); if (!response.ok) { const payload = await response.json().catch(() => ({})); if (response.status === 401 && !options.skipAuth) { clearStoredToken(); } const detail = payload.detail; const error = new Error( typeof detail === "string" ? detail : payload.message || `Ошибка API (${response.status})` ); error.status = response.status; error.payload = payload; error.path = path; throw error; } return response.json(); } function makeCartKey(productId, size = "") { return `${productId}::${size || ""}`; } function buildCartIndex(items) { return Object.fromEntries(items.map((item) => [item.key, item])); } function normalizeState(payload) { const favorites = payload.favorites || []; const cartItems = (payload.cart || []).map((item) => ({ ...item, size: item.size || "", key: item.key || makeCartKey(item.product_id, item.size || ""), })); return { user: payload.user || null, favorites, cartItems, cartIndex: buildCartIndex(cartItems), orders: payload.orders || [], supportUrl: payload.support_url || null, supportUsername: payload.support_username || null, }; } function Icon({ id }) { return ( ); } function CatalogHeader({ activeCategory, searchOpen, searchQuery, onSearchToggle, onSearchChange, onOpenCategories, onCycleSort, onOpenProfile, categoryButtons, }) { return ( <>

TEST

{categoryButtons}
{searchOpen ? (
) : null}
); } function ProductCard({ product, isFavorite, onToggleFavorite, onOpen }) { return (
{product.images.map((_, index) => ( ))}
); } function FavoritesEmpty({ onBackToShop }) { return (

В избранном пока пусто

Сохраняйте понравившиеся вещи, чтобы быстро вернуться к ним позже.

); } const checkoutSteps = [ { id: "contact", label: "Контакты" }, { id: "delivery", label: "Доставка" }, { id: "shipping", label: "Способ доставки" }, { id: "payment", label: "Оплата" }, { id: "review", label: "Подтверждение" }, ]; function formatCdekEta(tariff) { if (!tariff) { return ""; } if (tariff.period_min && tariff.period_max) { if (tariff.period_min === tariff.period_max) { return `${tariff.period_max} дн.`; } return `${tariff.period_min}-${tariff.period_max} дн.`; } if (tariff.period_max) { return `${tariff.period_max} дн.`; } return ""; } function getOrderStatusLabel(status, paymentProvider) { const normalized = (status || "").toLowerCase(); if (normalized === "paid") { return "Оплачен"; } if (normalized === "pending_payment") { return "Ожидает оплаты"; } if (normalized === "new" || normalized === "manual_review") { return paymentProvider === "manual" ? "Нужно подтвердить" : "Новый заказ"; } if (normalized === "processing") { return "В обработке"; } if (normalized === "completed") { return "Завершен"; } return "Заказ создан"; } function CheckoutFlow({ open, step, draft, options, cartItems, productMap, couponCode, promoPreview, applyingPromo, submitting, onClose, onBack, onAdvance, onSubmit, onChange, onDeliveryPicked, onCouponChange, onApplyPromo, }) { const widgetRef = useRef(null); const deliveryPickedRef = useRef(onDeliveryPicked); const [widgetLoading, setWidgetLoading] = useState(false); const [widgetError, setWidgetError] = useState(""); const widgetConfig = options?.cdek_widget || {}; const deliveryChosen = Boolean(draft.delivery_code && draft.delivery_method_id && draft.delivery_address); const totalItems = cartItems.reduce((sum, item) => sum + item.quantity, 0); const itemsAmount = cartItems.reduce((sum, item) => { const product = productMap[item.product_id]; return sum + (product ? product.price * item.quantity : 0); }, 0); const discountAmount = promoPreview?.coupon_applied ? promoPreview.discount_amount || 0 : 0; const deliveryAmount = draft.delivery_amount || 0; const finalTotal = (promoPreview?.coupon_applied ? promoPreview.total_amount || Math.max(0, itemsAmount - discountAmount) : itemsAmount) + deliveryAmount; const activeStepIndex = checkoutSteps.findIndex((item) => item.id === step); const reviewDeliveryAddress = draft.delivery_address || ""; useEffect(() => { deliveryPickedRef.current = onDeliveryPicked; }, [onDeliveryPicked]); useEffect(() => { let cancelled = false; async function initWidget() { if (!open || !widgetConfig.configured) { return; } setWidgetLoading(true); setWidgetError(""); try { const CDEKWidget = await ensureCdekWidgetScript(); if (cancelled) { return; } if (widgetRef.current?.destroy) { widgetRef.current.destroy(); } widgetRef.current = new CDEKWidget({ apiKey: widgetConfig.api_key, servicePath: widgetConfig.service_path, popup: true, canChoose: true, defaultLocation: draft.contact_city || widgetConfig.default_location || "Чебоксары", from: widgetConfig.from, goods: widgetConfig.goods || [], lang: "rus", currency: "RUB", hideDeliveryOptions: draft.delivery_mode === "pickup" ? { door: true } : { office: true }, onChoose(type, tariff, address) { const mode = type === "door" ? "courier" : "pickup"; const pointLabel = address?.name || address?.office_name || (mode === "courier" ? "Адрес доставки" : "Пункт выдачи СДЭК"); const label = tariff?.tariff_name || tariff?.name || (mode === "courier" ? "Курьерская доставка" : "Посылка склад-склад"); const normalizedAddress = mode === "courier" ? address?.formatted || address?.address || draft.delivery_address || "" : address?.address || address?.formatted || ""; const normalizedCode = address?.code || address?.office_code || address?.city_code || normalizedAddress || `${mode}-${Date.now()}`; const cityCode = address?.city_code || address?.location?.city_code || address?.city?.code || ""; const officeCode = address?.code || address?.office_code || ""; deliveryPickedRef.current?.({ delivery_mode: mode, delivery_code: String(normalizedCode), delivery_point_label: pointLabel, delivery_address: normalizedAddress, delivery_city_code: cityCode ? String(cityCode) : "", delivery_office_code: officeCode ? String(officeCode) : "", delivery_method_id: String(tariff?.tariff_code || tariff?.id || mode), delivery_label: label, delivery_eta: formatCdekEta(tariff), delivery_amount: Number(tariff?.delivery_sum || 0), }); try { this.close(); } catch (err) { console.warn("Не удалось закрыть виджет СДЭК автоматически.", err); } }, }); } catch (err) { if (!cancelled) { setWidgetError(err.message || "Не удалось инициализировать виджет СДЭК"); } } finally { if (!cancelled) { setWidgetLoading(false); } } } initWidget(); return () => { cancelled = true; if (widgetRef.current?.destroy) { widgetRef.current.destroy(); } widgetRef.current = null; }; }, [ open, widgetConfig.configured, widgetConfig.api_key, widgetConfig.service_path, widgetConfig.from, widgetConfig.goods, widgetConfig.default_location, draft.contact_city, draft.delivery_mode, ]); if (!open || !options) { return null; } function openDeliveryWidget() { if (!widgetConfig.configured) { setWidgetError( "Добавьте на сервере YANDEX_MAPS_API_KEY, CDEK_INTEGRATION_ACCOUNT и CDEK_INTEGRATION_PASSWORD, чтобы включить выбор доставки." ); return; } if (!widgetRef.current?.open) { setWidgetError("Виджет СДЭК еще не успел загрузиться. Попробуйте еще раз через секунду."); return; } setWidgetError(""); widgetRef.current.open(); } return (
TEST
{checkoutSteps.map((item, index) => ( ))}
{step === "contact" ? (

Контактные данные

Ваши данные
🇷🇺
Комментарий к заказу

Эти данные нужны, чтобы мы могли оформить и доставить ваш заказ.

) : null} {step === "delivery" ? (
{draft.delivery_mode === "pickup" ? "СДЭК • ПВЗ" : "СДЭК • КУРЬЕР"}

{draft.delivery_mode === "pickup" ? "Выберите удобный пункт выдачи" : "Выберите адрес и способ доставки"}

Официальный виджет СДЭК откроется поверх экрана и вернет готовый вариант доставки в оформление.

{!deliveryChosen && !widgetError ? (
После выбора мы автоматически подставим адрес, срок и стоимость на следующий шаг.
) : null} {widgetError ?
{widgetError}
: null} {deliveryChosen ? (
{draft.delivery_label} {rub(deliveryAmount)}
{reviewDeliveryAddress}
{draft.delivery_mode === "pickup" ? "Пункт выдачи" : "Курьер"} {draft.delivery_eta ? ( {draft.delivery_eta} ) : null}
) : null}
) : null} {step === "shipping" ? (

Способ доставки

Выбранная доставка
{draft.delivery_label || "Способ доставки не выбран"} {draft.delivery_eta || "Срок появится после выбора в виджете"} {rub(deliveryAmount)}
{reviewDeliveryAddress || "Откройте предыдущий шаг и выберите ПВЗ или адрес"}
) : null} {step === "payment" ? (

Оплата

Способ оплаты
) : null} {step === "review" ? (

Подтверждение заказа

Контактные данные
{draft.contact_first_name} {draft.contact_last_name} {draft.contact_phone} {draft.contact_email}
Доставка {draft.delivery_label || (draft.delivery_mode === "pickup" ? "СДЭК" : "Курьер")}: {reviewDeliveryAddress}
{draft.delivery_eta || "Доставка"}
{cartItems.map((item) => { const product = productMap[item.product_id]; if (!product) { return null; } return (
{product.title}
); })}
{draft.delivery_label || "Доставка"} {rub(deliveryAmount)}
Способ оплаты
Подтверждение через менеджера
Комментарий к заказу
{draft.order_note || "Без комментария"}
onCouponChange(event.target.value.toUpperCase())} />
{promoPreview?.promo_error ? (
{promoPreview.promo_error}
) : null} {promoPreview?.coupon_applied ? (
Промокод {promoPreview.coupon_code} применен, скидка {rub(discountAmount)}
) : null}
Товары ({totalItems}) {rub(itemsAmount)}
Доставка {rub(deliveryAmount)}
{discountAmount > 0 ? (
Скидка -{rub(discountAmount)}
) : null}
Итого {rub(finalTotal)}

Нажимая кнопку, вы соглашаетесь на обработку персональных данных

) : null}
{step === "contact" ? ( ) : null} {step === "delivery" ? ( ) : null} {step === "shipping" ? ( ) : null} {step === "payment" ? ( ) : null} {step === "review" ? ( ) : null}
); } function CartScreen({ cartItems, productMap, couponCode, promoPreview, applyingPromo, onCouponChange, onApplyPromo, onClearCart, onIncrement, onDecrement, onCheckout, }) { if (!cartItems.length) { return (

Корзина пока пустая

Добавьте товары в корзину, чтобы оформить заказ.

); } const totalItems = cartItems.reduce((sum, item) => sum + item.quantity, 0); const subtotalAmount = cartItems.reduce((sum, item) => { const product = productMap[item.product_id]; return sum + (product ? product.price * item.quantity : 0); }, 0); const discountAmount = promoPreview?.coupon_applied ? promoPreview.discount_amount || 0 : 0; const totalAmount = promoPreview?.coupon_applied ? promoPreview.total_amount || Math.max(0, subtotalAmount - discountAmount) : subtotalAmount; const appliedCouponCode = promoPreview?.coupon_applied ? promoPreview.coupon_code : ""; return (

Корзина

{cartItems.map((item) => { const product = productMap[item.product_id]; if (!product) { return null; } return (
{product.title}

{product.title}

Размер одежды {item.size || "Единый"}
{item.quantity}
{rub(product.price * item.quantity)}
); })}
onCouponChange(event.target.value.toUpperCase())} />
{promoPreview?.promo_error ? (
{promoPreview.promo_error}
) : null} {promoPreview?.coupon_applied ? (
Промокод {appliedCouponCode} применен, скидка {rub(discountAmount)}
) : null}
Товары ({totalItems}) {rub(subtotalAmount)}
{discountAmount > 0 ? (
Скидка -{rub(discountAmount)}
) : null}
Итого {rub(totalAmount)}
); } function ProfileScreen({ user, orders, favoritesCount, cartCount, productMap, supportUrl, supportUsername, onOpenAdmin, onResumeOrderPayment, }) { const displayName = getDisplayName(user); const username = user?.username ? `@${user.username}` : "не указан"; const authDate = user?.last_auth_date ? formatOrderDate(user.last_auth_date) : "\u2014"; return (
{user?.photo_url ? ( {displayName} ) : (
{getInitials(user)}
)}

{displayName}

{username}
Заказы {orders.length}
Избранное {favoritesCount}
Товары в корзине {cartCount}
Профиль
Имя пользователя {username}
Язык {user?.language_code || "ru"}
Последний вход {authDate}
{supportUsername ? (
Поддержка @{supportUsername}
) : null} {user?.is_admin ? ( ) : null}
История заказов
{orders.length ? (
{orders.map((order) => (
Заказ #{order.id}
{formatOrderDate(order.created_at)}
{rub(order.total_amount)}
{getOrderStatusLabel(order.status, order.payment_provider)}
{order.items.map((item, index) => { const product = productMap[item.product_id]; return (
{product?.title || `Товар #${item.product_id}`} {item.quantity} × {rub(item.unit_price)}
); })}
{order.coupon_code ?
Промокод: {order.coupon_code}
: null} {order.status !== "paid" && order.payment_provider === "robokassa" ? ( ) : null}
))}
) : (
У вас пока нет заказов. Когда оформите первую покупку, она появится здесь.
)}
); } function AdminScreen({ dashboard, loading, promoForm, productForm, editingPromoId, editingProductId, onPromoFormChange, onProductFormChange, onSavePromo, onEditPromo, onResetPromo, onSaveProduct, onEditProduct, onToggleProduct, onResetProduct, onRefresh, }) { if (loading) { return (

Загружаем данные

Еще немного, и все будет готово.

); } return (

Админка

Заказы, промокоды и управление товарами

Все заказы
{dashboard.orders.length ? ( dashboard.orders.map((order) => (
Заказ #{order.id}
{order.customer.display_name} {order.customer.username ? ` • @${order.customer.username}` : ""}
{rub(order.total_amount)}
{formatOrderDate(order.created_at)}
{order.coupon_code ? (
Промокод: {order.coupon_code} • скидка {rub(order.discount_amount || 0)}
) : null}
{order.items.map((item, index) => (
{item.product_title || `Товар #${item.product_id}`} {item.quantity} × {rub(item.unit_price)} {item.size ? `• ${item.size}` : ""}
))}
)) ) : (
Заказов пока нет.
)}
Промокоды
{editingPromoId ? ( ) : null}
{dashboard.promoCodes.map((promo) => (
{promo.code}
{promo.discount_type === "percent" ? "процент" : "фикс"} • {promo.amount} {promo.usage_limit ? ` • ${promo.uses_count}/${promo.usage_limit}` : ` • использован ${promo.uses_count} раз`}
{promo.is_active ? "активен" : "выключен"}
))}
Товары