From aa6597248369dd231e524b3ebd8e1ee5177aaf23 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Sun, 24 May 2026 09:30:58 +0300 Subject: [PATCH] feat: add admin payment detail view --- .../bot/app/web/admin_api_impl/payments.py | 29 ++ backend/bot/app/web/admin_api_impl/routes.py | 1 + backend/bot/app/web/webapp/routes.py | 1 + frontend/src/App.svelte | 2 + frontend/src/admin/AdminPanel.svelte | 39 ++- .../admin/sections/PaymentDetailModal.svelte | 315 ++++++++++++++++++ .../src/admin/sections/PaymentsSection.svelte | 27 +- .../src/lib/admin/stores/paymentsStore.js | 91 ++++- frontend/src/lib/webapp/mockApi.js | 75 ++++- frontend/src/lib/webapp/routes.js | 12 +- frontend/src/styles/admin.css | 168 ++++++++++ locales/en.json | 22 ++ locales/ru.json | 22 ++ 13 files changed, 786 insertions(+), 18 deletions(-) create mode 100644 frontend/src/admin/sections/PaymentDetailModal.svelte diff --git a/backend/bot/app/web/admin_api_impl/payments.py b/backend/bot/app/web/admin_api_impl/payments.py index d554b5c..33dbd54 100644 --- a/backend/bot/app/web/admin_api_impl/payments.py +++ b/backend/bot/app/web/admin_api_impl/payments.py @@ -32,6 +32,35 @@ async def admin_payments_list_route(request: web.Request) -> web.Response: ) +async def admin_payment_detail_route(request: web.Request) -> web.Response: + _require_admin_user_id(request) + async_session_factory: sessionmaker = request.app["async_session_factory"] + + try: + payment_id = int(request.match_info["payment_id"]) + except (TypeError, ValueError): + return _error(400, "invalid_payment", "Invalid payment id") + + async with async_session_factory() as session: + payment = await payment_dal.get_payment_by_db_id(session, payment_id) + if not payment: + return _error(404, "not_found", "Payment not found") + + payload = _serialize_payment(payment) + payload.update( + { + "yookassa_payment_id": payment.yookassa_payment_id, + "idempotence_key": payment.idempotence_key, + "promo_code": ( + payment.promo_code_used.code if payment.promo_code_used is not None else None + ), + "updated_at": payment.updated_at.isoformat() if payment.updated_at else None, + } + ) + + return _ok({"payment": payload}) + + async def admin_payments_export_route(request: web.Request) -> web.Response: _require_admin_user_id(request) async_session_factory: sessionmaker = request.app["async_session_factory"] diff --git a/backend/bot/app/web/admin_api_impl/routes.py b/backend/bot/app/web/admin_api_impl/routes.py index a6d9d98..fe57f77 100644 --- a/backend/bot/app/web/admin_api_impl/routes.py +++ b/backend/bot/app/web/admin_api_impl/routes.py @@ -36,6 +36,7 @@ def setup_admin_routes(app: web.Application) -> None: router.add_delete("/api/admin/users/{user_id:-?\\d+}", admin_user_delete_route) router.add_get("/api/admin/payments", admin_payments_list_route) + router.add_get("/api/admin/payments/{payment_id:\\d+}", admin_payment_detail_route) router.add_get("/api/admin/payments/export.csv", admin_payments_export_route) router.add_get("/api/admin/promos", admin_promos_list_route) diff --git a/backend/bot/app/web/webapp/routes.py b/backend/bot/app/web/webapp/routes.py index 99db843..c5b9716 100644 --- a/backend/bot/app/web/webapp/routes.py +++ b/backend/bot/app/web/webapp/routes.py @@ -23,6 +23,7 @@ def setup_subscription_webapp_routes(app: web.Application) -> None: index_route, ) app.router.add_get("/admin/users/{user_id:-?[0-9]+}", index_route) + app.router.add_get("/admin/payments/{payment_id:\\d+}", index_route) app.router.add_get("/admin/support/{ticket_id:\\d+}", index_route) app.router.add_get("/auth/telegram/start", telegram_oauth_start_route) app.router.add_get("/auth/telegram/callback", telegram_oauth_callback_route) diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 8eeb809..63b5a3b 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -79,6 +79,7 @@ import { mockApi as runMockApi } from "./lib/webapp/mockApi.js"; import { DEV_MOCK, applyPreviewMock } from "./lib/webapp/previewMock.js"; import { + adminPaymentIdFromPath, adminSectionFromPath, adminUserIdFromPath, normalizeSection, @@ -788,6 +789,7 @@ onClose: closeAdminPanel, onToast: (text) => showToast(text), initialSection: adminSectionFromPath(window.location.pathname), + initialPaymentId: adminPaymentIdFromPath(window.location.pathname), initialUserId: adminUserIdFromPath(window.location.pathname), onSectionChange: handleAdminSectionChange, onSettingsSaved: handleAdminPersistedSaved, diff --git a/frontend/src/admin/AdminPanel.svelte b/frontend/src/admin/AdminPanel.svelte index 995bc53..a0c8e47 100644 --- a/frontend/src/admin/AdminPanel.svelte +++ b/frontend/src/admin/AdminPanel.svelte @@ -30,6 +30,7 @@ import AdsSection from "./sections/AdsSection.svelte"; import BroadcastSection from "./sections/BroadcastSection.svelte"; import LogsSection from "./sections/LogsSection.svelte"; + import PaymentDetailModal from "./sections/PaymentDetailModal.svelte"; import PaymentsSection from "./sections/PaymentsSection.svelte"; import PromosSection from "./sections/PromosSection.svelte"; import SettingsSection from "./sections/SettingsSection.svelte"; @@ -75,6 +76,7 @@ export let onClose = () => {}; export let onToast = () => {}; export let initialSection = "stats"; + export let initialPaymentId = null; export let initialUserId = null; export let onSectionChange = () => {}; export let onSettingsSaved = () => {}; @@ -213,7 +215,7 @@ const adsStore = createAdsStore({ api, onToast: flash, at }); const broadcastStore = createBroadcastStore({ api, onToast: flash, at }); const logsStore = createLogsStore({ api, at }); - const paymentsStore = createPaymentsStore({ api, at }); + const paymentsStore = createPaymentsStore({ api, onToast: flash, at }); const promosStore = createPromosStore({ api, onToast: flash, at }); const settingsStore = createSettingsStore({ api, onToast: flash, at }); const statsStore = createStatsStore({ api, onToast: flash, at }); @@ -235,6 +237,7 @@ setContext("themesStore", themesStore); $: usersStore.setActive(active); + $: paymentsStore.setActive(active); $: supportStore.setActive(active); $: dirtyCount = Object.keys($settingsStore.settingsDirty || {}).length; $: syncBusy = $statsStore.syncBusy; @@ -251,13 +254,14 @@ if (active === next) return; active = next; usersStore.closeUser(); + paymentsStore.closePayment(); supportStore.closeTicketView(); onSectionChange(next); } function readSectionFromPath() { if (typeof window === "undefined") return "stats"; - const match = window.location.pathname.match(/^\/admin\/([a-z0-9_-]+)(?:\/[^/]+)?$/i); + const match = window.location.pathname.match(/^\/admin\/([a-z0-9_-]+)(?:\/.*)?$/i); return normalizeSection(match ? match[1].toLowerCase() : "stats"); } @@ -273,6 +277,12 @@ return match ? Number(match[1]) : null; } + function readPaymentIdFromPath() { + if (typeof window === "undefined") return null; + const match = window.location.pathname.match(/^\/admin\/payments\/(\d+)$/); + return match ? Number(match[1]) : null; + } + function onPopState() { active = readSectionFromPath(); sidebarOpen = false; @@ -284,6 +294,14 @@ } else if ($usersStore.openedUser) { usersStore.closeUser({ skipPush: true }); } + const paymentId = readPaymentIdFromPath(); + if (active === "payments" && paymentId) { + if (!$paymentsStore.openedPaymentId || $paymentsStore.openedPaymentId !== paymentId) { + paymentsStore.openPayment(paymentId, { skipPush: true }); + } + } else if ($paymentsStore.openedPaymentId) { + paymentsStore.closePayment({ skipPush: true }); + } const ticketId = readSupportTicketIdFromPath(); if (active === "support" && ticketId) { if (!$supportStore.openedTicketId || $supportStore.openedTicketId !== ticketId) { @@ -433,6 +451,15 @@ ) { usersStore.openUser(initialUserId, { skipPush: true }); } + + $: if ( + active === "payments" && + initialPaymentId && + (!$paymentsStore.openedPaymentId || $paymentsStore.openedPaymentId !== initialPaymentId) + ) { + paymentsStore.openPayment(initialPaymentId, { skipPush: true }); + } +