feat: add admin payment detail view
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import {
|
||||
CalendarDays,
|
||||
Copy,
|
||||
CreditCard,
|
||||
Database,
|
||||
Tag,
|
||||
User,
|
||||
WalletCards,
|
||||
} from "$components/ui/icons.js";
|
||||
import { AdminBadge, AdminButton } from "$components/patterns/admin/index.js";
|
||||
import Dialog from "$components/ui/dialog.svelte";
|
||||
|
||||
export let at = (key, _params = {}, fallback = "") => fallback || key;
|
||||
export let fmtDate = (value) => value;
|
||||
export let fmtMoney = (amount, currency) => `${amount} ${currency || ""}`.trim();
|
||||
export let paymentStatusVariant = () => "muted";
|
||||
export let onOpenUserCard = () => {};
|
||||
|
||||
const paymentsStore = getContext("paymentsStore");
|
||||
|
||||
$: ({ openedPaymentId, openedPayment, paymentDetailLoading } = $paymentsStore);
|
||||
$: payment = openedPayment || (openedPaymentId ? { payment_id: openedPaymentId } : null);
|
||||
$: title = payment
|
||||
? at("payment_detail_title", { id: payment.payment_id }, `Платёж #${payment.payment_id}`)
|
||||
: "";
|
||||
$: description = payment
|
||||
? [
|
||||
payment.provider,
|
||||
payment.created_at ? fmtDate(payment.created_at) : "",
|
||||
payment.user_label || payment.user_id,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")
|
||||
: "";
|
||||
|
||||
function present(value) {
|
||||
return value !== null && value !== undefined && value !== "";
|
||||
}
|
||||
|
||||
function display(value) {
|
||||
return present(value) ? String(value) : "—";
|
||||
}
|
||||
|
||||
function money(value, currency) {
|
||||
return present(value) ? fmtMoney(value, currency) : "—";
|
||||
}
|
||||
|
||||
function formatGb(value) {
|
||||
if (!present(value)) return "—";
|
||||
const n = Number(value);
|
||||
if (Number.isNaN(n)) return display(value);
|
||||
const rounded = Math.abs(n - Math.round(n)) < 1e-9 ? Math.round(n) : Math.round(n * 100) / 100;
|
||||
return `${rounded} GB`;
|
||||
}
|
||||
|
||||
function formatTrafficSplit(p) {
|
||||
const parts = [];
|
||||
if (present(p?.traffic_regular_gb)) {
|
||||
parts.push(
|
||||
at(
|
||||
"payment_detail_regular_traffic",
|
||||
{ gb: formatGb(p.traffic_regular_gb) },
|
||||
`Основной: ${formatGb(p.traffic_regular_gb)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
if (present(p?.traffic_premium_gb)) {
|
||||
parts.push(
|
||||
at(
|
||||
"payment_detail_premium_traffic",
|
||||
{ gb: formatGb(p.traffic_premium_gb) },
|
||||
`Премиум: ${formatGb(p.traffic_premium_gb)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
return parts.join(" · ") || "—";
|
||||
}
|
||||
|
||||
function paymentDescription(p) {
|
||||
const raw = p?.description && String(p.description).trim();
|
||||
if (raw) return raw;
|
||||
return formatTrafficSplit(p);
|
||||
}
|
||||
|
||||
function copy(value) {
|
||||
paymentsStore.copyToClipboard(value, at("payment_detail_copied", {}, "Скопировано"));
|
||||
}
|
||||
|
||||
function openUser() {
|
||||
if (!payment?.user_id) return;
|
||||
paymentsStore.closePayment({ skipPush: true });
|
||||
onOpenUserCard(payment.user_id);
|
||||
}
|
||||
|
||||
$: paymentRows = [
|
||||
{
|
||||
label: "ID",
|
||||
value: payment?.payment_id ? `#${payment.payment_id}` : "",
|
||||
copy: payment?.payment_id,
|
||||
},
|
||||
{
|
||||
label: at("amount", {}, "Сумма"),
|
||||
value: money(payment?.amount, payment?.currency),
|
||||
},
|
||||
{ label: at("status", {}, "Статус"), value: payment?.status },
|
||||
{
|
||||
label: at("date", {}, "Дата"),
|
||||
value: payment?.created_at ? fmtDate(payment.created_at) : "",
|
||||
},
|
||||
{
|
||||
label: at("payment_detail_updated_at", {}, "Обновлён"),
|
||||
value: payment?.updated_at ? fmtDate(payment.updated_at) : "",
|
||||
},
|
||||
{ label: at("description", {}, "Описание"), value: paymentDescription(payment) },
|
||||
];
|
||||
|
||||
$: providerRows = [
|
||||
{ label: at("provider", {}, "Провайдер"), value: payment?.provider },
|
||||
{
|
||||
label: at("payment_detail_provider_payment_id", {}, "ID у провайдера"),
|
||||
value: payment?.provider_payment_id,
|
||||
copy: payment?.provider_payment_id,
|
||||
},
|
||||
{
|
||||
label: "YooKassa ID",
|
||||
value: payment?.yookassa_payment_id,
|
||||
copy: payment?.yookassa_payment_id,
|
||||
},
|
||||
{
|
||||
label: at("payment_detail_idempotence_key", {}, "Ключ идемпотентности"),
|
||||
value: payment?.idempotence_key,
|
||||
copy: payment?.idempotence_key,
|
||||
},
|
||||
];
|
||||
|
||||
$: purchaseRows = [
|
||||
{ label: at("payment_detail_sale_mode", {}, "Тип продажи"), value: payment?.sale_mode },
|
||||
{ label: at("payment_detail_tariff_key", {}, "Тариф"), value: payment?.tariff_key },
|
||||
{
|
||||
label: at("payment_detail_duration_months", {}, "Период"),
|
||||
value: present(payment?.subscription_duration_months)
|
||||
? at(
|
||||
"payment_detail_months_count",
|
||||
{ count: payment.subscription_duration_months },
|
||||
`${payment.subscription_duration_months} мес.`
|
||||
)
|
||||
: "",
|
||||
},
|
||||
{
|
||||
label: at("payment_detail_traffic", {}, "Трафик"),
|
||||
value: formatTrafficSplit(payment),
|
||||
},
|
||||
{
|
||||
label: at("payment_detail_purchased_gb", {}, "Куплено GB"),
|
||||
value: present(payment?.purchased_gb) ? formatGb(payment.purchased_gb) : "",
|
||||
},
|
||||
{
|
||||
label: at("payment_detail_hwid_devices", {}, "HWID-устройства"),
|
||||
value: payment?.purchased_hwid_devices,
|
||||
},
|
||||
{ label: at("payment_detail_promo_code", {}, "Промокод"), value: payment?.promo_code },
|
||||
];
|
||||
|
||||
$: userRows = [
|
||||
{ label: at("user", {}, "Пользователь"), value: payment?.user_label },
|
||||
{ label: "User ID", value: payment?.user_id, copy: payment?.user_id },
|
||||
{ label: "Telegram ID", value: payment?.telegram_id, copy: payment?.telegram_id },
|
||||
];
|
||||
</script>
|
||||
|
||||
<Dialog
|
||||
open={Boolean(openedPaymentId)}
|
||||
{title}
|
||||
{description}
|
||||
closeLabel={at("close", {}, "Закрыть")}
|
||||
onclose={paymentsStore.closePayment}
|
||||
class="admin-dialog admin-payment-dialog"
|
||||
>
|
||||
{#if payment}
|
||||
<div class="admin-payment-dialog-body">
|
||||
<aside class="admin-payment-aside">
|
||||
<div class="admin-payment-summary">
|
||||
<span class="admin-payment-icon" aria-hidden="true">
|
||||
<WalletCards size={24} />
|
||||
</span>
|
||||
<div class="admin-payment-summary-meta">
|
||||
<strong>{money(payment.amount, payment.currency)}</strong>
|
||||
<small>{paymentDescription(payment)}</small>
|
||||
<div class="admin-payment-summary-tags">
|
||||
<AdminBadge variant={paymentStatusVariant(payment.status)}
|
||||
>{display(payment.status)}</AdminBadge
|
||||
>
|
||||
{#if payment.provider}
|
||||
<AdminBadge variant="muted">{payment.provider}</AdminBadge>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-payment-stats">
|
||||
<div class="admin-payment-stat">
|
||||
<CreditCard size={15} />
|
||||
<span>{at("payment_detail_provider", {}, "Провайдер")}</span>
|
||||
<strong>{display(payment.provider)}</strong>
|
||||
</div>
|
||||
<div class="admin-payment-stat">
|
||||
<CalendarDays size={15} />
|
||||
<span>{at("date", {}, "Дата")}</span>
|
||||
<strong>{payment.created_at ? fmtDate(payment.created_at) : "—"}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-subsection-title">
|
||||
{at("payment_detail_user_section", {}, "Пользователь")}
|
||||
</div>
|
||||
<ul class="admin-meta-list admin-payment-meta-list">
|
||||
{#each userRows as row}
|
||||
<li>
|
||||
<span>{row.label}</span>
|
||||
<strong class:admin-meta-truncate={row.copy}>{display(row.value)}</strong>
|
||||
{#if row.copy}
|
||||
<AdminButton
|
||||
size="icon"
|
||||
variant="icon"
|
||||
title={at("user_copy_tooltip", {}, "Скопировать")}
|
||||
onclick={() => copy(row.copy)}
|
||||
>
|
||||
<Copy size={14} />
|
||||
</AdminButton>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<AdminButton variant="ghost" onclick={openUser} disabled={!payment.user_id}>
|
||||
<User size={14} />
|
||||
{at("payments_open_user", {}, "Открыть карточку пользователя")}
|
||||
</AdminButton>
|
||||
</aside>
|
||||
|
||||
<main class="admin-payment-main">
|
||||
{#if paymentDetailLoading && !openedPayment}
|
||||
<p class="admin-muted">{at("loading", {}, "Загрузка...")}</p>
|
||||
{:else}
|
||||
<section class="admin-payment-panel">
|
||||
<div class="admin-payment-panel-head">
|
||||
<CreditCard size={16} />
|
||||
<h3>{at("payment_detail_payment_section", {}, "Платёж")}</h3>
|
||||
</div>
|
||||
<ul class="admin-meta-list admin-payment-meta-list">
|
||||
{#each paymentRows as row}
|
||||
<li>
|
||||
<span>{row.label}</span>
|
||||
<strong class:admin-meta-truncate={row.copy}>{display(row.value)}</strong>
|
||||
{#if row.copy}
|
||||
<AdminButton
|
||||
size="icon"
|
||||
variant="icon"
|
||||
title={at("user_copy_tooltip", {}, "Скопировать")}
|
||||
onclick={() => copy(row.copy)}
|
||||
>
|
||||
<Copy size={14} />
|
||||
</AdminButton>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="admin-payment-panel">
|
||||
<div class="admin-payment-panel-head">
|
||||
<Database size={16} />
|
||||
<h3>{at("payment_detail_provider_section", {}, "Провайдер")}</h3>
|
||||
</div>
|
||||
<ul class="admin-meta-list admin-payment-meta-list">
|
||||
{#each providerRows as row}
|
||||
<li>
|
||||
<span>{row.label}</span>
|
||||
<strong class:admin-meta-truncate={row.copy}>{display(row.value)}</strong>
|
||||
{#if row.copy}
|
||||
<AdminButton
|
||||
size="icon"
|
||||
variant="icon"
|
||||
title={at("user_copy_tooltip", {}, "Скопировать")}
|
||||
onclick={() => copy(row.copy)}
|
||||
>
|
||||
<Copy size={14} />
|
||||
</AdminButton>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="admin-payment-panel">
|
||||
<div class="admin-payment-panel-head">
|
||||
<Tag size={16} />
|
||||
<h3>{at("payment_detail_purchase_section", {}, "Покупка")}</h3>
|
||||
</div>
|
||||
<ul class="admin-meta-list admin-payment-meta-list">
|
||||
{#each purchaseRows as row}
|
||||
<li>
|
||||
<span>{row.label}</span>
|
||||
<strong>{display(row.value)}</strong>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
</Dialog>
|
||||
Reference in New Issue
Block a user