Added new translations for agent management and settlement features in English, Nepali, and Chinese, improving multi-language support. Updated the agents console to reflect changes in funding modes and player details, enhancing user experience. Refactored the admin permission gate to include new logic for handling bound line agents, ensuring better permission management. Additionally, streamlined the UI for agent-related pages and improved navigation to the settlement center, consolidating related functionalities for better accessibility.
269 lines
8.7 KiB
TypeScript
269 lines
8.7 KiB
TypeScript
"use client";
|
||
|
||
import { useTranslation } from "react-i18next";
|
||
|
||
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 { formatAdminCreditMajorDecimal } from "@/lib/money";
|
||
import { cn } from "@/lib/utils";
|
||
import type { AgentParentCaps } from "@/types/api/admin-agent";
|
||
|
||
export type AgentProfileFieldsProps = {
|
||
disabled?: boolean;
|
||
loading?: boolean;
|
||
parentCaps: AgentParentCaps | null;
|
||
availableCredit: number | null;
|
||
canCreateChildAgent: boolean;
|
||
isSuperAdmin: boolean;
|
||
shareRate: string;
|
||
onShareRateChange: (value: string) => void;
|
||
creditLimit: string;
|
||
onCreditLimitChange: (value: string) => void;
|
||
rebateLimit: string;
|
||
onRebateLimitChange: (value: string) => void;
|
||
defaultRebate: string;
|
||
onDefaultRebateChange: (value: string) => void;
|
||
settlementCycle: "daily" | "weekly" | "monthly";
|
||
onSettlementCycleChange: (value: "daily" | "weekly" | "monthly") => void;
|
||
extraRebate: boolean;
|
||
onExtraRebateChange: (value: boolean) => void;
|
||
canCreatePlayer: boolean;
|
||
onCanCreatePlayerChange: (value: boolean) => void;
|
||
canCreateChild: boolean;
|
||
onCanCreateChildChange: (value: boolean) => void;
|
||
riskTags: string;
|
||
onRiskTagsChange: (value: string) => void;
|
||
idPrefix?: string;
|
||
currencyCode?: string;
|
||
/** card:用于代理线路详情 Tab 内的卡片表单 */
|
||
variant?: "default" | "card";
|
||
};
|
||
|
||
export function AgentProfileFields({
|
||
disabled = false,
|
||
loading = false,
|
||
parentCaps,
|
||
availableCredit,
|
||
canCreateChildAgent,
|
||
isSuperAdmin,
|
||
shareRate,
|
||
onShareRateChange,
|
||
creditLimit,
|
||
onCreditLimitChange,
|
||
rebateLimit,
|
||
onRebateLimitChange,
|
||
defaultRebate,
|
||
onDefaultRebateChange,
|
||
settlementCycle,
|
||
onSettlementCycleChange,
|
||
extraRebate,
|
||
onExtraRebateChange,
|
||
canCreatePlayer,
|
||
onCanCreatePlayerChange,
|
||
canCreateChild,
|
||
onCanCreateChildChange,
|
||
riskTags,
|
||
onRiskTagsChange,
|
||
idPrefix = "agent-profile",
|
||
currencyCode = "NPR",
|
||
variant = "default",
|
||
}: AgentProfileFieldsProps): React.ReactElement {
|
||
const { t } = useTranslation(["agents", "common"]);
|
||
const fieldDisabled = disabled || loading;
|
||
const isCard = variant === "card";
|
||
|
||
return (
|
||
<div className="space-y-5">
|
||
{(parentCaps || availableCredit !== null) && !loading ? (
|
||
<div
|
||
className={cn(
|
||
"rounded-lg text-xs text-muted-foreground",
|
||
isCard ? "border border-border/60 bg-muted/25 px-3 py-2.5 space-y-1" : "space-y-1",
|
||
)}
|
||
>
|
||
{parentCaps ? (
|
||
<p>
|
||
{t("profile.parentCaps", {
|
||
defaultValue: "上级占成 {{share}}%,可下发额度 {{credit}}",
|
||
share: parentCaps.total_share_rate,
|
||
credit: formatAdminCreditMajorDecimal(parentCaps.available_credit, currencyCode),
|
||
})}
|
||
</p>
|
||
) : null}
|
||
{availableCredit !== null ? (
|
||
<p>
|
||
{t("profile.availableCredit", {
|
||
defaultValue: "可下发额度:{{amount}}",
|
||
amount: formatAdminCreditMajorDecimal(availableCredit, currencyCode),
|
||
})}
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
) : null}
|
||
{loading ? (
|
||
<p className="text-sm text-muted-foreground">
|
||
{t("profile.loading", { defaultValue: "正在加载占成与授信…" })}
|
||
</p>
|
||
) : null}
|
||
|
||
<div
|
||
className={cn(
|
||
"grid gap-4 sm:grid-cols-2",
|
||
fieldDisabled ? "pointer-events-none opacity-50" : "",
|
||
)}
|
||
>
|
||
<div className="space-y-2">
|
||
<Label htmlFor={`${idPrefix}-share-rate`}>
|
||
{t("profile.totalShareRate", { defaultValue: "占成比例 (%)" })}
|
||
</Label>
|
||
<Input
|
||
id={`${idPrefix}-share-rate`}
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
step="0.01"
|
||
value={shareRate}
|
||
onChange={(e) => onShareRateChange(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor={`${idPrefix}-credit-limit`}>
|
||
{t("profile.creditLimit", { defaultValue: "授信额度" })}
|
||
</Label>
|
||
<Input
|
||
id={`${idPrefix}-credit-limit`}
|
||
type="number"
|
||
min={0}
|
||
value={creditLimit}
|
||
onChange={(e) => onCreditLimitChange(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor={`${idPrefix}-rebate-limit`}>
|
||
{t("profile.rebateLimit", { defaultValue: "回水上限 (%)" })}
|
||
</Label>
|
||
<Input
|
||
id={`${idPrefix}-rebate-limit`}
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
step="0.01"
|
||
value={rebateLimit}
|
||
onChange={(e) => onRebateLimitChange(e.target.value)}
|
||
placeholder="0.5"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor={`${idPrefix}-default-rebate`}>
|
||
{t("profile.defaultPlayerRebate", { defaultValue: "默认玩家回水 (%)" })}
|
||
</Label>
|
||
<Input
|
||
id={`${idPrefix}-default-rebate`}
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
step="0.01"
|
||
value={defaultRebate}
|
||
onChange={(e) => onDefaultRebateChange(e.target.value)}
|
||
placeholder="0.5"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2 sm:col-span-2">
|
||
<Label htmlFor={`${idPrefix}-risk-tags`}>
|
||
{t("profile.riskTags", { defaultValue: "风控标签" })}
|
||
</Label>
|
||
<Input
|
||
id={`${idPrefix}-risk-tags`}
|
||
value={riskTags}
|
||
onChange={(e) => onRiskTagsChange(e.target.value)}
|
||
placeholder={t("profile.riskTagsPlaceholder", {
|
||
defaultValue: "逗号分隔,如 overdue, high_turnover",
|
||
})}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2 sm:col-span-2">
|
||
<Label htmlFor={`${idPrefix}-settlement-cycle`}>
|
||
{t("profile.settlementCycle", { defaultValue: "结算周期" })}
|
||
</Label>
|
||
<Select
|
||
value={settlementCycle}
|
||
onValueChange={(value) =>
|
||
onSettlementCycleChange((value as "daily" | "weekly" | "monthly") ?? "weekly")
|
||
}
|
||
>
|
||
<SelectTrigger id={`${idPrefix}-settlement-cycle`}>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="daily">{t("profile.cycleDaily", { defaultValue: "日结" })}</SelectItem>
|
||
<SelectItem value="weekly">{t("profile.cycleWeekly", { defaultValue: "周结" })}</SelectItem>
|
||
<SelectItem value="monthly">{t("profile.cycleMonthly", { defaultValue: "月结" })}</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
className={cn(
|
||
"space-y-4 border-t border-border/60 pt-4",
|
||
fieldDisabled ? "pointer-events-none opacity-50" : "",
|
||
)}
|
||
>
|
||
{!isCard ? (
|
||
<p className="text-xs text-muted-foreground">
|
||
{t("profile.capabilityHint", {
|
||
defaultValue:
|
||
"保存后约束该代理主账号能否开玩家/下级;与平台「代理」角色叠加,以本开关为准。",
|
||
})}
|
||
</p>
|
||
) : null}
|
||
<div className="grid gap-4 sm:grid-cols-1">
|
||
<SwitchRow
|
||
checked={extraRebate}
|
||
onCheckedChange={onExtraRebateChange}
|
||
label={t("profile.canGrantExtraRebate", { defaultValue: "允许额外回水" })}
|
||
/>
|
||
<SwitchRow
|
||
checked={canCreatePlayer}
|
||
onCheckedChange={onCanCreatePlayerChange}
|
||
label={t("profile.canCreatePlayer", { defaultValue: "允许创建玩家" })}
|
||
/>
|
||
<SwitchRow
|
||
checked={canCreateChild}
|
||
onCheckedChange={onCanCreateChildChange}
|
||
disabled={!canCreateChildAgent && !isSuperAdmin}
|
||
label={t("profile.canCreateChildAgent", { defaultValue: "允许创建下级代理" })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SwitchRow({
|
||
checked,
|
||
onCheckedChange,
|
||
label,
|
||
disabled = false,
|
||
}: {
|
||
checked: boolean;
|
||
onCheckedChange: (value: boolean) => void;
|
||
label: string;
|
||
disabled?: boolean;
|
||
}): React.ReactElement {
|
||
return (
|
||
<div className="flex items-center justify-between gap-4 rounded-lg border border-border/60 bg-muted/20 px-3 py-2.5">
|
||
<Label className="font-normal">{label}</Label>
|
||
<Switch checked={checked} onCheckedChange={onCheckedChange} disabled={disabled} />
|
||
</div>
|
||
);
|
||
}
|