2026-06-15 00:37:46 +08:00

356 lines
12 KiB
JavaScript

import AccountBalanceWalletOutlined from "@mui/icons-material/AccountBalanceWalletOutlined";
import PaidOutlined from "@mui/icons-material/PaidOutlined";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import { useCallback, useEffect, useState } from "react";
import { adjustSalaryWallet, listSalaryWalletHistory } from "@/features/host-org/api";
import styles from "@/features/host-org/host-org.module.css";
import {
AdminFormAmountField,
AdminFormDialog,
AdminFormFieldGrid,
AdminFormReadOnlyField,
AdminFormSection,
} from "@/shared/ui/AdminFormDialog.jsx";
import { AdminActionIconButton } from "@/shared/ui/AdminListLayout.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
import { formatMillis } from "@/shared/utils/time.js";
const historyPageSize = 20;
const emptyHistory = { items: [], page: 1, pageSize: historyPageSize, total: 0 };
const emptyAdjustForm = () => ({
amount: "",
reason: "",
type: "increase",
});
const historyColumns = [
{
key: "createdAtMs",
label: "时间",
render: (item) => formatMillis(item.createdAtMs),
width: "minmax(170px, 0.9fr)",
},
{
key: "amountMinor",
label: "金额",
render: (item) => <SalaryHistoryAmount item={item} />,
width: "minmax(120px, 0.65fr)",
},
{
key: "balance",
label: "余额",
render: (item) => formatSalaryWalletAmount(item.availableAfter),
width: "minmax(120px, 0.65fr)",
},
{
key: "bizType",
label: "类型",
render: (item) => salaryWalletBizLabel(item.bizType),
width: "minmax(150px, 0.8fr)",
},
{
key: "operator",
label: "操作人",
render: (item) => salaryWalletOperatorName(item),
width: "minmax(150px, 0.8fr)",
},
{
key: "reason",
label: "原因",
render: (item) => item.reason || "-",
width: "minmax(220px, 1.15fr)",
},
];
export function SalaryWalletCell({ role, userId, userLabel, wallet }) {
const [open, setOpen] = useState(false);
const balance = Number(wallet?.availableAmount || 0);
return (
<>
<button
className={styles.salaryWalletButton}
type="button"
onClick={() => setOpen(true)}
>
<span className={styles.salaryWalletAmount}>{formatSalaryWalletAmount(balance)}</span>
</button>
<SalaryWalletHistoryDrawer
open={open}
role={role}
userId={userId}
userLabel={userLabel}
onClose={() => setOpen(false)}
/>
</>
);
}
export function SalaryWalletAdjustButton({ disabled = false, onAdjusted, role, userId, userLabel }) {
const { showToast } = useToast();
const [open, setOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [form, setForm] = useState(emptyAdjustForm);
const close = () => {
if (submitting) {
return;
}
setOpen(false);
setForm(emptyAdjustForm());
};
const submit = async (event) => {
event.preventDefault();
let amountMinor;
try {
amountMinor = parseSalaryUSDMinor(form.amount);
if (!String(form.reason || "").trim()) {
throw new Error("请填写原因");
}
} catch (err) {
showToast(err.message || "工资操作参数不正确", "error");
return;
}
setSubmitting(true);
try {
const result = await adjustSalaryWallet({
amountMinor,
commandId: makeSalaryWalletCommandId(role, userId),
reason: String(form.reason || "").trim(),
role,
targetUserId: userId,
type: form.type,
});
showToast("工资钱包已更新", "success");
setOpen(false);
setForm(emptyAdjustForm());
await onAdjusted?.(result);
} catch (err) {
showToast(err.message || "工资钱包操作失败", "error");
} finally {
setSubmitting(false);
}
};
return (
<>
<AdminActionIconButton
disabled={disabled || !role || !userId}
label="工资操作"
onClick={() => setOpen(true)}
>
<PaidOutlined fontSize="small" />
</AdminActionIconButton>
<AdminFormDialog
disabled={disabled}
loading={submitting}
open={open}
size="large"
submitDisabled={disabled || submitting || !role || !userId}
submitLabel="确认"
title="工资操作"
onClose={close}
onSubmit={submit}
>
<AdminFormSection title="目标身份">
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
<AdminFormReadOnlyField label="身份" value={salaryWalletRoleLabel(role)} />
<AdminFormReadOnlyField label="用户" value={userLabel || userId || "-"} />
</AdminFormFieldGrid>
</AdminFormSection>
<AdminFormSection title="操作信息">
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
<TextField
disabled={disabled || submitting}
label="类型"
required
select
value={form.type}
onChange={(event) => setForm((current) => ({ ...current, type: event.target.value }))}
>
<MenuItem value="increase">工资增加</MenuItem>
<MenuItem value="decrease">工资减少</MenuItem>
</TextField>
<AdminFormAmountField
disabled={disabled || submitting}
label="金额"
required
unit="USD"
value={form.amount}
onChange={(event) => setForm((current) => ({ ...current, amount: event.target.value }))}
/>
</AdminFormFieldGrid>
<TextField
disabled={disabled || submitting}
label="原因"
multiline
minRows={3}
required
value={form.reason}
onChange={(event) => setForm((current) => ({ ...current, reason: event.target.value }))}
/>
</AdminFormSection>
</AdminFormDialog>
</>
);
}
function SalaryWalletHistoryDrawer({ onClose, open, role, userId, userLabel }) {
const [page, setPage] = useState(1);
const [data, setData] = useState(emptyHistory);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const loadHistory = useCallback(async () => {
if (!open || !role || !userId) {
return;
}
setError("");
setLoading(true);
try {
const nextData = await listSalaryWalletHistory({
page,
page_size: historyPageSize,
role,
user_id: userId,
});
setData(nextData || emptyHistory);
} catch (err) {
setError(err.message || "加载工资钱包历史失败");
} finally {
setLoading(false);
}
}, [open, page, role, userId]);
useEffect(() => {
if (!open) {
return;
}
void loadHistory();
}, [loadHistory, open]);
useEffect(() => {
if (open) {
setPage(1);
}
}, [open, role, userId]);
const total = data.total || 0;
return (
<SideDrawer
className={styles.salaryWalletDrawer}
contentClassName={styles.salaryWalletDrawerBody}
open={open}
title={`工资钱包历史 - ${userLabel || userId || ""}`}
width="wide"
onClose={onClose}
>
<div className={styles.salaryWalletSummary}>
<AccountBalanceWalletOutlined fontSize="small" />
<span>{salaryWalletRoleLabel(role)}</span>
</div>
<DataState error={error} loading={loading} onRetry={loadHistory}>
<DataTable
columns={historyColumns}
emptyLabel="暂无工资钱包历史"
items={data.items || []}
minWidth="930px"
pagination={
total > 0
? {
page,
pageSize: data.pageSize || historyPageSize,
total,
onPageChange: setPage,
}
: undefined
}
rowKey={(item) => item.entryId || item.transactionId}
/>
</DataState>
</SideDrawer>
);
}
function SalaryHistoryAmount({ item }) {
const delta = Number(item.availableDelta ?? item.amountMinor ?? 0);
const expense = delta < 0;
return (
<span className={expense ? styles.salaryWalletExpense : styles.salaryWalletIncome}>
{expense ? "-" : "+"}
{formatSalaryWalletAmount(Math.abs(delta))}
</span>
);
}
export function formatSalaryWalletAmount(value) {
const minor = Number(value || 0);
return `$${(minor / 100).toLocaleString("en-US", {
maximumFractionDigits: 2,
minimumFractionDigits: 2,
})}`;
}
function parseSalaryUSDMinor(value) {
const raw = String(value || "").trim();
if (!/^\d+(\.\d{1,2})?$/.test(raw)) {
throw new Error("请输入正确的 USD 金额,最多两位小数");
}
const [integerPart, decimalPart = ""] = raw.split(".");
const amountMinor = Number(integerPart) * 100 + Number(decimalPart.padEnd(2, "0"));
if (!Number.isSafeInteger(amountMinor) || amountMinor <= 0) {
throw new Error("请输入大于 0 的工资金额");
}
return amountMinor;
}
function salaryWalletRoleLabel(role) {
switch (role) {
case "host":
return "Host";
case "agency":
return "Agency";
case "bd":
return "BD";
case "bd_leader":
return "BD Leader";
default:
return role || "-";
}
}
function salaryWalletBizLabel(value) {
switch (value) {
case "manual_credit":
return "手工调整";
case "salary_settlement":
case "host_salary_settlement":
case "team_salary_settlement":
return "工资结算";
case "transfer":
return "转账";
case "withdraw":
return "提现";
default:
return value || "-";
}
}
function salaryWalletOperatorName(item) {
const operator = item.operator || {};
return operator.name || operator.username || item.operatorUserId || "-";
}
function makeSalaryWalletCommandId(role, userId) {
return `salary-wallet-${role || "role"}-${userId || "user"}-${Date.now()}`;
}