feat(admin, settlement, dashboard): strengthen permission gating and billing workflows
This commit is contained in:
@@ -5,9 +5,13 @@ import { useCallback, useMemo, useState, type ReactElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
BarChart3,
|
||||
Flame,
|
||||
Landmark,
|
||||
Network,
|
||||
RefreshCw,
|
||||
Sparkles,
|
||||
Ticket,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
@@ -48,7 +52,7 @@ export function AgentDashboardConsole(): ReactElement {
|
||||
const tRef = useTranslationRef(["dashboard", "common"]);
|
||||
const profile = useAdminProfile();
|
||||
const agent = profile?.agent ?? null;
|
||||
const permissions = profile?.permissions ?? [];
|
||||
const permissions = useMemo(() => profile?.permissions ?? [], [profile?.permissions]);
|
||||
|
||||
const todayLabel = useMemo(() => {
|
||||
const locale = normalizeAdminLanguage(i18n.resolvedLanguage ?? i18n.language);
|
||||
@@ -109,6 +113,7 @@ export function AgentDashboardConsole(): ReactElement {
|
||||
}, []);
|
||||
|
||||
const currency = "NPR";
|
||||
const displayCurrency = overview?.currency_code ?? currency;
|
||||
|
||||
const quickLinks = useMemo(() => {
|
||||
const links: { href: string; label: string; icon: ReactElement }[] = [];
|
||||
@@ -189,78 +194,268 @@ export function AgentDashboardConsole(): ReactElement {
|
||||
))}
|
||||
</div>
|
||||
) : overview ? (
|
||||
<section className="grid min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("agent.creditTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1 text-sm">
|
||||
<p className="text-2xl font-semibold tabular-nums">
|
||||
{formatDashboardCreditMajor(overview.credit_limit, currency)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("agent.creditAvailable", {
|
||||
amount: formatDashboardCreditMajor(overview.available_credit, currency),
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("agent.creditAllocated", {
|
||||
amount: formatDashboardCreditMajor(overview.allocated_credit, currency),
|
||||
})}
|
||||
{" · "}
|
||||
{t("agent.creditUsed", {
|
||||
amount: formatDashboardCreditMajor(overview.used_credit, currency),
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("agent.shareRate", { rate: overview.total_share_rate })}
|
||||
{" · "}
|
||||
{t("agent.settlementCycle", { cycle: overview.settlement_cycle })}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<section className="space-y-4">
|
||||
<div className="grid min-w-0 grid-cols-1 gap-4 xl:grid-cols-[1.35fr_0.95fr]">
|
||||
<Card className="overflow-hidden border-slate-200 bg-[radial-gradient(circle_at_top_left,_rgba(16,185,129,0.18),_transparent_42%),linear-gradient(135deg,_#0f172a,_#111827_55%,_#1f2937)] text-white shadow-[0_20px_60px_-25px_rgba(15,23,42,0.65)]">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.24em] text-emerald-200/80">
|
||||
{t("agent.heroEyebrow")}
|
||||
</p>
|
||||
<CardTitle className="mt-2 text-2xl font-semibold">
|
||||
{t("agent.heroTitle", { name: overview.agent_name || overview.agent_code })}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/15 bg-white/10 p-2">
|
||||
<Sparkles className="size-4 text-emerald-200" />
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-2xl border border-white/10 bg-white/8 p-4 backdrop-blur">
|
||||
<p className="text-xs text-slate-300">{t("agent.todayBet")}</p>
|
||||
<p className="mt-2 text-2xl font-semibold tabular-nums">
|
||||
{formatDashboardMoneyMinor(overview.today_bet_minor, displayCurrency)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/10 bg-white/8 p-4 backdrop-blur">
|
||||
<p className="text-xs text-slate-300">{t("agent.todayPayout")}</p>
|
||||
<p className="mt-2 text-2xl font-semibold tabular-nums">
|
||||
{formatDashboardMoneyMinor(overview.today_payout_minor, displayCurrency)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/10 bg-white/8 p-4 backdrop-blur">
|
||||
<p className="text-xs text-slate-300">{t("agent.todayProfit")}</p>
|
||||
<p className="mt-2 text-2xl font-semibold tabular-nums">
|
||||
{formatDashboardMoneyMinor(overview.today_profit_minor, displayCurrency)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-xl border border-white/10 px-4 py-3">
|
||||
<p className="text-[11px] text-slate-300">{t("agent.activePlayersToday")}</p>
|
||||
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.active_player_count_today}</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/10 px-4 py-3">
|
||||
<p className="text-[11px] text-slate-300">{t("agent.betOrdersToday")}</p>
|
||||
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.bet_order_count_today}</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/10 px-4 py-3">
|
||||
<p className="text-[11px] text-slate-300">{t("agent.pendingBills")}</p>
|
||||
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.pending_bill_count}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-slate-300">
|
||||
<span>{t("agent.shareRate", { rate: overview.total_share_rate })}</span>
|
||||
<span>{t("agent.settlementCycle", { cycle: overview.settlement_cycle })}</span>
|
||||
<span>
|
||||
{overview.latest_bet_at
|
||||
? t("agent.latestBetAt", { time: new Date(overview.latest_bet_at).toLocaleString() })
|
||||
: t("agent.noBetToday")}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("agent.teamTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("agent.directChildren")}</p>
|
||||
<p className="text-xl font-semibold tabular-nums">{overview.direct_child_count}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("agent.directPlayers")}</p>
|
||||
<p className="text-xl font-semibold tabular-nums">{overview.direct_player_count}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<p className="text-xs text-muted-foreground">{t("agent.subtreeAgents")}</p>
|
||||
<p className="text-lg font-semibold tabular-nums">{overview.subtree_agent_count}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-slate-200 bg-[linear-gradient(180deg,_rgba(248,250,252,0.98),_rgba(241,245,249,0.92))]">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-semibold">{t("agent.creditTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<p className="text-3xl font-semibold tabular-nums text-slate-900">
|
||||
{formatDashboardCreditMajor(overview.credit_limit, displayCurrency)}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
{t("agent.creditAvailable", {
|
||||
amount: formatDashboardCreditMajor(overview.available_credit, displayCurrency),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="rounded-2xl border bg-white px-4 py-3">
|
||||
<p className="text-xs text-slate-500">{t("agent.creditAllocatedLabel")}</p>
|
||||
<p className="mt-1 text-lg font-semibold tabular-nums">
|
||||
{formatDashboardCreditMajor(overview.allocated_credit, displayCurrency)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border bg-white px-4 py-3">
|
||||
<p className="text-xs text-slate-500">{t("agent.creditUsedLabel")}</p>
|
||||
<p className="mt-1 text-lg font-semibold tabular-nums">
|
||||
{formatDashboardCreditMajor(overview.used_credit, displayCurrency)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
{t("agent.pendingUnpaid", {
|
||||
amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, displayCurrency),
|
||||
})}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="sm:col-span-2 xl:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("agent.pendingBills")}</CardTitle>
|
||||
{adminHasAnyPermission(permissions, [...PRD_SETTLEMENT_AGENT_ACCESS_ANY]) ? (
|
||||
<Link
|
||||
href="/admin/settlement-center"
|
||||
className={cn(buttonVariants({ variant: "link", size: "sm" }), "h-auto px-0 text-xs")}
|
||||
>
|
||||
{t("agent.viewBills")}
|
||||
</Link>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold tabular-nums">{overview.pending_bill_count}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{t("agent.pendingUnpaid", {
|
||||
amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, currency),
|
||||
})}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||
<TrendingUp className="size-4 text-emerald-600" />
|
||||
{t("agent.sevenDayTitle")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1 text-sm">
|
||||
<p className="text-xl font-semibold tabular-nums">
|
||||
{formatDashboardMoneyMinor(overview.seven_day_bet_minor, displayCurrency)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("agent.sevenDayPayout", {
|
||||
amount: formatDashboardMoneyMinor(overview.seven_day_payout_minor, displayCurrency),
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("agent.sevenDayProfit", {
|
||||
amount: formatDashboardMoneyMinor(overview.seven_day_profit_minor, displayCurrency),
|
||||
})}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||
<Users className="size-4 text-sky-600" />
|
||||
{t("agent.teamTitle")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("agent.directChildren")}</p>
|
||||
<p className="text-xl font-semibold tabular-nums">{overview.direct_child_count}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("agent.subtreeAgents")}</p>
|
||||
<p className="text-xl font-semibold tabular-nums">{overview.subtree_agent_count}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("agent.directPlayers")}</p>
|
||||
<p className="text-xl font-semibold tabular-nums">{overview.direct_player_count}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("agent.teamPlayers")}</p>
|
||||
<p className="text-xl font-semibold tabular-nums">{overview.team_player_count}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||
<Wallet className="size-4 text-amber-600" />
|
||||
{t("agent.pendingBills")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1">
|
||||
<p className="text-2xl font-semibold tabular-nums">{overview.pending_bill_count}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("agent.pendingUnpaid", {
|
||||
amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, displayCurrency),
|
||||
})}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||
<Flame className="size-4 text-rose-600" />
|
||||
{t("agent.topMomentum")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1">
|
||||
{overview.top_agent_today ? (
|
||||
<>
|
||||
<p className="text-sm font-semibold text-slate-900">
|
||||
{overview.top_agent_today.agent_name || overview.top_agent_today.agent_code}
|
||||
</p>
|
||||
<p className="text-xl font-semibold tabular-nums">
|
||||
{formatDashboardMoneyMinor(overview.top_agent_today.total_bet_minor, displayCurrency)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("agent.topMomentumHint", {
|
||||
profit: formatDashboardMoneyMinor(
|
||||
overview.top_agent_today.approx_house_gross_minor,
|
||||
displayCurrency,
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">{t("agent.noBetToday")}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
||||
<Landmark className="size-4 text-slate-700" />
|
||||
{t("agent.managementFocus")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-2xl border bg-slate-50 px-4 py-3">
|
||||
<p className="text-xs text-slate-500">{t("agent.focusBet")}</p>
|
||||
<p className="mt-1 text-lg font-semibold tabular-nums">
|
||||
{formatDashboardMoneyMinor(overview.today_bet_minor, displayCurrency)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border bg-slate-50 px-4 py-3">
|
||||
<p className="text-xs text-slate-500">{t("agent.focusPlayers")}</p>
|
||||
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.active_player_count_today}</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border bg-slate-50 px-4 py-3">
|
||||
<p className="text-xs text-slate-500">{t("agent.focusBills")}</p>
|
||||
<p className="mt-1 text-lg font-semibold tabular-nums">{overview.pending_bill_count}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("agent.quickStatsTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">{t("agent.canCreateChildAgent")}</span>
|
||||
<span className="font-medium">
|
||||
{overview.can_create_child_agent ? t("agent.yes") : t("agent.no")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">{t("agent.canCreatePlayer")}</span>
|
||||
<span className="font-medium">
|
||||
{overview.can_create_player ? t("agent.yes") : t("agent.no")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">{t("agent.lineDepth")}</span>
|
||||
<span className="font-medium tabular-nums">{overview.depth}</span>
|
||||
</div>
|
||||
{adminHasAnyPermission(permissions, [...PRD_SETTLEMENT_AGENT_ACCESS_ANY]) ? (
|
||||
<Link
|
||||
href="/admin/settlement-center"
|
||||
className={cn(buttonVariants({ variant: "link", size: "sm" }), "h-auto px-0")}
|
||||
>
|
||||
{t("agent.viewBills")}
|
||||
</Link>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user