Commit Graph

367 Commits

Author SHA1 Message Date
Egor
eeb715c095 Merge PR #484: поле icon_custom_emoji_id у кастомных кнопок рассылки
feat(broadcasts): поле icon_custom_emoji_id у кастомных кнопок рассылки
2026-07-13 15:13:22 +03:00
Fringg
f9ee16a2e6 feat(admin): бейдж «Команда» для команд бота в таймлайне активности
Команды (/start и др.) приходят типом button_click с подтипом command —
добавлен перевод бейджа во всех 4 локалях.
2026-07-13 04:19:32 +03:00
Fringg
5fc8e7f97a feat(admin): клики кнопок бота и действия в кабинете во вкладке «Активность»
Два новых типа записей таймлайна: button_click (нажатия кнопок в боте,
с текстом кнопки) и cabinet_action (мутационные действия юзера в
кабинете, метод + нормализованный путь). Чип-фильтр «Клики», иконки
BotIcon/BoltIcon, переводы во всех 4 локалях.

biome check, type-check и build проходят (pre-commit пропущен: biome
не установлен локально).
2026-07-13 03:46:53 +03:00
Fringg
7547b49d7a feat(admin): вкладка «Активность» в карточке пользователя
Единый таймлайн действий юзера в боте и кабинете
(GET /cabinet/admin/users/{id}/activity): вертикальная лента с
иконками-кружками по типу записи, бейджи подтипов, суммы со знаком
(+зелёный / −красный), относительное время (абсолютное — в title),
чипы-фильтры по категориям (платежи, события, промо, тикеты, подарки,
рефералы, входы), счётчик StatCard и постраничная догрузка по house
load-more паттерну. Компонент самодостаточный (ActivityTab), как
TicketsTab. Переводы во всех 4 локалях; время переиспользует
admin.auditLog.time.*.

biome check, type-check и build проходят (pre-commit пропущен: biome
не установлен локально — бинарь есть только в CI).
2026-07-13 03:34:07 +03:00
kewldan
b4b4bca2cb feat(broadcasts): поле icon_custom_emoji_id для кастомных кнопок рассылки 2026-07-13 02:34:46 +03:00
Fringg
758a645dff Merge PR #479: счётчик символов и предупреждение о разбивке длинных текстов
Парный к боту #3049. В /admin/legal-pages под полем политики/оферты/правил:
постоянный счётчик символов + предупреждение с оценкой числа сообщений бота
(estimateTelegramParts зеркалит split_telegram_text, порог 3500). Скрыто там,
где документ в боте не показывается (recurrent-payments, web-only); счётчик
остаётся. Переводы ru/en с плюральными формами, прочие локали — en fallback.

