From d6399cf79634395ca401336cd46e7b39a5ef75de Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Sun, 24 May 2026 20:30:19 -0700 Subject: [PATCH] feat: add withdrawal management pages --- apps/admin/src/layout/navs.ts | 5 + apps/admin/src/routeTree.gen.ts | 25 ++ .../src/routes/dashboard/withdrawal.lazy.tsx | 6 + apps/admin/src/sections/withdrawal/index.tsx | 328 +++++++++++++++++ .../src/sections/user/affiliate/index.tsx | 332 +++++++++++++++++- packages/ui/src/services/admin/index.ts | 2 + packages/ui/src/services/admin/typings.d.ts | 32 ++ packages/ui/src/services/admin/withdrawal.ts | 55 +++ packages/ui/src/services/user/typings.d.ts | 8 + packages/ui/src/services/user/user.ts | 18 + 10 files changed, 800 insertions(+), 11 deletions(-) create mode 100644 apps/admin/src/routes/dashboard/withdrawal.lazy.tsx create mode 100644 apps/admin/src/sections/withdrawal/index.tsx create mode 100644 packages/ui/src/services/admin/withdrawal.ts diff --git a/apps/admin/src/layout/navs.ts b/apps/admin/src/layout/navs.ts index 228d175..5c8bb28 100644 --- a/apps/admin/src/layout/navs.ts +++ b/apps/admin/src/layout/navs.ts @@ -93,6 +93,11 @@ export function useNavs() { url: "/dashboard/user", icon: "flat-color-icons:conference-call", }, + { + title: t("Withdrawal Management", "Withdrawal Management"), + url: "/dashboard/withdrawal", + icon: "flat-color-icons:money-transfer", + }, { title: t("Device Group", "Device Group"), url: "/dashboard/family", diff --git a/apps/admin/src/routeTree.gen.ts b/apps/admin/src/routeTree.gen.ts index eab2f6b..fb10ff4 100644 --- a/apps/admin/src/routeTree.gen.ts +++ b/apps/admin/src/routeTree.gen.ts @@ -15,6 +15,9 @@ import { Route as rootRouteImport } from './routes/__root' const DashboardRouteLazyRouteImport = createFileRoute('/dashboard')() const IndexLazyRouteImport = createFileRoute('/')() const DashboardIndexLazyRouteImport = createFileRoute('/dashboard/')() +const DashboardWithdrawalLazyRouteImport = createFileRoute( + '/dashboard/withdrawal', +)() const DashboardServersLazyRouteImport = createFileRoute('/dashboard/servers')() const DashboardNodesLazyRouteImport = createFileRoute('/dashboard/nodes')() const DashboardUserIndexLazyRouteImport = createFileRoute('/dashboard/user/')() @@ -109,6 +112,13 @@ const DashboardIndexLazyRoute = DashboardIndexLazyRouteImport.update({ } as any).lazy(() => import('./routes/dashboard/index.lazy').then((d) => d.Route), ) +const DashboardWithdrawalLazyRoute = DashboardWithdrawalLazyRouteImport.update({ + id: '/withdrawal', + path: '/withdrawal', + getParentRoute: () => DashboardRouteLazyRoute, +} as any).lazy(() => + import('./routes/dashboard/withdrawal.lazy').then((d) => d.Route), +) const DashboardServersLazyRoute = DashboardServersLazyRouteImport.update({ id: '/servers', path: '/servers', @@ -346,6 +356,7 @@ export interface FileRoutesByFullPath { '/dashboard': typeof DashboardRouteLazyRouteWithChildren '/dashboard/nodes': typeof DashboardNodesLazyRoute '/dashboard/servers': typeof DashboardServersLazyRoute + '/dashboard/withdrawal': typeof DashboardWithdrawalLazyRoute '/dashboard/': typeof DashboardIndexLazyRoute '/dashboard/log/balance': typeof DashboardLogBalanceLazyRoute '/dashboard/log/commission': typeof DashboardLogCommissionLazyRoute @@ -380,6 +391,7 @@ export interface FileRoutesByTo { '/': typeof IndexLazyRoute '/dashboard/nodes': typeof DashboardNodesLazyRoute '/dashboard/servers': typeof DashboardServersLazyRoute + '/dashboard/withdrawal': typeof DashboardWithdrawalLazyRoute '/dashboard': typeof DashboardIndexLazyRoute '/dashboard/log/balance': typeof DashboardLogBalanceLazyRoute '/dashboard/log/commission': typeof DashboardLogCommissionLazyRoute @@ -416,6 +428,7 @@ export interface FileRoutesById { '/dashboard': typeof DashboardRouteLazyRouteWithChildren '/dashboard/nodes': typeof DashboardNodesLazyRoute '/dashboard/servers': typeof DashboardServersLazyRoute + '/dashboard/withdrawal': typeof DashboardWithdrawalLazyRoute '/dashboard/': typeof DashboardIndexLazyRoute '/dashboard/log/balance': typeof DashboardLogBalanceLazyRoute '/dashboard/log/commission': typeof DashboardLogCommissionLazyRoute @@ -453,6 +466,7 @@ export interface FileRouteTypes { | '/dashboard' | '/dashboard/nodes' | '/dashboard/servers' + | '/dashboard/withdrawal' | '/dashboard/' | '/dashboard/log/balance' | '/dashboard/log/commission' @@ -487,6 +501,7 @@ export interface FileRouteTypes { | '/' | '/dashboard/nodes' | '/dashboard/servers' + | '/dashboard/withdrawal' | '/dashboard' | '/dashboard/log/balance' | '/dashboard/log/commission' @@ -522,6 +537,7 @@ export interface FileRouteTypes { | '/dashboard' | '/dashboard/nodes' | '/dashboard/servers' + | '/dashboard/withdrawal' | '/dashboard/' | '/dashboard/log/balance' | '/dashboard/log/commission' @@ -581,6 +597,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardIndexLazyRouteImport parentRoute: typeof DashboardRouteLazyRoute } + '/dashboard/withdrawal': { + id: '/dashboard/withdrawal' + path: '/withdrawal' + fullPath: '/dashboard/withdrawal' + preLoaderRoute: typeof DashboardWithdrawalLazyRouteImport + parentRoute: typeof DashboardRouteLazyRoute + } '/dashboard/servers': { id: '/dashboard/servers' path: '/servers' @@ -797,6 +820,7 @@ declare module '@tanstack/react-router' { interface DashboardRouteLazyRouteChildren { DashboardNodesLazyRoute: typeof DashboardNodesLazyRoute DashboardServersLazyRoute: typeof DashboardServersLazyRoute + DashboardWithdrawalLazyRoute: typeof DashboardWithdrawalLazyRoute DashboardIndexLazyRoute: typeof DashboardIndexLazyRoute DashboardLogBalanceLazyRoute: typeof DashboardLogBalanceLazyRoute DashboardLogCommissionLazyRoute: typeof DashboardLogCommissionLazyRoute @@ -831,6 +855,7 @@ interface DashboardRouteLazyRouteChildren { const DashboardRouteLazyRouteChildren: DashboardRouteLazyRouteChildren = { DashboardNodesLazyRoute: DashboardNodesLazyRoute, DashboardServersLazyRoute: DashboardServersLazyRoute, + DashboardWithdrawalLazyRoute: DashboardWithdrawalLazyRoute, DashboardIndexLazyRoute: DashboardIndexLazyRoute, DashboardLogBalanceLazyRoute: DashboardLogBalanceLazyRoute, DashboardLogCommissionLazyRoute: DashboardLogCommissionLazyRoute, diff --git a/apps/admin/src/routes/dashboard/withdrawal.lazy.tsx b/apps/admin/src/routes/dashboard/withdrawal.lazy.tsx new file mode 100644 index 0000000..de30e22 --- /dev/null +++ b/apps/admin/src/routes/dashboard/withdrawal.lazy.tsx @@ -0,0 +1,6 @@ +import { createLazyFileRoute } from "@tanstack/react-router"; +import WithdrawalManagementPage from "@/sections/withdrawal"; + +export const Route = createLazyFileRoute("/dashboard/withdrawal")({ + component: WithdrawalManagementPage, +}); diff --git a/apps/admin/src/sections/withdrawal/index.tsx b/apps/admin/src/sections/withdrawal/index.tsx new file mode 100644 index 0000000..f0548e4 --- /dev/null +++ b/apps/admin/src/sections/withdrawal/index.tsx @@ -0,0 +1,328 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Badge } from "@workspace/ui/components/badge"; +import { Button } from "@workspace/ui/components/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@workspace/ui/components/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@workspace/ui/components/form"; +import { Textarea } from "@workspace/ui/components/textarea"; +import type { ProTableActions } from "@workspace/ui/composed/pro-table/pro-table"; +import { ProTable } from "@workspace/ui/composed/pro-table/pro-table"; +import { + approveWithdrawal, + getWithdrawalList, + rejectWithdrawal, +} from "@workspace/ui/services/admin/withdrawal"; +import { LoaderCircle } from "lucide-react"; +import { useRef, useState, useTransition } from "react"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Display } from "@/components/display"; +import { UserDetail } from "@/sections/user/user-detail"; +import { formatDate } from "@/utils/common"; + +const rejectionSchema = z.object({ + reason: z + .string() + .trim() + .min(1, "Reason is required") + .max(500, "Reason must be 500 characters or fewer"), +}); + +const WITHDRAWAL_STATUS_OPTIONS = [ + { label: "All", value: "all" }, + { label: "Pending", value: "0" }, + { label: "Approved", value: "1" }, + { label: "Rejected", value: "2" }, +] as const; + +function getStatusMeta( + status: number, + t: ReturnType>["t"] +) { + switch (status) { + case 1: + return { + label: t("withdrawal.status.approved", "Approved"), + variant: "default" as const, + }; + case 2: + return { + label: t("withdrawal.status.rejected", "Rejected"), + variant: "destructive" as const, + }; + default: + return { + label: t("withdrawal.status.pending", "Pending"), + variant: "secondary" as const, + }; + } +} + +function RejectWithdrawalDialog({ + row, + onSuccess, +}: { + row: API.AdminWithdrawalLog; + onSuccess: () => Promise | void; +}) { + const { t } = useTranslation("user"); + const [open, setOpen] = useState(false); + const [loading, startTransition] = useTransition(); + const form = useForm>({ + resolver: zodResolver(rejectionSchema), + defaultValues: { + reason: "", + }, + }); + + const handleSubmit = (values: z.infer) => { + startTransition(async () => { + await rejectWithdrawal({ + withdrawal_id: row.id, + reason: values.reason.trim(), + }); + toast.success(t("withdrawal.rejectSuccess", "Withdrawal rejected")); + await onSuccess(); + setOpen(false); + form.reset(); + }); + }; + + return ( + { + setOpen(nextOpen); + if (!nextOpen) form.reset(); + }} + open={open} + > + + + + + + + {t("withdrawal.rejectTitle", "Reject Withdrawal")} + + + {t( + "withdrawal.rejectDescription", + "Provide a reason that will be shown in the user's withdrawal history." + )} + + +
+ + ( + + + {t("withdrawal.rejectReason", "Reject Reason")} + + +