|
|
"use client";
|
|
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
import { useParams } from "next/navigation";
|
|
|
import { AdminLayout } from "@/components/layout/admin-layout";
|
|
|
import { PageHeader } from "@/components/layout/page-header";
|
|
|
import { Card } from "@/components/ui/card";
|
|
|
import { ErrorBanner } from "@/components/ui/error-banner";
|
|
|
import { InputField } from "@/components/ui/input-field";
|
|
|
import { SelectField } from "@/components/ui/select-field";
|
|
|
import { PrimaryButton, SecondaryButton } from "@/components/ui/primary-button";
|
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
|
import {
|
|
|
adminCreateBusinessCustomer,
|
|
|
adminGetBusinessCustomerUsers,
|
|
|
adminGetBusinessCustomers,
|
|
|
adminGetMarkupConfigs,
|
|
|
adminListTenantUsers,
|
|
|
adminUpdateBusinessCustomer,
|
|
|
adminUpdateMarkupConfig,
|
|
|
} from "@/lib/frontend/api-client";
|
|
|
import { formatDateTime, formatPercent, formatUSD } from "@/lib/frontend/format";
|
|
|
import type {
|
|
|
BusinessCustomerRecord,
|
|
|
BusinessCustomerUserRecord,
|
|
|
DataPageStatus,
|
|
|
MarkupConfig,
|
|
|
MarkupType,
|
|
|
} from "@/lib/frontend/types";
|
|
|
|
|
|
function formatMarkupRule(
|
|
|
row: Pick<
|
|
|
MarkupConfig,
|
|
|
"markup_type" | "markup_percent" | "markup_fixed_amount"
|
|
|
>,
|
|
|
): string {
|
|
|
if (row.markup_type === "fixed") {
|
|
|
const amount = row.markup_fixed_amount ?? 0;
|
|
|
return amount > 0 ? `固定 ${formatUSD(amount)}` : "未配置";
|
|
|
}
|
|
|
return row.markup_percent > 0 ? formatPercent(row.markup_percent) : "0%";
|
|
|
}
|
|
|
|
|
|
export default function TenantBusinessCustomersPage() {
|
|
|
const params = useParams<{ customer_id: string }>();
|
|
|
const tenantId = decodeURIComponent(params.customer_id);
|
|
|
const { token } = useAuth();
|
|
|
const [status, setStatus] = useState<DataPageStatus>("loading");
|
|
|
const [rows, setRows] = useState<BusinessCustomerRecord[]>([]);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
/** 业务客户 → 当前加价摘要(打开/保存后刷新) */
|
|
|
const [markupByBcId, setMarkupByBcId] = useState<Record<string, string>>({});
|
|
|
|
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
|
const [creating, setCreating] = useState(false);
|
|
|
const [createError, setCreateError] = useState<string | null>(null);
|
|
|
const [createForm, setCreateForm] = useState({
|
|
|
business_customer_id: "",
|
|
|
name: "",
|
|
|
external_code: "",
|
|
|
remark: "",
|
|
|
});
|
|
|
|
|
|
const [editing, setEditing] = useState<BusinessCustomerRecord | null>(null);
|
|
|
const [savingEdit, setSavingEdit] = useState(false);
|
|
|
const [editError, setEditError] = useState<string | null>(null);
|
|
|
const [editForm, setEditForm] = useState({
|
|
|
name: "",
|
|
|
external_code: "",
|
|
|
remark: "",
|
|
|
status: "active" as "active" | "disabled",
|
|
|
});
|
|
|
|
|
|
const [markupTarget, setMarkupTarget] = useState<BusinessCustomerRecord | null>(
|
|
|
null,
|
|
|
);
|
|
|
const [markupType, setMarkupType] = useState<MarkupType>("percent");
|
|
|
const [markupPercent, setMarkupPercent] = useState("");
|
|
|
const [markupFixedAmount, setMarkupFixedAmount] = useState("");
|
|
|
const [markupRemark, setMarkupRemark] = useState("");
|
|
|
const [savingMarkup, setSavingMarkup] = useState(false);
|
|
|
const [loadingMarkup, setLoadingMarkup] = useState(false);
|
|
|
const [markupError, setMarkupError] = useState<string | null>(null);
|
|
|
const [markupSuccess, setMarkupSuccess] = useState<string | null>(null);
|
|
|
|
|
|
const [usersTarget, setUsersTarget] = useState<BusinessCustomerRecord | null>(
|
|
|
null,
|
|
|
);
|
|
|
const [usersStatus, setUsersStatus] = useState<DataPageStatus>("empty");
|
|
|
const [users, setUsers] = useState<BusinessCustomerUserRecord[]>([]);
|
|
|
const [usersError, setUsersError] = useState<string | null>(null);
|
|
|
const [onlyWithUsers, setOnlyWithUsers] = useState(true);
|
|
|
const [allUsersOpen, setAllUsersOpen] = useState(false);
|
|
|
const [allUsersKeyword, setAllUsersKeyword] = useState("");
|
|
|
const [allUsersStatus, setAllUsersStatus] = useState<DataPageStatus>("empty");
|
|
|
const [allUsers, setAllUsers] = useState<BusinessCustomerUserRecord[]>([]);
|
|
|
const [allUsersError, setAllUsersError] = useState<string | null>(null);
|
|
|
|
|
|
const load = useCallback(async () => {
|
|
|
if (!token) return;
|
|
|
setStatus("loading");
|
|
|
setError(null);
|
|
|
const res = await adminGetBusinessCustomers("", token, tenantId);
|
|
|
if (res.code !== 0) {
|
|
|
setStatus("error");
|
|
|
setError(res.message);
|
|
|
return;
|
|
|
}
|
|
|
setRows(res.data.list);
|
|
|
setStatus(res.data.list.length ? "success" : "empty");
|
|
|
}, [tenantId, token]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
void load();
|
|
|
}, [load]);
|
|
|
|
|
|
const sortedRows = useMemo(() => {
|
|
|
const filtered = onlyWithUsers
|
|
|
? rows.filter((r) => (r.user_count ?? 0) > 0)
|
|
|
: rows;
|
|
|
return [...filtered].sort((a, b) => {
|
|
|
const uc = (b.user_count ?? 0) - (a.user_count ?? 0);
|
|
|
if (uc !== 0) return uc;
|
|
|
const ac = (
|
|
|
a.external_code ||
|
|
|
a.name ||
|
|
|
a.business_customer_id
|
|
|
).toLowerCase();
|
|
|
const bc = (
|
|
|
b.external_code ||
|
|
|
b.name ||
|
|
|
b.business_customer_id
|
|
|
).toLowerCase();
|
|
|
return ac.localeCompare(bc);
|
|
|
});
|
|
|
}, [rows, onlyWithUsers]);
|
|
|
|
|
|
const loadAllUsers = useCallback(
|
|
|
async (keyword?: string) => {
|
|
|
if (!token) return;
|
|
|
setAllUsersStatus("loading");
|
|
|
setAllUsersError(null);
|
|
|
const res = await adminListTenantUsers("", token, tenantId, keyword);
|
|
|
if (res.code !== 0) {
|
|
|
setAllUsersStatus("error");
|
|
|
setAllUsersError(res.message);
|
|
|
return;
|
|
|
}
|
|
|
setAllUsers(res.data.list);
|
|
|
setAllUsersStatus(res.data.list.length ? "success" : "empty");
|
|
|
},
|
|
|
[tenantId, token],
|
|
|
);
|
|
|
|
|
|
const openEdit = (row: BusinessCustomerRecord) => {
|
|
|
setEditing(row);
|
|
|
setEditError(null);
|
|
|
setEditForm({
|
|
|
name: row.name,
|
|
|
external_code: row.external_code ?? "",
|
|
|
remark: row.remark ?? "",
|
|
|
status: row.status,
|
|
|
});
|
|
|
};
|
|
|
|
|
|
const handleCreate = async () => {
|
|
|
if (!token) return;
|
|
|
setCreating(true);
|
|
|
setCreateError(null);
|
|
|
const res = await adminCreateBusinessCustomer("", token, tenantId, createForm);
|
|
|
setCreating(false);
|
|
|
if (res.code !== 0) {
|
|
|
setCreateError(res.message);
|
|
|
return;
|
|
|
}
|
|
|
setCreateOpen(false);
|
|
|
setCreateForm({
|
|
|
business_customer_id: "",
|
|
|
name: "",
|
|
|
external_code: "",
|
|
|
remark: "",
|
|
|
});
|
|
|
await load();
|
|
|
};
|
|
|
|
|
|
const handleSaveEdit = async () => {
|
|
|
if (!token || !editing) return;
|
|
|
setSavingEdit(true);
|
|
|
setEditError(null);
|
|
|
const res = await adminUpdateBusinessCustomer(
|
|
|
"",
|
|
|
token,
|
|
|
tenantId,
|
|
|
editing.business_customer_id,
|
|
|
editForm,
|
|
|
);
|
|
|
setSavingEdit(false);
|
|
|
if (res.code !== 0) {
|
|
|
setEditError(res.message);
|
|
|
return;
|
|
|
}
|
|
|
setRows((list) =>
|
|
|
list.map((item) =>
|
|
|
item.business_customer_id === res.data.business_customer_id
|
|
|
? res.data
|
|
|
: item,
|
|
|
),
|
|
|
);
|
|
|
setEditing(null);
|
|
|
};
|
|
|
|
|
|
const openMarkup = async (row: BusinessCustomerRecord) => {
|
|
|
setMarkupTarget(row);
|
|
|
setMarkupType("percent");
|
|
|
setMarkupPercent("");
|
|
|
setMarkupFixedAmount("");
|
|
|
setMarkupRemark("");
|
|
|
setMarkupError(null);
|
|
|
setMarkupSuccess(null);
|
|
|
if (!token) {
|
|
|
setMarkupError("未登录或登录已失效,请重新登录后再保存");
|
|
|
return;
|
|
|
}
|
|
|
setLoadingMarkup(true);
|
|
|
const kw =
|
|
|
row.external_code?.trim() ||
|
|
|
row.business_customer_id ||
|
|
|
row.name ||
|
|
|
"";
|
|
|
const res = await adminGetMarkupConfigs("", token, 1, 50, kw);
|
|
|
setLoadingMarkup(false);
|
|
|
if (res.code !== 0) {
|
|
|
setMarkupError(`读取已有加价失败:${res.message}`);
|
|
|
return;
|
|
|
}
|
|
|
const found = res.data.list.find(
|
|
|
(c) =>
|
|
|
c.customer_id === tenantId &&
|
|
|
(c.business_customer_id ?? "") === row.business_customer_id,
|
|
|
);
|
|
|
if (!found) return;
|
|
|
setMarkupType(found.markup_type);
|
|
|
setMarkupPercent(String(found.markup_percent ?? ""));
|
|
|
setMarkupFixedAmount(
|
|
|
found.markup_fixed_amount !== null && found.markup_fixed_amount !== undefined
|
|
|
? String(found.markup_fixed_amount)
|
|
|
: "",
|
|
|
);
|
|
|
setMarkupRemark(found.remark ?? "");
|
|
|
setMarkupByBcId((m) => ({
|
|
|
...m,
|
|
|
[row.business_customer_id]: formatMarkupRule(found),
|
|
|
}));
|
|
|
};
|
|
|
|
|
|
const handleSaveMarkup = async () => {
|
|
|
if (!markupTarget) return;
|
|
|
if (!token) {
|
|
|
setMarkupError("未登录或登录已失效,请重新登录后再保存");
|
|
|
return;
|
|
|
}
|
|
|
setMarkupError(null);
|
|
|
setMarkupSuccess(null);
|
|
|
|
|
|
let percentValue = 0;
|
|
|
let fixedValue: number | undefined;
|
|
|
if (markupType === "percent") {
|
|
|
const raw = markupPercent.trim();
|
|
|
if (!raw) {
|
|
|
setMarkupError("请填写加价比例(0~30),空值不会保存");
|
|
|
return;
|
|
|
}
|
|
|
const n = Number(raw);
|
|
|
if (Number.isNaN(n) || n < 0 || n > 30) {
|
|
|
setMarkupError("加价比例须在 0~30% 之间");
|
|
|
return;
|
|
|
}
|
|
|
percentValue = n;
|
|
|
} else {
|
|
|
const raw = markupFixedAmount.trim();
|
|
|
if (!raw) {
|
|
|
setMarkupError("请填写固定加价金额(USD),空值不会保存");
|
|
|
return;
|
|
|
}
|
|
|
const n = Number(raw);
|
|
|
if (Number.isNaN(n) || n < 0) {
|
|
|
setMarkupError("固定加价金额须 ≥ 0");
|
|
|
return;
|
|
|
}
|
|
|
fixedValue = n;
|
|
|
}
|
|
|
|
|
|
setSavingMarkup(true);
|
|
|
const res = await adminUpdateMarkupConfig("", token, tenantId, {
|
|
|
business_customer_id: markupTarget.business_customer_id,
|
|
|
markup_type: markupType,
|
|
|
markup_percent: markupType === "percent" ? percentValue : 0,
|
|
|
markup_fixed_amount: markupType === "fixed" ? fixedValue : undefined,
|
|
|
remark: markupRemark,
|
|
|
});
|
|
|
setSavingMarkup(false);
|
|
|
if (res.code !== 0) {
|
|
|
setMarkupError(res.message || "保存失败,请稍后重试");
|
|
|
return;
|
|
|
}
|
|
|
const summary = formatMarkupRule(res.data);
|
|
|
setMarkupByBcId((m) => ({
|
|
|
...m,
|
|
|
[markupTarget.business_customer_id]: summary,
|
|
|
}));
|
|
|
setMarkupSuccess(`已保存:${summary}`);
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
<AdminLayout>
|
|
|
<PageHeader
|
|
|
title="业务客户"
|
|
|
subtitle={`租户 ${tenantId} · 客户组织与登录账号(加价按客户代码,识别靠账号)`}
|
|
|
action={
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
<SecondaryButton
|
|
|
onClick={() => {
|
|
|
setAllUsersOpen(true);
|
|
|
void loadAllUsers(allUsersKeyword);
|
|
|
}}
|
|
|
>
|
|
|
全部登录账号
|
|
|
</SecondaryButton>
|
|
|
<PrimaryButton onClick={() => setCreateOpen(true)}>
|
|
|
新增业务客户
|
|
|
</PrimaryButton>
|
|
|
</div>
|
|
|
}
|
|
|
/>
|
|
|
|
|
|
{status === "loading" && (
|
|
|
<div className="space-y-2">
|
|
|
{Array.from({ length: 4 }).map((_, i) => (
|
|
|
<Skeleton key={i} className="h-14 w-full" />
|
|
|
))}
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
{status === "error" && (
|
|
|
<ErrorBanner action={<SecondaryButton onClick={() => void load()}>重试</SecondaryButton>}>
|
|
|
{error ?? "加载业务客户失败"}
|
|
|
</ErrorBanner>
|
|
|
)}
|
|
|
|
|
|
{status === "empty" && (
|
|
|
<Card className="p-8 text-center text-text-secondary">
|
|
|
当前租户还没有业务客户
|
|
|
</Card>
|
|
|
)}
|
|
|
|
|
|
{status === "success" && (
|
|
|
<Card className="overflow-x-auto p-0">
|
|
|
<div className="flex flex-wrap items-center gap-3 border-b border-border px-4 py-3 text-sm">
|
|
|
<label className="inline-flex items-center gap-2 text-text-secondary">
|
|
|
<input
|
|
|
type="checkbox"
|
|
|
checked={onlyWithUsers}
|
|
|
onChange={(e) => setOnlyWithUsers(e.target.checked)}
|
|
|
/>
|
|
|
仅显示有登录账号的客户({rows.filter((r) => (r.user_count ?? 0) > 0).length}/
|
|
|
{rows.length})
|
|
|
</label>
|
|
|
</div>
|
|
|
<table className="min-w-full text-sm">
|
|
|
<thead className="bg-bg text-left text-text-secondary">
|
|
|
<tr>
|
|
|
<th className="px-4 py-3 font-medium">客户代码</th>
|
|
|
<th className="px-4 py-3 font-medium">客户名称</th>
|
|
|
<th className="px-4 py-3 font-medium">加价规则</th>
|
|
|
<th className="px-4 py-3 font-medium">登录账号数</th>
|
|
|
<th className="px-4 py-3 font-medium">状态</th>
|
|
|
<th className="px-4 py-3 font-medium">备注</th>
|
|
|
<th className="px-4 py-3 font-medium">更新时间</th>
|
|
|
<th className="px-4 py-3 font-medium">操作</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
{sortedRows.map((row) => (
|
|
|
<tr key={row.business_customer_id} className="border-t border-border">
|
|
|
<td className="px-4 py-3 font-mono">
|
|
|
{row.external_code || "—"}
|
|
|
</td>
|
|
|
<td className="px-4 py-3">{row.name}</td>
|
|
|
<td className="px-4 py-3 font-mono text-primary">
|
|
|
{markupByBcId[row.business_customer_id] ?? "—"}
|
|
|
</td>
|
|
|
<td className="px-4 py-3 font-mono">{row.user_count ?? 0}</td>
|
|
|
<td className="px-4 py-3">{row.status === "active" ? "启用" : "停用"}</td>
|
|
|
<td className="px-4 py-3 text-text-secondary">{row.remark || "—"}</td>
|
|
|
<td className="px-4 py-3 text-text-secondary">
|
|
|
{formatDateTime(row.updated_at)}
|
|
|
</td>
|
|
|
<td className="px-4 py-3">
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
<SecondaryButton onClick={() => openEdit(row)}>
|
|
|
编辑
|
|
|
</SecondaryButton>
|
|
|
<SecondaryButton
|
|
|
onClick={() => void openMarkup(row)}
|
|
|
>
|
|
|
客户加价
|
|
|
</SecondaryButton>
|
|
|
<SecondaryButton
|
|
|
onClick={async () => {
|
|
|
setUsersTarget(row);
|
|
|
setUsersError(null);
|
|
|
setUsers([]);
|
|
|
setUsersStatus("loading");
|
|
|
if (!token) return;
|
|
|
const res = await adminGetBusinessCustomerUsers(
|
|
|
"",
|
|
|
token,
|
|
|
tenantId,
|
|
|
row.business_customer_id,
|
|
|
);
|
|
|
if (res.code !== 0) {
|
|
|
setUsersStatus("error");
|
|
|
setUsersError(res.message);
|
|
|
return;
|
|
|
}
|
|
|
setUsers(res.data.list);
|
|
|
setUsersStatus(
|
|
|
res.data.list.length ? "success" : "empty",
|
|
|
);
|
|
|
}}
|
|
|
>
|
|
|
登录账号
|
|
|
</SecondaryButton>
|
|
|
</div>
|
|
|
</td>
|
|
|
</tr>
|
|
|
))}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</Card>
|
|
|
)}
|
|
|
|
|
|
{createOpen && (
|
|
|
<div
|
|
|
className="fixed inset-0 z-50 flex justify-end bg-black/30"
|
|
|
onClick={() => !creating && setCreateOpen(false)}
|
|
|
>
|
|
|
<div
|
|
|
className="h-full w-full max-w-md bg-surface p-6 shadow-lg"
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
>
|
|
|
<h3 className="text-lg font-semibold">新增业务客户</h3>
|
|
|
<div className="mt-6 space-y-4">
|
|
|
<InputField
|
|
|
label="客户 ID"
|
|
|
value={createForm.business_customer_id}
|
|
|
onChange={(e) =>
|
|
|
setCreateForm((prev) => ({
|
|
|
...prev,
|
|
|
business_customer_id: e.target.value,
|
|
|
}))
|
|
|
}
|
|
|
/>
|
|
|
<InputField
|
|
|
label="名称"
|
|
|
value={createForm.name}
|
|
|
onChange={(e) =>
|
|
|
setCreateForm((prev) => ({ ...prev, name: e.target.value }))
|
|
|
}
|
|
|
/>
|
|
|
<InputField
|
|
|
label="外部编码"
|
|
|
value={createForm.external_code}
|
|
|
onChange={(e) =>
|
|
|
setCreateForm((prev) => ({
|
|
|
...prev,
|
|
|
external_code: e.target.value,
|
|
|
}))
|
|
|
}
|
|
|
/>
|
|
|
<InputField
|
|
|
label="备注"
|
|
|
value={createForm.remark}
|
|
|
onChange={(e) =>
|
|
|
setCreateForm((prev) => ({ ...prev, remark: e.target.value }))
|
|
|
}
|
|
|
/>
|
|
|
{createError ? <ErrorBanner>{createError}</ErrorBanner> : null}
|
|
|
<div className="flex justify-end gap-2">
|
|
|
<SecondaryButton onClick={() => setCreateOpen(false)}>取消</SecondaryButton>
|
|
|
<PrimaryButton loading={creating} onClick={() => void handleCreate()}>
|
|
|
保存
|
|
|
</PrimaryButton>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
{editing && (
|
|
|
<div
|
|
|
className="fixed inset-0 z-50 flex justify-end bg-black/30"
|
|
|
onClick={() => !savingEdit && setEditing(null)}
|
|
|
>
|
|
|
<div
|
|
|
className="h-full w-full max-w-md bg-surface p-6 shadow-lg"
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
>
|
|
|
<h3 className="text-lg font-semibold">编辑业务客户</h3>
|
|
|
<div className="mt-6 space-y-4">
|
|
|
<InputField
|
|
|
label="名称"
|
|
|
value={editForm.name}
|
|
|
onChange={(e) =>
|
|
|
setEditForm((prev) => ({ ...prev, name: e.target.value }))
|
|
|
}
|
|
|
/>
|
|
|
<InputField
|
|
|
label="外部编码"
|
|
|
value={editForm.external_code}
|
|
|
onChange={(e) =>
|
|
|
setEditForm((prev) => ({
|
|
|
...prev,
|
|
|
external_code: e.target.value,
|
|
|
}))
|
|
|
}
|
|
|
/>
|
|
|
<SelectField
|
|
|
label="状态"
|
|
|
value={editForm.status}
|
|
|
options={[
|
|
|
{ value: "active", label: "启用" },
|
|
|
{ value: "disabled", label: "停用" },
|
|
|
]}
|
|
|
onChange={(e) =>
|
|
|
setEditForm((prev) => ({
|
|
|
...prev,
|
|
|
status: e.target.value as "active" | "disabled",
|
|
|
}))
|
|
|
}
|
|
|
/>
|
|
|
<InputField
|
|
|
label="备注"
|
|
|
value={editForm.remark}
|
|
|
onChange={(e) =>
|
|
|
setEditForm((prev) => ({ ...prev, remark: e.target.value }))
|
|
|
}
|
|
|
/>
|
|
|
{editError ? <ErrorBanner>{editError}</ErrorBanner> : null}
|
|
|
<div className="flex justify-end gap-2">
|
|
|
<SecondaryButton onClick={() => setEditing(null)}>取消</SecondaryButton>
|
|
|
<PrimaryButton loading={savingEdit} onClick={() => void handleSaveEdit()}>
|
|
|
保存
|
|
|
</PrimaryButton>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
{markupTarget && (
|
|
|
<div
|
|
|
className="fixed inset-0 z-50 flex justify-end bg-black/30"
|
|
|
onClick={() => !savingMarkup && setMarkupTarget(null)}
|
|
|
>
|
|
|
<div
|
|
|
className="h-full w-full max-w-md bg-surface p-6 shadow-lg"
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
>
|
|
|
<h3 className="text-lg font-semibold">
|
|
|
客户加价 ·{" "}
|
|
|
{[markupTarget.external_code, markupTarget.name]
|
|
|
.filter(Boolean)
|
|
|
.join(" · ") || markupTarget.business_customer_id}
|
|
|
</h3>
|
|
|
<p className="mt-1 text-sm text-text-secondary">
|
|
|
仅对本业务客户生效;未配置时询价按 0% 处理。
|
|
|
</p>
|
|
|
<div className="mt-6 space-y-4">
|
|
|
{loadingMarkup ? (
|
|
|
<Skeleton className="h-10 w-full" />
|
|
|
) : null}
|
|
|
<SelectField
|
|
|
label="加价方式"
|
|
|
value={markupType}
|
|
|
disabled={savingMarkup || loadingMarkup}
|
|
|
options={[
|
|
|
{ value: "percent", label: "按运费百分比" },
|
|
|
{ value: "fixed", label: "固定金额(USD)" },
|
|
|
]}
|
|
|
onChange={(e) => {
|
|
|
setMarkupType(e.target.value as MarkupType);
|
|
|
setMarkupError(null);
|
|
|
setMarkupSuccess(null);
|
|
|
}}
|
|
|
/>
|
|
|
{markupType === "percent" ? (
|
|
|
<InputField
|
|
|
label="加价比例(%)"
|
|
|
type="number"
|
|
|
step="0.01"
|
|
|
min={0}
|
|
|
max={30}
|
|
|
value={markupPercent}
|
|
|
disabled={savingMarkup || loadingMarkup}
|
|
|
onChange={(e) => {
|
|
|
setMarkupPercent(e.target.value);
|
|
|
setMarkupError(null);
|
|
|
setMarkupSuccess(null);
|
|
|
}}
|
|
|
/>
|
|
|
) : (
|
|
|
<InputField
|
|
|
label="固定金额(USD)"
|
|
|
type="number"
|
|
|
step="0.01"
|
|
|
min={0}
|
|
|
value={markupFixedAmount}
|
|
|
disabled={savingMarkup || loadingMarkup}
|
|
|
onChange={(e) => {
|
|
|
setMarkupFixedAmount(e.target.value);
|
|
|
setMarkupError(null);
|
|
|
setMarkupSuccess(null);
|
|
|
}}
|
|
|
/>
|
|
|
)}
|
|
|
<InputField
|
|
|
label="备注"
|
|
|
value={markupRemark}
|
|
|
disabled={savingMarkup || loadingMarkup}
|
|
|
onChange={(e) => setMarkupRemark(e.target.value)}
|
|
|
/>
|
|
|
<Card className="bg-bg text-sm text-text-secondary">
|
|
|
保存后按客户组织 ID 匹配加价。百分比上限 30%;固定金额为每档报价叠加。空值不会保存,也不会覆盖已有规则。
|
|
|
</Card>
|
|
|
{markupError ? <ErrorBanner>{markupError}</ErrorBanner> : null}
|
|
|
{markupSuccess ? (
|
|
|
<Card className="border border-emerald-200 bg-emerald-50 text-sm text-emerald-900">
|
|
|
{markupSuccess}
|
|
|
</Card>
|
|
|
) : null}
|
|
|
<div className="flex justify-end gap-2">
|
|
|
<SecondaryButton
|
|
|
type="button"
|
|
|
disabled={savingMarkup}
|
|
|
onClick={() => setMarkupTarget(null)}
|
|
|
>
|
|
|
{markupSuccess ? "关闭" : "取消"}
|
|
|
</SecondaryButton>
|
|
|
<PrimaryButton
|
|
|
type="button"
|
|
|
loading={savingMarkup}
|
|
|
disabled={loadingMarkup}
|
|
|
onClick={() => void handleSaveMarkup()}
|
|
|
>
|
|
|
保存
|
|
|
</PrimaryButton>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
{usersTarget && (
|
|
|
<div
|
|
|
className="fixed inset-0 z-50 flex justify-end bg-black/30"
|
|
|
onClick={() => setUsersTarget(null)}
|
|
|
>
|
|
|
<div
|
|
|
className="flex h-full w-full max-w-lg flex-col bg-surface p-6 shadow-lg"
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
>
|
|
|
<h3 className="text-lg font-semibold">
|
|
|
登录账号 ·{" "}
|
|
|
{[usersTarget.external_code, usersTarget.name]
|
|
|
.filter(Boolean)
|
|
|
.join(" · ") || usersTarget.business_customer_id}
|
|
|
</h3>
|
|
|
<p className="mt-1 text-sm text-text-secondary">
|
|
|
来自 ccnew PC/Web 登录账号(LR_Base_User),用于询价识别加价客户。
|
|
|
</p>
|
|
|
<div className="mt-4 flex-1 overflow-auto">
|
|
|
{usersStatus === "loading" ? <Skeleton className="h-24 w-full" /> : null}
|
|
|
{usersError ? <ErrorBanner>{usersError}</ErrorBanner> : null}
|
|
|
{usersStatus === "empty" ? (
|
|
|
<p className="text-sm text-text-secondary">暂无同步账号</p>
|
|
|
) : null}
|
|
|
{usersStatus === "success" ? (
|
|
|
<table className="min-w-full text-sm">
|
|
|
<thead className="bg-bg text-left text-text-secondary">
|
|
|
<tr>
|
|
|
<th className="px-3 py-2 font-medium">账号</th>
|
|
|
<th className="px-3 py-2 font-medium">姓名</th>
|
|
|
<th className="px-3 py-2 font-medium">状态</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
{users.map((u) => (
|
|
|
<tr
|
|
|
key={u.external_user_id}
|
|
|
className="border-t border-border"
|
|
|
>
|
|
|
<td className="px-3 py-2 font-mono">{u.account}</td>
|
|
|
<td className="px-3 py-2">{u.display_name || "—"}</td>
|
|
|
<td className="px-3 py-2">
|
|
|
{u.status === "active" ? "启用" : "停用"}
|
|
|
</td>
|
|
|
</tr>
|
|
|
))}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
) : null}
|
|
|
</div>
|
|
|
<div className="mt-4 flex justify-end">
|
|
|
<SecondaryButton onClick={() => setUsersTarget(null)}>关闭</SecondaryButton>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
{allUsersOpen && (
|
|
|
<div
|
|
|
className="fixed inset-0 z-50 flex justify-end bg-black/30"
|
|
|
onClick={() => setAllUsersOpen(false)}
|
|
|
>
|
|
|
<div
|
|
|
className="flex h-full w-full max-w-2xl flex-col bg-surface p-6 shadow-lg"
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
>
|
|
|
<h3 className="text-lg font-semibold">全部登录账号</h3>
|
|
|
<p className="mt-1 text-sm text-text-secondary">
|
|
|
对应 ccnew「用户管理」中的客户账号;询价靠账号识别所属客户代码再套加价。
|
|
|
</p>
|
|
|
<div className="mt-4 flex gap-2">
|
|
|
<InputField
|
|
|
label="搜索账号 / 客户代码 / 名称"
|
|
|
value={allUsersKeyword}
|
|
|
onChange={(e) => setAllUsersKeyword(e.target.value)}
|
|
|
/>
|
|
|
<div className="flex items-end">
|
|
|
<SecondaryButton onClick={() => void loadAllUsers(allUsersKeyword)}>
|
|
|
查询
|
|
|
</SecondaryButton>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div className="mt-4 flex-1 overflow-auto">
|
|
|
{allUsersStatus === "loading" ? (
|
|
|
<Skeleton className="h-24 w-full" />
|
|
|
) : null}
|
|
|
{allUsersError ? <ErrorBanner>{allUsersError}</ErrorBanner> : null}
|
|
|
{allUsersStatus === "empty" ? (
|
|
|
<p className="text-sm text-text-secondary">暂无账号</p>
|
|
|
) : null}
|
|
|
{allUsersStatus === "success" ? (
|
|
|
<table className="min-w-full text-sm">
|
|
|
<thead className="bg-bg text-left text-text-secondary">
|
|
|
<tr>
|
|
|
<th className="px-3 py-2 font-medium">账号</th>
|
|
|
<th className="px-3 py-2 font-medium">姓名</th>
|
|
|
<th className="px-3 py-2 font-medium">客户代码</th>
|
|
|
<th className="px-3 py-2 font-medium">客户名称</th>
|
|
|
<th className="px-3 py-2 font-medium">状态</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
{allUsers.map((u) => (
|
|
|
<tr
|
|
|
key={u.external_user_id}
|
|
|
className="border-t border-border"
|
|
|
>
|
|
|
<td className="px-3 py-2 font-mono">{u.account}</td>
|
|
|
<td className="px-3 py-2">{u.display_name || "—"}</td>
|
|
|
<td className="px-3 py-2 font-mono">
|
|
|
{u.business_customer_code || "—"}
|
|
|
</td>
|
|
|
<td className="px-3 py-2">
|
|
|
{u.business_customer_name || "—"}
|
|
|
</td>
|
|
|
<td className="px-3 py-2">
|
|
|
{u.status === "active" ? "启用" : "停用"}
|
|
|
</td>
|
|
|
</tr>
|
|
|
))}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
) : null}
|
|
|
</div>
|
|
|
<div className="mt-4 flex justify-end">
|
|
|
<SecondaryButton onClick={() => setAllUsersOpen(false)}>关闭</SecondaryButton>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
</AdminLayout>
|
|
|
);
|
|
|
}
|