Валидация: JSON-ключи, tsc, biome lint/format, build — чисто.
2026-07-12 23:57:05 +03:00
kewldan
93cfca46dc feat(admin): предупреждение о разбивке длинных текстов системных страниц в боте
Кабинет, в отличие от админки бота (лимит 4000 символов), позволяет
сохранять юридические тексты любой длины — и до парного фикса в боте
(BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot#3049) текст длиннее
~4096 символов ронял показ правил и политики при /start
(MESSAGE_TOO_LONG).

Теперь под текстовым полем политики, оферты и правил постоянно
показывается счётчик символов, а для текстов длиннее одного сообщения —
предупреждение с оценкой, на сколько сообщений бот разобьёт текст.
Оценка зеркалит жадную упаковку абзацев из split_telegram_text бота
(порог 3500 символов на кусок).

Предупреждение не показывается там, где оно неуместно:
- «Рекуррентные платежи» — документ не отображается в боте вовсе;
- режим отображения «Только веб» — бот документ не показывает.

Переводы добавлены в ru/en (с плюральными формами); остальные локали
используют английский fallback через defaultValue.
2026-07-12 19:41:03 +03:00
kewldan
799e986d84 feat(coupons): раздел оптовых купонов + публичная страница активации
UI для купонов из BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot#3042
(API: PR #3044 бота):

- /admin/coupons — список партий с погашениями и offset-пагинацией;
- /admin/coupons/create — мастер партии (тариф, дни, количество,
  учётная оптовая цена, срок), после создания — экран со ссылками
  (копирование всех, скачивание .txt);
- /admin/coupons/:id — карточка со статистикой, экспорт актуальных
  ссылок, отзыв непогашенных за PermissionGate coupons:edit
  с подтверждением и тостом;
- /coupon/:token — публичная страница купона: тариф/срок, кнопка
  активации в боте, для залогиненных — активация прямо в кабинете
  с маппингом структурированных кодов ошибок {code, message};
- пункт меню в разделе тарифов (гейт coupons:read), лениво
  загружаемые роуты, локали ru/en (zh/fa падают на ru-фолбэк).
2026-07-11 23:52:13 +03:00
Fringg
20565b8f59 feat(legal): wire recurrent-payments document + restore footer link
Backend now serves a dedicated recurring-payments legal document, so:
- Add adminLegalPagesApi.get/updateRecurrentPayments and infoApi.getRecurrentPayments
  (+ InfoVisibility.recurrent).
- AdminLegalPages: new 'Рекуррентные платежи' editor tab (DocumentEditor now driven
  by a DOCUMENT_API dispatch map instead of privacy/offer ternaries).
- PublicLegal: add 'recurrent' doc; add public /recurrent-payments route.
- Re-add the recurrent link to the login LegalFooter (was removed pending backend).
- i18n: admin.legalPages.tabs.recurrent for ru/en/fa/zh (footer.recurrent already existed).

type-check, eslint, prettier and build all pass.
2026-07-09 18:47:24 +03:00
Fringg
37a53e40ea fix(auth): make login footer legal links open real pages, not the login tab
The login-page footer linked to /offer, /privacy and /recurrent-payments with
target=_blank, but none of those routes existed, so opening them hit the SPA
catch-all, which redirects unauthenticated users to /login — every footer link
just opened another login tab.

- Add a public PublicLegal page that fetches the offer / privacy document from the
  existing public /cabinet/info endpoints and renders sanitized HTML (DOMPurify via
  the shared formatContent helper).
- Register public /offer and /privacy routes.
- Extract sanitizeHtml/formatContent into src/utils/legalContent.ts and reuse from
  Info.tsx (was duplicated).
- Drop the /recurrent-payments footer link: there is no recurring-payments legal
  document or endpoint in the backend, so it had no real destination. Re-add once
  such a document exists.

type-check, eslint, prettier and build all pass.
2026-07-09 18:14:49 +03:00
Fringg
984d1b0776 feat(cabinet): legal footer on login + admin toggle
Adds a legal footer (Public Offer / Privacy / Recurring Payments) under the
login card, gated by CABINET_FOOTER_ENABLED (backend endpoint added in bot
PR #3011), plus an admin toggle in BrandingTab and ru/en/fa/zh i18n.

Footer-only subset of PR #464 — excludes the unrelated storePartnerClickId
helper and the version/CHANGELOG churn.

Co-authored-by: smediainfo <dev@matrixvpn.top>
2026-06-30 07:30:59 +03:00
Boris Kovalskii
ecc2d45562 fix(admin): gate overpay certificate block by settings permission
(cherry picked from commit 28068bbe45221145df3bd580a46e7bd7fce416e9)
2026-06-16 06:31:46 +03:00
Boris Kovalskii
f52360449b feat(admin): overpay certificate upload block
(cherry picked from commit b51b9d7d45db7b4597838d25a475291e453c7a7a)
2026-06-16 06:31:46 +03:00
c0mrade
9456c7f54e fix(ui): finish design-audit follow-ups
- useFeatureFlags: кэш флагов в localStorage — нижняя навигация больше
  не прыгает на холодном старте (таб «Рефералы»/«Колесо» появлялся после
  загрузки terms/config)
- applyThemeColors: частичный/битый ответ /branding/colors добивается
  дефолтами вместо падения всего приложения в ErrorBoundary
- карточка «Связаться с поддержкой»: кнопка больше не дублирует заголовок
  («Написать», без переноса)
- «Мои подписки»: заголовок и «Купить ещё» в одну строку на 390px
- StatCard: подписи плиток переносятся на две строки вместо обрезки
  многоточием («Бонус новому польз…»)
2026-06-11 16:00:19 +03:00
c0mrade
7f68e2cd5e fix(theme): readable text on any operator palette + admin consistency pass
Контраст-аудит (WCAG-обход всех текстовых узлов, 4 палитры × юзер+админ
страницы) показал системные провалы читаемости: вторичный текст 1.5-2.5
на светлой/кастомных темах, статусные цвета 1.3-1.6, белый текст на
светлых акцентах 1.9.

Корневые фиксы:
- useThemeColors: контраст-кламп производных серых токенов — dark-400/
  champagne-600 (вторичный текст) держат >=5.0 к поверхности, dark-500/
  champagne-500 (подсказки) >=3.8; палитры с достаточным контрастом
  рендерятся байт-в-байт как раньше
- --color-on-accent/-success/-warning/-error: чёрный или белый текст
  по фактической яркости цвета оператора; text-white на accent-заливках
  заменён на text-on-accent (135 мест + variant примитива Button +
  .btn-primary)
- .light: статусные текстовые оттенки *-300/400 ремапятся на тёмные
  *-700 (жёлтая «Админка» 1.26 -> 3.8+, зелёные суммы 1.4 -> 3.4+,
  бейджи состояний)

Замер после: операторская тёмная палитра — 0 нарушений (всё >=4.5),
худшие точки остальных подняты с 1.26-1.6 до 3.2+.

Консистентность:
- заголовки админ-страниц приведены к text-xl font-bold text-dark-100
  (были 18/20/24/30px и 600/700 вперемешку)
- «Создать FAQ»: warning-500/80+белый -> warning-500+text-on-warning
- formatTraffic: единицы из i18n (убран микс «GB»/«ГБ» на одном экране)
- categoryCanvas -> categoryCANVAS в локалях (битый ключ на настройках)
- «Управление сквадами» -> «Управление локациями» (юзерский экран)
- мин-макс сумм на выборе метода: dark-600 -> dark-400
- неактивные пункты нижнего нава: dark-500 -> dark-400
2026-06-11 15:49:17 +03:00
Boris Kovalskii
510e4bd738 feat(backgrounds): add constellation background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
71160c6700 feat(backgrounds): add liquid-gradient background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
ab91c86f81 feat(backgrounds): add matrix-rain background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
3742f2f88d feat(backgrounds): add starfield background with depth projection 2026-06-11 11:14:57 +10:00
Boris Kovalskii
e3dbc4e663 feat(backgrounds): add snowfall background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
1d00ca97e5 feat(backgrounds): add fireflies background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
725bf8df39 fix(backgrounds): configurable wave colors and boxes grid color with explicit multicolor mode 2026-06-11 11:14:56 +10:00
Boris Kovalskii
acbccc8af8 fix(backgrounds): make beams and beams-collision colors configurable 2026-06-11 11:14:56 +10:00
Boris Kovalskii
f1f4281e11 fix(backgrounds): expose remaining hardcoded colors in shooting-stars, meteors, gradient-animation 2026-06-11 11:14:56 +10:00
Boris Kovalskii
515c3d73b8 fix(admin): harden legal pages editor state handling 2026-06-11 11:14:56 +10:00
Boris Kovalskii
0d6a85a4f9 feat: add system pages admin section 2026-06-11 11:14:56 +10:00
Boris Kovalskii
04cbbb5e51 feat: add display mode selector to info page editor 2026-06-11 11:14:56 +10:00
Boris Kovalskii
92198854b2 fix(balance): align quick amounts edge cases with bot behavior 2026-06-11 11:14:56 +10:00
Boris Kovalskii
bb8b823b37 feat(admin): quick amounts editor in payment method settings 2026-06-11 11:14:56 +10:00
c0mrade
8b2501d908 feat(admin): show common placeholders group in email template editor
Companion to the bot-side change: the variables hint now renders two
groups — type-specific vars and common ones ({service_name},
{cabinet_url}, {support_username}, {username}, {email}, {date}) that
work in every template. The block is visible even for types with no
vars of their own (previously it was hidden entirely, looking like
placeholders didn't exist). Common chips insert at cursor the same way.
2026-06-10 23:37:31 +03:00
c0mrade
ac75a806d9 fix(admin): rework email template editor (#667043)
Companion to the bot-side fix: defaults now arrive with {placeholders}
instead of baked-in sample values (example.com links).

- inline preview tab (iframe srcDoc) — works on mobile and in Telegram;
  the separate desktop-only preview page and its route are removed
- preview/test render the CURRENT editor content with sample values
- test email: recipient input + detailed backend error messages
- variable chips insert at cursor instead of copying to clipboard
- 'insert default template' button to migrate broken saved overrides
- fa added to language tabs
2026-06-10 22:53:08 +03:00
c0mrade
16fad9f4fe fix(cabinet): recover "Сервис недоступен" false-positive + top-up fixes
- health probe: tolerant timeout (12s) + retry before flagging the backend down. A
  hardcoded 5s probe racing auth bootstrap falsely showed ServiceUnavailableScreen on
  slow devices / cold mobile connections while the 30s API requests would have
  succeeded ("works on one device, not another on the same Wi-Fi"). The recovery poll
  self-reschedules with the tolerant timeout so slow devices auto-recover.
- TopUpAmount: fetch payment methods with a real query (fixes the infinite spinner on a
  cold cache / browser-back) and use canonical RUB for quick-amount chips so FX rounding
  can't reject a min-amount selection in non-RUB locales.
- settings UI: render secret values as a masked password input (pairs with the backend
  secret masking); leaving the field empty keeps the stored secret.
- deps: npm audit fix (18 -> 5 advisories).

Also bundles in-progress settings env-lock UI work.
2026-06-10 16:15:55 +03:00
c0mrade
75a570b862 docs(payments): correct open_url_direct toggle hint to match real behavior (#654272)
The hint claimed the provider opens 'inside the MiniApp/tab' — that is the broken behavior that caused #654272 (a same-WebView navigation to the provider can't hand off to a bank app / SBP). After the fix the link opens in the EXTERNAL browser inside Telegram regardless of the toggle. Rewrote the hint in all four locales: ON = open immediately (no button panel), OFF = show an 'Open payment' button; both open externally in Telegram so bank-app/SBP payments work; Stars/CryptoBot (t.me/ links) always open natively and ignore the flag.

Note: the external-browser open is unconditional (both toggle states, all methods incl. WATA) — the toggle only controls auto-open vs a button tap.
2026-06-08 15:51:06 +03:00
c0mrade
35428cc27d fix(promocode): correct activation UX, error mapping and validity dates
Map promocode errors by stable backend code (detail.code) instead of brittle English
substring matching, so previously-unmapped codes (active_discount_exists, daily_limit,
no_subscription_for_days, trial_*, etc.) show the right message instead of 'server error';
added the missing keys to all 4 locales.

valid_until now anchors to end-of-day in the admin's LOCAL timezone on both create AND
edit read-back, so codes no longer expire at the start of the day and editing no longer
drifts the date forward for negative-offset admins.

Allow 0-hour discount ('until first purchase'). Route user-facing discount deactivation
to the endpoint that rolls back promocode usage when the discount came from a promocode,
and parse the structured {code,message} error.
2026-06-08 00:23:31 +03:00
c0mrade
5de43c1071 fix(admin-wheel): unify statistics rendering + fill config UI gaps
Statistics tab now uses the shared StatCard + BreakdownList (ranked bars +
share %) and the currency-aware formatter — matching the rest of the admin
stats instead of ad-hoc 'card p-4 text-center' / text-3xl tiles — and gains
proper loading, error and empty states.

Also surface backend-supported config the form couldn't reach (forcing DB edits):
- promo_validity_days (model + schema-bounded 1-365) is now editable in Settings.
- per-prize manual_probability override (0-1) now has an input in the prize form.
2026-06-04 22:33:12 +03:00
c0mrade
3224320095 feat(connected-accounts): email-merge confirmation via emailed one-time code
When linking an email that belongs to another account, the backend now mails a
one-time code to it (merge_verification='email_code') instead of asking for that
account's password. Add a 6-digit code step: on the email_code response show the
code input, verify via authApi.verifyEmailMerge → on success redirect to /merge.
Preview/execute already go through apiClient (JWT), so they satisfy the new
initiator-bound merge endpoints. i18n in ru/en/zh/fa.
2026-06-04 19:06:54 +03:00
c0mrade
7d31dc3ffa fix(connected-accounts): guide the user to enter the existing account's password when merging by email
The backend account-takeover fix now requires the existing account's password
before merging an email that already belongs to another account. Show a clear
message (profile.emailMergePasswordRequired) on that response instead of the
generic "already registered" dead-end, so the legitimate owner knows to enter
that account's password (in both password fields) to complete the merge. i18n
in ru/en/zh/fa.
2026-06-04 14:06:45 +03:00
c0mrade
7413837faf feat(cabinet): bulletproof Close button + kill the /login flash on the service-unavailable screen
Two Telegram-Mini-App follow-ups to the backend-unavailable screen:

- Reliable "Close" button (Telegram only) that actually EXITS the Mini App
  instead of routing back. closeTelegramApp() tries the legacy
  window.Telegram.WebApp.close() global (telegram-web-app.js), then the SDK
  closeMiniApp(), then the raw postEvent('web_app_close') — all emit the same
  close event, so it can't silently fail. Guarded so the first path never
  silently no-ops.
- Eliminate the flash of the /login page before the outage screen. On the
  bootstrap path (the app has never reached the backend) reportPossibleBackendDown()
  now flips the screen IMMEDIATELY and synchronously instead of waiting on the
  confirm probe, so the overlay is up before isLoading flips and /login can never
  paint uncovered. Add an eager checkBackendOnStartup() liveness ping at launch
  (parallel with auth) so even the no-stored-token / fresh-Telegram path that
  makes no early request shows the screen at once. The confirm-probe still guards
  already-loaded sessions from a one-off blip.

i18n close key added in ru/en/zh/fa.
2026-06-04 13:06:05 +03:00
c0mrade
ac8a0fc41a feat(cabinet): recoverable "service unavailable" screen when the backend is unreachable
When the backend was down the cabinet got stuck on a blank loader: the bootstrap
token refresh used bare axios with no timeout (hung forever), and every
interceptor guard required an HTTP response — so a transport-level failure had
zero handling and no error UI ever appeared.

Add a full-screen ServiceUnavailableScreen (extends the existing blocking-store
pattern) shown whenever the backend is unreachable, that auto-recovers when it
returns:
- New 'backend_unavailable' blocking type + BlockingOverlay branch; mirrors
  MaintenanceScreen, CloudWarningIcon, i18n in ru/en/zh/fa.
- api/health.ts: pingBackend() probes the root /health/unified (bypasses the
  /api baseURL + interceptor); URL derived from the origin for remote/sub-path
  deploys; 502/503/504 count as down. reportPossibleBackendDown() confirms an
  outage with a liveness probe before flipping, so a one-off blip never blanks a
  loaded app.
- Detected from the response interceptor (no-response) and the bootstrap refresh
  path; doRefresh gets a timeout so it can no longer hang.
- A transport failure during refresh now PRESERVES the session (distinguished
  from a rejected token via lastFailureWasTransport) instead of logging the user
  out, so recovery actually resumes.
- Recovery lifts the overlay + refetches for an already-loaded session, and only
  hard-reloads when the initial bootstrap never reached the backend — no lost
  form state. Manual retry + 5s auto-poll. Dev proxy for /health.
2026-06-04 12:44:27 +03:00
c0mrade
f7bd36a95e fix(traffic): correct misleading add-traffic note — 30-day validity
The buy-traffic warning said purchased traffic "does not carry over to the next
period", which contradicts how add-ons actually work: a package lives 30 days
and stays in the limit across the monthly reset within those 30 days. Reword to
"valid for 30 days from purchase" (ru/en/zh/fa) so users aren't misled.
2026-06-04 11:33:36 +03:00
c0mrade
78c633ff54 feat(landing-stats): rebuild charts on shared components + add funnel & breakdowns
The landing stats page rendered raw recharts by hand plus a bespoke progress-bar
list, looking nothing like the Sales/Remnawave stats. Rebuild it on the shared
chart components and add the visualizations the data warrants:

- Daily: MultiSeriesAreaChart (created/paid) + SimpleAreaChart (revenue),
  replacing the hand-rolled area chart and the custom progress-bar list.
- Tariff & traffic sources: BreakdownList; payment methods & gift composition:
  DonutChart — all the same components Sales stats uses.
- New conversion Funnel (created → paid) and a Gift activation card
  (claimed/sent + rate), backed by the new backend breakdowns.
- Containers use the bento-card surface; i18n added in ru/en/zh/fa.
2026-06-03 15:11:47 +03:00
c0mrade
b9521f1aa7 fix(gift): cabinet GiftResult shows claim link for directed gifts + review fixes
Pairs with the backend cabinet-unification. Directed cabinet gifts now stay
PAID/claimable, so the buyer's GiftResult must show the share link for them too.

- GiftResult.tsx: render the share state (and stop polling) for ANY claimable
  paid gift via is_claimable, not just is_code_only.
- api/gift.ts: GiftPurchaseStatus gains is_claimable.
- GiftClaim.tsx: explicit failed/expired state (was falling into the 'almost
  ready' spinner forever).
- PurchaseSuccess.tsx: a paid gift is always claimable — stop polling and show
  the share block on `paid && is_gift` (don't gate on is_claimable, which could
  strand the buyer on a spinner).
- i18n: landing.giftClaim.failedTitle/failedDesc in ru/en/zh/fa.
2026-06-03 14:34:19 +03:00
c0mrade
db31d395cd feat(gift): transferable gift claim — buyer share link + recipient claim page
Pairs with the bot's unified claimable-gift model. A landing gift now lands on
the buyer's success page as PAID/claimable (deferred) instead of being delivered
to a phantom recipient, so the buyer gets a transferable link to forward and the
recipient claims it themselves — guaranteeing delivery regardless of channel or
whether they pre-exist.

- api/landings.ts: PurchaseStatus gains is_claimable/claim_url/bot_claim_link;
  add GiftClaimResult + getGiftClaim()/claimGift().
- PurchaseSuccess.tsx: GiftLinkShareState — buyer sees the claim link + Telegram
  link + copy-message when a gift is PAID/claimable; stop polling for that state
  (it stays PAID until claimed) instead of spinning forever.
- GiftClaim.tsx (new) + /buy/gift/:token route: public recipient claim page with
  a Telegram arm (GIFT_ deep link) and a web/email arm (binds + one-click login),
  polling while the payment settles, idempotent "already claimed" + 404 states.
- i18n: landing.giftClaim.* / landing.giftLink.* in ru/en/zh/fa.
2026-06-03 13:46:00 +03:00
c0mrade
39bfef58d0 fix(subscriptions): «+ Купить ещё» только при платной подписке, явный CTA покупки
В мульти-тарифе кнопка «+ Купить ещё подписку» висела всегда, когда есть
хоть какая-то подписка — даже если это только триал или всё истёкшее.
Теперь она показывается только при наличии НАСТОЯЩЕЙ платной активной
подписки (hasActivePaid: не триал + status active/limited).

А «Посмотреть тарифы и купить подписку» из тусклой (пунктир + opacity-60,
спрятана в триал-блоке) стала ЯВНОЙ primary-кнопкой (solid accent) и
показывается, когда платной активной подписки ещё нет (триал/истёкшие/
новый юзер) — мы продаём подписки, путь к покупке должен быть очевиден.
Затронуты Dashboard и Subscriptions. Добавлен ключ subscriptions.browsePlans
(раньше рендерился только inline-фолбэк) в 4 локали.
2026-06-03 11:07:45 +03:00
c0mrade
3adda6fa03 feat(sales-stats): конкретика в сводке
- «Активные подписки» теперь с % (нетто-изменение за период: новые
  платные − истёкшие; обычная дельта снимка была бы всегда 0).
- Новая карточка «Новые платные подписки» (с дельтой период-к-периоду).
- «Доход» больше не включает ручные пополнения (бэк отвязал).
- Убрал «Средняя длит.» из триалов — была вечный 0 и непонятна.
- Сводка 9 карточек: сетка 2/3 с orphan-fill на мобайле, без пустот.
2026-06-02 19:38:38 +03:00
c0mrade
436d9f860c feat(sales-stats): вкладка «Оплаты» — success-rate и неудачные покупки
Новая вкладка в статистике продаж:
- success-rate платежей (оплачено/создано) — общий + по каждому шлюзу с
  цветными барами (зелёный ≥90%, жёлтый ≥70%, красный ниже);
- кол-во попыток оплат;
- «Неудачные покупки» — внутренние откаты при ошибке покупки (сигнал
  надёжности, не возвраты).
Данные из нового эндпоинта /payment-health. Шлюзы без активности не
показываются. Карточки 1/3 (без сирот), адаптив. 4 локали.
2026-06-02 18:46:42 +03:00
c0mrade
ec23618a56 revert(sales-stats): убрать карточки возвратов из сводки
Возвраты для оператора всегда 0 (реальные возвраты только Apple/Tribute,
которых нет) — постоянно пустые карточки только путают. Сводка вернулась
к 8 карточкам (grid 2/4). Убрал также неиспользуемый invertDelta из
StatCard и ключи refunds из локалей.
2026-06-02 18:01:33 +03:00
c0mrade
bd12113294 feat(sales-stats): карточки возвратов в сводке
Две карточки — количество и сумма возвратов за период (красный тон).
Сетку сводки перевёл на 2/5 (10 карточек делятся ровно, без пустых
ячеек). StatCard получил invertDelta: для «меньше — лучше» метрик рост
красится в красный, падение в зелёный (стрелка по факту). 4 локали.
2026-06-02 16:53:30 +03:00
c0mrade
40c684cc88 fix(sales-stats): «Выручка» → «Оборот по подпискам» во вкладке Продажи
Поле суммирует все подписочные платежи (новые + продления + докупки),
поэтому пересекалось по смыслу с вкладками Продления/Допы. Честное имя
«Оборот по подпискам» убирает путаницу: видно, что это итог по всем
подписочным платежам, а не только новые продажи. Все 4 локали.
2026-06-02 16:45:40 +03:00
c0mrade
edd2d25024 feat(ui): тёмный DateField на react-day-picker вместо нативного input
Нативный <input type="date"> выглядел чужеродно (браузерный светлый календарь).
Новый общий компонент DateField: react-day-picker внутри Radix Popover, тёмная
тема через CSS-переменные .rdp-dark, навигация — наши иконки из барреля,
локализация месяцев/дней по языку кабинета (date-fns). Принимает/отдаёт
'YYYY-MM-DD' — drop-in замена нативного инпута. Подключён в селекторе периода
статистики (свой период). Остальные места — следующими шагами.
2026-06-02 16:10:48 +03:00