// pages-system.jsx — Requisites + Content + Settings (NEW) const { useState: useStateS, useMemo: useMemoS, useRef: useRefS, useEffect: useEffectS } = React; /* ============================ REQUISITES — NEW ============================ */ const TYPE_META = { card: { icon: "card", label: "Карта", color: "from-indigo-300 to-indigo-500" }, crypto: { icon: "bitcoin", label: "Крипто", color: "from-amber-400 to-amber-600" }, stars: { icon: "star", label: "TG Stars", color: "from-rose-400 to-rose-600" }, payment: { icon: "card", label: "PayPal/иное", color: "from-emerald-400 to-emerald-600" }, }; function RequisitesPage() { const [list, setList] = useStateS([]); const [loading, setLoading] = useStateS(true); const [selected, setSelected] = useStateS(null); const [filter, setFilter] = useStateS("all"); // Группа реквизитов: "" = общие (мои аккаунты без владельца), иначе ownerId // владельца (напр. Рома). Каждый владелец может иметь свой набор карт; его // аккаунты в боте принимают оплату на них (workspace резолвит по ownerId). const [group, setGroup] = useStateS(""); const [groups, setGroups] = useStateS([]); const [dirty, setDirty] = useStateS(false); const loadGroups = React.useCallback(async () => { setGroups(await fetchPaymentGroups()); }, []); useEffectS(() => { loadGroups(); }, [loadGroups]); const activeGroup = groups.find((g) => g.id === group) || groups[0] || { id: "", name: "Общие", own: true, count: 0 }; // Редактируем, если это общие ИЛИ владелец включил свои реквизиты. Иначе показ // копии общих как превью (без записи), пока не нажал «Задать свои». const editable = !group || !!activeGroup.own; // ВАЖНО: НЕТ polling — это страница редактирования. Polling перезатирал бы ввод. const reload = React.useCallback(async () => { setLoading(true); const data = await fetchPayments(group); setList(data); setLoading(false); setDirty(false); setSelected((cur) => cur || (data[0] ? data[0].id : null)); }, [group]); useEffectS(() => { let stopped = false; setLoading(true); (async () => { const data = await fetchPayments(group); if (stopped) return; setList(data); setLoading(false); setDirty(false); setSelected((cur) => cur || (data[0] ? data[0].id : null)); })(); return () => { stopped = true; }; }, [group]); const onToggleOwn = async (enabled) => { if (!group) return; await toggleOwnerPayments(group, enabled); await loadGroups(); await reload(); }; const filtered = useMemoS(() => { if (filter === "all") return list; return list.filter((r) => r.type === filter); }, [list, filter]); // Расфасовка списка по валютам: свой блок на гривну, свой на рубли и т.д. // СБП попадает к рублям (это рублёвый способ), а монеты (USDT/BTC/TON) сведены // в один блок «Крипта» — это способы оплаты, а не отдельные валюты прайса. const CURRENCY_SECTIONS = [ { id: "UAH", title: "Гривна", sign: "₴", match: (r) => r.currency === "UAH" }, { id: "RUB", title: "Рубль", sign: "₽", match: (r) => r.currency === "RUB" }, { id: "USD", title: "Доллар", sign: "$", match: (r) => r.currency === "USD" }, { id: "CRYPTO", title: "Крипта", sign: "₿", match: (r) => r.type === "crypto" }, { id: "STARS", title: "Telegram Stars", sign: "★", match: (r) => r.type === "stars" }, ]; const grouped = useMemoS(() => { const used = new Set(); const out = []; CURRENCY_SECTIONS.forEach((s) => { const rows = filtered.filter((r) => !used.has(r.id) && s.match(r)); rows.forEach((r) => used.add(r.id)); if (rows.length) out.push({ id: s.id, title: s.title, sign: s.sign, rows }); }); // Всё, что не подошло ни под один блок, показываем отдельно — чтобы реквизит // не пропал из вкладки молча. const rest = filtered.filter((r) => !used.has(r.id)); if (rest.length) out.push({ id: "OTHER", title: "Прочее", sign: "•", rows: rest }); return out; }, [filtered]); const current = list.find((r) => r.id === selected); const totalActive = list.filter((r) => r.active).length; const totalPaid = list.reduce((s, r) => s + r.paid7d, 0); const topMethod = list.reduce( (acc, r) => (r.sum7d > (acc?.sum7d || 0) ? r : acc), null, ); const toggleActive = (id) => { if (!editable) return; // группа на общих реквизитах — только просмотр setList(list.map((r) => (r.id === id ? { ...r, active: !r.active } : r))); setDirty(true); }; const updateCurrent = (patch) => { if (!editable) return; setList(list.map((r) => (r.id === selected ? { ...r, ...patch } : r))); setDirty(true); }; return (
{/* «Применить ко всем» убрано: реквизиты теперь глобальные (единый источник defaults), правка применяется ко всем аккаунтам автоматически. */} {/* Dirty-dot: визуальный маркер несохранённых изменений */} {editable && (
{dirty && ( )} { await savePayments(group, list); // После save — refetch чтобы paid7d/sum7d стали свежими await reload(); }} label={dirty ? "Сохранить все •" : "Сохранить все"} />
)}
{groups.length > 1 && (
{groups.map((g) => { const on = g.id === group; return ( ); })} группы = владельцы из «Аккаунтов»
)} {group && !activeGroup.own && (
Аккаунты «{activeGroup.name}» ({activeGroup.count}) принимают оплату на общие реквизиты. Ниже — их копия для примера, редактирование выключено.
)} {group && !!activeGroup.own && (
«{activeGroup.name}» ({activeGroup.count} акк.) принимают на свои реквизиты. Правь карты ниже — общие не затронутся.
)}
Топ-метод
{topMethod ? topMethod.bank : "—"}
{topMethod ? `${topMethod.paid7d} оплат · ${topMethod.sum7d.toLocaleString("ru")} ${topMethod.currency}` : "нет данных"}
Скрыты
{list.length - totalActive}
не используются ботом
Все реквизиты
{loading && Array.from({ length: 5 }).map((_, i) => (
))} {!loading && filtered.length === 0 && (
Реквизиты не настроены для этого аккаунта
)} {!loading && grouped.map((section) => (
{section.sign} {section.title} · {section.rows.length}
{section.rows.map((r) => { const isActive = selected === r.id; const meta = TYPE_META[r.type] || TYPE_META.card; return (
setSelected(r.id)} className={"px-4 py-3.5 flex items-center gap-4 cursor-pointer transition-colors " + (isActive ? "bg-[rgba(231,220,196,0.55)]" : "hover:bg-black/[0.03]") + (!r.active ? " opacity-60" : "")} >
{r.bank}
{r.currency} {!r.active && скрыт}
{r.number}
{r.paid7d}
7 дней
e.stopPropagation()}> toggleActive(r.id)} />
); })}
))}
{!current && !loading && } {current && (
{current.bank}
{(TYPE_META[current.type] || TYPE_META.card).label} · {current.currency}
toggleActive(current.id)} />
{/* Visual card */}
{current.bank}
{current.number || "—"}
Метка
{current.label || "—"}
Валюта
{current.currency}
Оплат за 7 дней
{current.paid7d}
Сумма за 7 дней
{current.sum7d.toLocaleString("ru")} {current.currency === "RUB" ? "₽" : current.currency === "UAH" ? "₴" : current.currency === "USDT" ? "$" : "⭐"}
updateCurrent({ bank: e.target.value })} /> updateCurrent({ number: e.target.value })} /> updateCurrent({ label: e.target.value })} /> {/* Прогрев — только для банковских карт (грн/руб). Крипта/PayPal/ Stars не греются. Карта наращивает оборот аккуратно: не больше N чеков в сутки + пауза после каждого, иначе бот её не выдаёт. */} {current.type === "card" && (
{current.warmup && ( <>
Чеков в сутки (макс)
updateCurrent({ warmupDailyLimit: Math.max(1, parseInt(e.target.value) || 1) })} className="w-full px-3 py-2 rounded-lg bg-white/50 border border-black/10 text-sm text-zinc-100 outline-none focus:border-[rgba(139,195,74,0.6)]" />
Пауза, часов
updateCurrent({ warmupPauseHours: Math.max(0, parseFloat(e.target.value) || 0) })} className="w-full px-3 py-2 rounded-lg bg-white/50 border border-black/10 text-sm text-zinc-100 outline-none focus:border-[rgba(139,195,74,0.6)]" />
Бот выдаёт эту карту не чаще {current.warmupDailyLimit ?? 3} раз в сутки, и после каждого присланного чека делает паузу {current.warmupPauseHours ?? 2} ч. Пока карта «остывает» — бот берёт другую карту той же валюты, а если других нет — уводит на другой способ.
)}
)}
{/* «Скопировать», «Удалить», нижняя «Сохранить» раньше были без onClick = silent fail. «Сохранить» особенно сбивал с толку (есть верхняя «Сохранить все» в SectionTitle которая реально шлёт savePayments). Оставлена только реально работающая Copy, остальные disabled с tooltip'ом. */}
Изменения сохраняются через «Сохранить все» в шапке страницы
)}
); } /* ============================ CONTENT — NEW ============================ */ function ChannelDeliveryEditor() { // «Текст выдачи после оплаты» (private_channel_text). Таргетит базовые // настройки (в этой панели нет per-account селектора). Отправляется клиенту // КАК ЕСТЬ после оплаты канала (Phase 3). const [text, setText] = useStateS(""); const [loaded, setLoaded] = useStateS(false); const [saving, setSaving] = useStateS(false); const [msg, setMsg] = useStateS(""); useEffectS(() => { let stop = false; (async () => { const r = await fetchChannelText(null); if (!stop) { setText((r && r.text) || ""); setLoaded(true); } })(); return () => { stop = true; }; }, []); const save = async () => { setSaving(true); setMsg(""); try { await saveChannelText(null, text); setMsg("Сохранено"); } catch (e) { setMsg("Ошибка: " + (e && e.message ? e.message : e)); } finally { setSaving(false); setTimeout(() => setMsg(""), 4000); } }; return (
Текст выдачи после оплаты
Отправляется клиенту как есть после оплаты канала: ссылка, несколько ссылок, инструкция, пароль, правила — любой текст. Пусто — продажа разрешена, доступ бот честно отложит.