316 lines
11 KiB
Svelte
316 lines
11 KiB
Svelte
<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>
|