@blocklet/payment-react
Version:
Reusable react components for payment kit v2
420 lines (419 loc) • 15.2 kB
JavaScript
import { jsx, jsxs } from "react/jsx-runtime";
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
import { Box, Typography, Grid, Stack, Link, Button } from "@mui/material";
import { useRequest } from "ahooks";
import { useNavigate } from "react-router-dom";
import React, { useEffect, useRef, useState } from "react";
import { styled } from "@mui/system";
import { joinURL } from "ufo";
import DateRangePicker from "../../components/date-range-picker.js";
import { formatToDate, getPrefix, formatCreditAmount, formatBNStr } from "../../libs/util.js";
import { usePaymentContext } from "../../contexts/payment.js";
import api from "../../libs/api.js";
import Table from "../../components/table.js";
import { createLink, handleNavigation } from "../../libs/navigation.js";
const fetchData = (params = {}) => {
const search = new URLSearchParams();
Object.keys(params).forEach((key) => {
if (params[key]) {
search.set(key, String(params[key]));
}
});
return api.get(`/api/credit-transactions?${search.toString()}`).then((res) => res.data);
};
const getGrantDetailLink = (grantId, inDashboard) => {
let path = `/customer/credit-grant/${grantId}`;
if (inDashboard) {
path = `/admin/customers/${grantId}`;
}
return {
link: createLink(path),
connect: false
};
};
const getInvoiceDetailLink = (invoiceId, inDashboard) => {
let path = `/customer/invoice/${invoiceId}`;
if (inDashboard) {
path = `/admin/billing/${invoiceId}`;
}
return {
link: createLink(path),
connect: false
};
};
const getSubscriptionDetailLink = (subscriptionId, inDashboard) => {
let path = `/customer/subscription/${subscriptionId}`;
if (inDashboard) {
path = `/admin/billing/${subscriptionId}`;
}
return {
link: createLink(path),
connect: false
};
};
const getCreditTransactionDetailLink = (transactionId) => {
const path = `/customer/credit-transaction/${transactionId}`;
return {
link: createLink(path),
connect: false
};
};
const getMeterEventDetailLink = (meterEventId) => {
const path = `/admin/billing/${meterEventId}`;
return {
link: createLink(path),
connect: false
};
};
const getSubscriptionId = (item) => item.metadata?.subscription_id || item.subscription_id || item.invoice?.subscription_id;
const getInvoiceId = (item) => item.metadata?.invoice_id || item.invoice?.id;
const getMeterEventId = (item) => item.source || item.metadata?.meter_event_id;
const getCreditActivityFlags = (item) => {
const isGrant = item.activity_type === "grant";
const isScheduled = isGrant && item.metadata?.delivery_mode === "schedule";
const isDepleted = isGrant && item.status === "depleted";
const isExpired = isGrant && (item.status === "expired" || item.status === "voided");
const isInactive = isDepleted || isExpired;
return { isGrant, isScheduled, isDepleted, isExpired, isInactive };
};
const getTransactionDetailLink = (item, inDashboard) => {
if (item.activity_type === "grant") {
const invoiceId = getInvoiceId(item);
if (invoiceId) {
return getInvoiceDetailLink(invoiceId, inDashboard);
}
return getGrantDetailLink(item.id, inDashboard);
}
if (!inDashboard) {
return getCreditTransactionDetailLink(item.id);
}
const meterEventId = getMeterEventId(item);
if (!meterEventId) {
return null;
}
return getMeterEventDetailLink(meterEventId);
};
const getTransactionDescription = (item, t) => {
const { isGrant, isScheduled, isInactive } = getCreditActivityFlags(item);
const isPaid = isGrant && item.category === "paid" && (!isScheduled || item.metadata?.schedule_seq === 1);
if (!isGrant) {
const secondLine = item.metadata?.is_repayment ? t("common.creditActivity.repayment") : item.description || "";
return {
isGrant,
isInactive,
activityType: t("common.creditActivity.consumption"),
secondLine
};
}
if (isPaid) {
let secondLine = item.description || "";
if (item.invoice?.total && item.invoice?.paymentCurrency) {
const invoiceCurrency = item.invoice.paymentCurrency;
const paidAmount = formatCreditAmount(
formatBNStr(item.invoice.total, invoiceCurrency.decimal || 0),
invoiceCurrency.symbol || ""
);
secondLine = t("common.creditActivity.paidAmount", { amount: paidAmount });
}
return {
isGrant,
isInactive,
activityType: t("common.creditActivity.paidGrant"),
secondLine
};
}
if (isScheduled) {
return {
isGrant,
isInactive,
activityType: t("common.creditActivity.resetGrant"),
secondLine: item.description || ""
};
}
return {
isGrant,
isInactive,
activityType: t("common.creditActivity.promotionalGrant"),
secondLine: item.description || ""
};
};
const TransactionsTable = React.memo((props) => {
const {
pageSize,
customer_id,
subscription_id,
credit_grant_id,
onTableDataChange,
showAdminColumns = false,
showTimeFilter = false,
includeGrants = false,
source,
mode = "portal"
} = props;
const listKey = "credit-transactions-table";
const { t, locale } = useLocaleContext();
const { session } = usePaymentContext();
const isAdmin = ["owner", "admin"].includes(session?.user?.role || "");
const isDashboard = isAdmin && mode === "dashboard";
const navigate = useNavigate();
const effectiveCustomerId = customer_id || session?.user?.did;
const [search, setSearch] = useState({
pageSize: pageSize || 10,
page: 1
});
const [filters, setFilters] = useState({
start: void 0,
end: void 0
});
const handleDateRangeChange = (newValue) => {
setFilters(newValue);
setSearch((prev) => ({
...prev,
page: 1,
start: newValue.start || void 0,
end: newValue.end || void 0
}));
};
const { loading, data = { list: [], count: 0 } } = useRequest(
() => fetchData({
...search,
customer_id: effectiveCustomerId,
subscription_id,
credit_grant_id,
source,
include_grants: includeGrants
}),
{
refreshDeps: [search, effectiveCustomerId, subscription_id, credit_grant_id, source, includeGrants]
}
);
useEffect(() => {
if (showTimeFilter && !search.start && !search.end) {
setSearch((prev) => ({
...prev,
page: 1,
start: filters.start || void 0,
end: filters.end || void 0
}));
}
}, [showTimeFilter, search.start, search.end, filters.start, filters.end]);
const prevData = useRef(data);
useEffect(() => {
if (onTableDataChange) {
onTableDataChange(data, prevData.current);
prevData.current = data;
}
}, [data]);
const handleTransactionClick = (e, item) => {
const detail = getTransactionDetailLink(item, isDashboard);
if (!detail) {
return;
}
handleNavigation(e, detail.link, navigate, { target: detail.link.external ? "_blank" : "_self" });
};
const openSubscription = (e, subscriptionId) => {
e.preventDefault();
const link = getSubscriptionDetailLink(subscriptionId, isDashboard);
handleNavigation(e, link.link, navigate);
};
const openInvoice = (e, invoiceId) => {
e.preventDefault();
const link = getInvoiceDetailLink(invoiceId, isDashboard);
handleNavigation(e, link.link, navigate);
};
const renderActionButton = (label, onClick) => /* @__PURE__ */ jsx(Button, { variant: "text", size: "small", color: "primary", sx: { whiteSpace: "nowrap" }, onClick, children: label });
const columns = [
{
label: t("common.date"),
name: "created_at",
options: {
setCellProps: () => ({ style: { width: "25%" } }),
customBodyRenderLite: (_, index) => {
const item = data?.list[index];
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleTransactionClick(e, item), children: /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { fontSize: "0.875rem" }, children: formatToDate(item.created_at, locale, "YYYY-MM-DD HH:mm") }) });
}
}
},
{
label: t("common.description"),
name: "description",
options: {
setCellProps: () => ({ style: { width: "25%" } }),
customBodyRenderLite: (_, index) => {
const item = data?.list[index];
const { activityType, secondLine, isInactive, isGrant } = getTransactionDescription(item, t);
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleTransactionClick(e, item), sx: { cursor: "pointer" }, children: /* @__PURE__ */ jsxs(Stack, { direction: "column", spacing: 0.25, children: [
/* @__PURE__ */ jsx(
Typography,
{
variant: "body2",
sx: {
color: isInactive ? "text.secondary" : isGrant ? "success.main" : "error.main"
},
children: activityType
}
),
secondLine && /* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { color: "text.secondary" }, children: secondLine })
] }) });
}
}
},
{
label: t("common.amount"),
name: "credit_amount",
align: "right",
options: {
setCellProps: () => ({ style: { width: "20%" } }),
customHeadLabelRender: () => /* @__PURE__ */ jsx(Box, { sx: { pr: 5 }, children: t("common.amount") }),
customBodyRenderLite: (_, index) => {
const item = data?.list[index];
const { isGrant, isDepleted, isExpired, isInactive } = getCreditActivityFlags(item);
const amount = isGrant ? item.amount : item.credit_amount;
const currency = item.paymentCurrency || item.currency;
const unit = !isGrant && item.meter?.unit ? item.meter.unit : currency?.symbol;
const displayAmount = formatCreditAmount(formatBNStr(amount, currency?.decimal || 0), unit);
if (!includeGrants) {
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleTransactionClick(e, item), sx: { pr: 5 }, children: /* @__PURE__ */ jsx(Typography, { children: displayAmount }) });
}
return /* @__PURE__ */ jsx(Box, { onClick: (e) => handleTransactionClick(e, item), sx: { pr: 5 }, children: /* @__PURE__ */ jsxs(Stack, { direction: "column", spacing: 0.25, alignItems: "flex-end", children: [
/* @__PURE__ */ jsxs(
Typography,
{
sx: {
fontWeight: 500,
color: isInactive ? "text.secondary" : isGrant ? "success.main" : "error.main",
whiteSpace: "nowrap"
},
children: [
isGrant ? "+" : "-",
" ",
displayAmount
]
}
),
isDepleted ? /* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { color: "text.secondary" }, children: t("common.consumed") }) : isExpired ? /* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { color: "text.secondary" }, children: t("common.expired") }) : null
] }) });
}
}
},
...showAdminColumns && isAdmin ? [
{
label: t("common.meterEvent"),
name: "meter_event",
options: {
customBodyRenderLite: (_, index) => {
const transaction = data?.list[index];
if (!transaction.meter) {
return /* @__PURE__ */ jsx(Typography, { variant: "body2", children: "-" });
}
return /* @__PURE__ */ jsx(Link, { href: joinURL(getPrefix(), `/admin/billing/${transaction.meter.id}`), children: /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "text.link" }, children: transaction.meter.event_name }) });
}
}
}
] : [],
{
label: t("common.actions"),
name: "actions",
options: {
setCellProps: () => ({ style: { width: "25%" } }),
customBodyRenderLite: (_, index) => {
const item = data?.list[index];
const { isGrant, isScheduled } = getCreditActivityFlags(item);
const isPaid = isGrant && item.category === "paid" && !isScheduled;
const subscriptionId = getSubscriptionId(item);
const invoiceId = isGrant ? getInvoiceId(item) : null;
const shouldShowSubscription = Boolean(subscriptionId) && (!isGrant || isScheduled || isPaid);
return /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", gap: 1, alignItems: "center", flexWrap: "nowrap" }, children: [
shouldShowSubscription && subscriptionId ? renderActionButton(t("common.viewSubscription"), (e) => openSubscription(e, subscriptionId)) : null,
isPaid && invoiceId ? renderActionButton(t("common.viewInvoice"), (e) => openInvoice(e, invoiceId)) : null
] });
}
}
}
].filter(Boolean);
const onTableChange = ({ page, rowsPerPage }) => {
if (search.pageSize !== rowsPerPage) {
setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
} else if (search.page !== page + 1) {
setSearch((x) => ({ ...x, page: page + 1 }));
}
};
return /* @__PURE__ */ jsxs(TableRoot, { children: [
showTimeFilter && /* @__PURE__ */ jsx(Box, { sx: { my: 2 }, children: /* @__PURE__ */ jsx(Box, { sx: { mt: 2 }, children: /* @__PURE__ */ jsx(
Grid,
{
container: true,
spacing: 2,
sx: {
alignItems: "center"
},
children: /* @__PURE__ */ jsx(
Grid,
{
size: {
xs: 12,
sm: 6,
md: 4
},
children: /* @__PURE__ */ jsx(DateRangePicker, { value: filters, onChange: handleDateRangeChange, size: "small", fullWidth: true })
}
)
}
) }) }),
/* @__PURE__ */ jsx(
Table,
{
hasRowLink: true,
durable: `__${listKey}__`,
durableKeys: ["page", "rowsPerPage"],
data: data.list,
columns,
options: {
count: data.count,
page: search.page - 1,
rowsPerPage: search.pageSize
},
loading,
onChange: onTableChange,
toolbar: false,
sx: { mt: 2 },
showMobile: false,
mobileTDFlexDirection: "row",
emptyNodeText: t("admin.creditTransactions.noTransactions")
}
)
] });
});
const TableRoot = styled(Box)`
@media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
.MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
> div {
width: fit-content;
flex: inherit;
font-size: 14px;
}
}
.invoice-summary {
padding-right: 20px;
}
}
`;
export default function CreditTransactionsList(rawProps) {
const props = Object.assign(
{
customer_id: "",
subscription_id: "",
credit_grant_id: "",
source: "",
pageSize: 10,
onTableDataChange: () => {
},
showAdminColumns: false,
showTimeFilter: false,
includeGrants: false,
mode: "portal"
},
rawProps
);
return /* @__PURE__ */ jsx(TransactionsTable, { ...props });
}