Added new types and API functions for settlement period summaries and credit ledgers, improving the management of agent settlements. Updated the admin console to reflect these changes, enhancing user experience with better navigation and data presentation. Additionally, expanded multi-language support by incorporating new translations in English, Nepali, and Chinese for settlement-related terms, ensuring consistency across the platform.
300 lines
11 KiB
TypeScript
300 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useMemo, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from "sonner";
|
|
|
|
import { postAdminAgentLine } from "@/api/admin-agent-lines";
|
|
import { getAdminIntegrationSites } from "@/api/admin-integration-sites";
|
|
import { AdminPageCard } from "@/components/admin/admin-page-card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
|
import { percentUiToRatio } from "@/lib/admin-rate-percent";
|
|
import { adminSiteCodeLabel } from "@/lib/admin-select-display";
|
|
import { LotteryApiBizError } from "@/types/api/errors";
|
|
import type { AdminIntegrationSiteRow } from "@/types/api/admin-integration-site";
|
|
|
|
export function AgentLineProvisionWizard(): React.ReactElement {
|
|
const { t } = useTranslation(["agents", "common"]);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [sitesLoading, setSitesLoading] = useState(true);
|
|
const [sites, setSites] = useState<AdminIntegrationSiteRow[]>([]);
|
|
const [form, setForm] = useState({
|
|
site_code: "",
|
|
code: "",
|
|
name: "",
|
|
username: "",
|
|
password: "",
|
|
total_share_rate: "0",
|
|
credit_limit: "0",
|
|
rebate_limit: "0",
|
|
default_player_rebate: "0",
|
|
settlement_cycle: "weekly" as "daily" | "weekly" | "monthly",
|
|
can_grant_extra_rebate: false,
|
|
});
|
|
|
|
useAsyncEffect(() => {
|
|
setSitesLoading(true);
|
|
void getAdminIntegrationSites()
|
|
.then((data) => setSites(data.items))
|
|
.catch(() => setSites([]))
|
|
.finally(() => setSitesLoading(false));
|
|
}, []);
|
|
|
|
const unboundSites = useMemo(
|
|
() => sites.filter((row) => !row.has_line_root),
|
|
[sites],
|
|
);
|
|
|
|
async function onSubmit(e: React.FormEvent): Promise<void> {
|
|
e.preventDefault();
|
|
if (!form.site_code.trim()) {
|
|
toast.error(t("agents:lineProvision.siteRequired", { defaultValue: "请选择接入站点" }));
|
|
return;
|
|
}
|
|
setSubmitting(true);
|
|
try {
|
|
await postAdminAgentLine({
|
|
site_code: form.site_code.trim().toLowerCase(),
|
|
code: form.code.trim().toLowerCase(),
|
|
name: form.name.trim(),
|
|
username: form.username.trim(),
|
|
password: form.password,
|
|
total_share_rate: Number.parseFloat(form.total_share_rate) || 0,
|
|
credit_limit: Number.parseInt(form.credit_limit, 10) || 0,
|
|
rebate_limit: percentUiToRatio(form.rebate_limit),
|
|
default_player_rebate: percentUiToRatio(form.default_player_rebate),
|
|
settlement_cycle: form.settlement_cycle,
|
|
can_grant_extra_rebate: form.can_grant_extra_rebate,
|
|
});
|
|
toast.success(t("agents:lineProvision.success", { defaultValue: "一级代理已创建" }));
|
|
setForm((f) => ({
|
|
...f,
|
|
site_code: "",
|
|
code: "",
|
|
name: "",
|
|
username: "",
|
|
password: "",
|
|
}));
|
|
const data = await getAdminIntegrationSites();
|
|
setSites(data.items);
|
|
} catch (err) {
|
|
const msg =
|
|
err instanceof LotteryApiBizError ? err.message : t("common:error.generic");
|
|
toast.error(msg);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<AdminPageCard title={t("agents:lineProvision.title", { defaultValue: "创建一级代理" })}>
|
|
<p className="mb-2 max-w-xl text-sm text-muted-foreground">
|
|
{t("agents:subnav.provisionHint", {
|
|
defaultValue:
|
|
"请先在「平台管理 → 接入配置」创建接入站点;对接密钥在站点创建时一次性展示。",
|
|
})}
|
|
</p>
|
|
<p className="mb-4 max-w-xl text-sm text-muted-foreground">
|
|
{t("agents:lineProvision.description", {
|
|
defaultValue:
|
|
"将一级代理绑定到已有接入站点,并配置后台登录账号与占成、授信、回水、结算周期。代理编码创建后不可修改。",
|
|
})}{" "}
|
|
<Link
|
|
href="/admin/config/integration-sites"
|
|
className="font-medium text-primary underline-offset-4 hover:underline"
|
|
>
|
|
{t("agents:lineProvision.openIntegrationSites", {
|
|
defaultValue: "前往接入站点",
|
|
})}
|
|
</Link>
|
|
</p>
|
|
<form className="grid max-w-xl gap-4" onSubmit={onSubmit}>
|
|
<div className="grid gap-2">
|
|
<Label>{t("agents:lineProvision.siteCode", { defaultValue: "接入站点" })}</Label>
|
|
<Select
|
|
value={form.site_code}
|
|
onValueChange={(value) => setForm((f) => ({ ...f, site_code: value ?? "" }))}
|
|
disabled={sitesLoading || unboundSites.length === 0}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue>
|
|
{(v) =>
|
|
adminSiteCodeLabel(
|
|
v,
|
|
unboundSites,
|
|
sitesLoading
|
|
? t("common:loading", { defaultValue: "加载中…" })
|
|
: unboundSites.length === 0
|
|
? t("agents:lineProvision.noUnboundSite", {
|
|
defaultValue: "暂无未绑定一级代理的站点",
|
|
})
|
|
: t("agents:lineProvision.siteCodePlaceholder", {
|
|
defaultValue: "选择站点",
|
|
}),
|
|
)
|
|
}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{unboundSites.map((site) => (
|
|
<SelectItem key={site.id} value={site.code}>
|
|
{site.name} ({site.code})
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>{t("agents:lineProvision.code", { defaultValue: "代理编码" })}</Label>
|
|
<Input
|
|
value={form.code}
|
|
onChange={(e) => setForm((f) => ({ ...f, code: e.target.value }))}
|
|
required
|
|
pattern="[a-z0-9][a-z0-9_-]*"
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>{t("agents:lineProvision.name", { defaultValue: "一级代理名称" })}</Label>
|
|
<Input
|
|
value={form.name}
|
|
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>{t("agents:lineProvision.username", { defaultValue: "后台登录账号" })}</Label>
|
|
<Input
|
|
value={form.username}
|
|
onChange={(e) => setForm((f) => ({ ...f, username: e.target.value }))}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>{t("agents:lineProvision.password", { defaultValue: "初始密码" })}</Label>
|
|
<Input
|
|
type="password"
|
|
value={form.password}
|
|
onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))}
|
|
required
|
|
minLength={8}
|
|
/>
|
|
</div>
|
|
|
|
<p className="text-sm font-medium">
|
|
{t("agents:profile.section", { defaultValue: "占成与授信" })}
|
|
</p>
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
<div className="grid gap-2">
|
|
<Label>{t("agents:profile.totalShareRate", { defaultValue: "占成比例 (%)" })}</Label>
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
max={100}
|
|
step="0.01"
|
|
value={form.total_share_rate}
|
|
onChange={(e) => setForm((f) => ({ ...f, total_share_rate: e.target.value }))}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>{t("agents:profile.creditLimit", { defaultValue: "授信额度" })}</Label>
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
value={form.credit_limit}
|
|
onChange={(e) => setForm((f) => ({ ...f, credit_limit: e.target.value }))}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>{t("agents:profile.rebateLimit", { defaultValue: "回水上限 (%)" })}</Label>
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
max={100}
|
|
step="0.01"
|
|
value={form.rebate_limit}
|
|
placeholder="0.5"
|
|
onChange={(e) => setForm((f) => ({ ...f, rebate_limit: e.target.value }))}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>{t("agents:profile.defaultPlayerRebate", { defaultValue: "默认玩家回水 (%)" })}</Label>
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
max={100}
|
|
step="0.01"
|
|
value={form.default_player_rebate}
|
|
placeholder="0.5"
|
|
onChange={(e) => setForm((f) => ({ ...f, default_player_rebate: e.target.value }))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label>{t("agents:profile.settlementCycle", { defaultValue: "结算周期" })}</Label>
|
|
<Select
|
|
value={form.settlement_cycle}
|
|
onValueChange={(value) =>
|
|
setForm((f) => ({
|
|
...f,
|
|
settlement_cycle: (value as "daily" | "weekly" | "monthly") ?? "weekly",
|
|
}))
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue>
|
|
{(v) =>
|
|
v === "daily"
|
|
? t("agents:profile.cycleDaily", { defaultValue: "日结" })
|
|
: v === "monthly"
|
|
? t("agents:profile.cycleMonthly", { defaultValue: "月结" })
|
|
: t("agents:profile.cycleWeekly", { defaultValue: "周结" })
|
|
}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="daily">
|
|
{t("agents:profile.cycleDaily", { defaultValue: "日结" })}
|
|
</SelectItem>
|
|
<SelectItem value="weekly">
|
|
{t("agents:profile.cycleWeekly", { defaultValue: "周结" })}
|
|
</SelectItem>
|
|
<SelectItem value="monthly">
|
|
{t("agents:profile.cycleMonthly", { defaultValue: "月结" })}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
checked={form.can_grant_extra_rebate}
|
|
onCheckedChange={(checked) =>
|
|
setForm((f) => ({ ...f, can_grant_extra_rebate: checked }))
|
|
}
|
|
/>
|
|
<Label>
|
|
{t("agents:profile.canGrantExtraRebate", { defaultValue: "允许额外回水" })}
|
|
</Label>
|
|
</div>
|
|
|
|
<Button type="submit" disabled={submitting || unboundSites.length === 0}>
|
|
{submitting
|
|
? t("common:submitting", { defaultValue: "提交中…" })
|
|
: t("agents:lineProvision.submit", { defaultValue: "创建一级代理" })}
|
|
</Button>
|
|
</form>
|
|
</AdminPageCard>
|
|
);
|
|
}
|