feat: add withdrawal management pages
This commit is contained in:
@@ -93,6 +93,11 @@ export function useNavs() {
|
|||||||
url: "/dashboard/user",
|
url: "/dashboard/user",
|
||||||
icon: "flat-color-icons:conference-call",
|
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"),
|
title: t("Device Group", "Device Group"),
|
||||||
url: "/dashboard/family",
|
url: "/dashboard/family",
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import { Route as rootRouteImport } from './routes/__root'
|
|||||||
const DashboardRouteLazyRouteImport = createFileRoute('/dashboard')()
|
const DashboardRouteLazyRouteImport = createFileRoute('/dashboard')()
|
||||||
const IndexLazyRouteImport = createFileRoute('/')()
|
const IndexLazyRouteImport = createFileRoute('/')()
|
||||||
const DashboardIndexLazyRouteImport = createFileRoute('/dashboard/')()
|
const DashboardIndexLazyRouteImport = createFileRoute('/dashboard/')()
|
||||||
|
const DashboardWithdrawalLazyRouteImport = createFileRoute(
|
||||||
|
'/dashboard/withdrawal',
|
||||||
|
)()
|
||||||
const DashboardServersLazyRouteImport = createFileRoute('/dashboard/servers')()
|
const DashboardServersLazyRouteImport = createFileRoute('/dashboard/servers')()
|
||||||
const DashboardNodesLazyRouteImport = createFileRoute('/dashboard/nodes')()
|
const DashboardNodesLazyRouteImport = createFileRoute('/dashboard/nodes')()
|
||||||
const DashboardUserIndexLazyRouteImport = createFileRoute('/dashboard/user/')()
|
const DashboardUserIndexLazyRouteImport = createFileRoute('/dashboard/user/')()
|
||||||
@@ -109,6 +112,13 @@ const DashboardIndexLazyRoute = DashboardIndexLazyRouteImport.update({
|
|||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
import('./routes/dashboard/index.lazy').then((d) => d.Route),
|
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({
|
const DashboardServersLazyRoute = DashboardServersLazyRouteImport.update({
|
||||||
id: '/servers',
|
id: '/servers',
|
||||||
path: '/servers',
|
path: '/servers',
|
||||||
@@ -346,6 +356,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/dashboard': typeof DashboardRouteLazyRouteWithChildren
|
'/dashboard': typeof DashboardRouteLazyRouteWithChildren
|
||||||
'/dashboard/nodes': typeof DashboardNodesLazyRoute
|
'/dashboard/nodes': typeof DashboardNodesLazyRoute
|
||||||
'/dashboard/servers': typeof DashboardServersLazyRoute
|
'/dashboard/servers': typeof DashboardServersLazyRoute
|
||||||
|
'/dashboard/withdrawal': typeof DashboardWithdrawalLazyRoute
|
||||||
'/dashboard/': typeof DashboardIndexLazyRoute
|
'/dashboard/': typeof DashboardIndexLazyRoute
|
||||||
'/dashboard/log/balance': typeof DashboardLogBalanceLazyRoute
|
'/dashboard/log/balance': typeof DashboardLogBalanceLazyRoute
|
||||||
'/dashboard/log/commission': typeof DashboardLogCommissionLazyRoute
|
'/dashboard/log/commission': typeof DashboardLogCommissionLazyRoute
|
||||||
@@ -380,6 +391,7 @@ export interface FileRoutesByTo {
|
|||||||
'/': typeof IndexLazyRoute
|
'/': typeof IndexLazyRoute
|
||||||
'/dashboard/nodes': typeof DashboardNodesLazyRoute
|
'/dashboard/nodes': typeof DashboardNodesLazyRoute
|
||||||
'/dashboard/servers': typeof DashboardServersLazyRoute
|
'/dashboard/servers': typeof DashboardServersLazyRoute
|
||||||
|
'/dashboard/withdrawal': typeof DashboardWithdrawalLazyRoute
|
||||||
'/dashboard': typeof DashboardIndexLazyRoute
|
'/dashboard': typeof DashboardIndexLazyRoute
|
||||||
'/dashboard/log/balance': typeof DashboardLogBalanceLazyRoute
|
'/dashboard/log/balance': typeof DashboardLogBalanceLazyRoute
|
||||||
'/dashboard/log/commission': typeof DashboardLogCommissionLazyRoute
|
'/dashboard/log/commission': typeof DashboardLogCommissionLazyRoute
|
||||||
@@ -416,6 +428,7 @@ export interface FileRoutesById {
|
|||||||
'/dashboard': typeof DashboardRouteLazyRouteWithChildren
|
'/dashboard': typeof DashboardRouteLazyRouteWithChildren
|
||||||
'/dashboard/nodes': typeof DashboardNodesLazyRoute
|
'/dashboard/nodes': typeof DashboardNodesLazyRoute
|
||||||
'/dashboard/servers': typeof DashboardServersLazyRoute
|
'/dashboard/servers': typeof DashboardServersLazyRoute
|
||||||
|
'/dashboard/withdrawal': typeof DashboardWithdrawalLazyRoute
|
||||||
'/dashboard/': typeof DashboardIndexLazyRoute
|
'/dashboard/': typeof DashboardIndexLazyRoute
|
||||||
'/dashboard/log/balance': typeof DashboardLogBalanceLazyRoute
|
'/dashboard/log/balance': typeof DashboardLogBalanceLazyRoute
|
||||||
'/dashboard/log/commission': typeof DashboardLogCommissionLazyRoute
|
'/dashboard/log/commission': typeof DashboardLogCommissionLazyRoute
|
||||||
@@ -453,6 +466,7 @@ export interface FileRouteTypes {
|
|||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/dashboard/nodes'
|
| '/dashboard/nodes'
|
||||||
| '/dashboard/servers'
|
| '/dashboard/servers'
|
||||||
|
| '/dashboard/withdrawal'
|
||||||
| '/dashboard/'
|
| '/dashboard/'
|
||||||
| '/dashboard/log/balance'
|
| '/dashboard/log/balance'
|
||||||
| '/dashboard/log/commission'
|
| '/dashboard/log/commission'
|
||||||
@@ -487,6 +501,7 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/dashboard/nodes'
|
| '/dashboard/nodes'
|
||||||
| '/dashboard/servers'
|
| '/dashboard/servers'
|
||||||
|
| '/dashboard/withdrawal'
|
||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/dashboard/log/balance'
|
| '/dashboard/log/balance'
|
||||||
| '/dashboard/log/commission'
|
| '/dashboard/log/commission'
|
||||||
@@ -522,6 +537,7 @@ export interface FileRouteTypes {
|
|||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/dashboard/nodes'
|
| '/dashboard/nodes'
|
||||||
| '/dashboard/servers'
|
| '/dashboard/servers'
|
||||||
|
| '/dashboard/withdrawal'
|
||||||
| '/dashboard/'
|
| '/dashboard/'
|
||||||
| '/dashboard/log/balance'
|
| '/dashboard/log/balance'
|
||||||
| '/dashboard/log/commission'
|
| '/dashboard/log/commission'
|
||||||
@@ -581,6 +597,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof DashboardIndexLazyRouteImport
|
preLoaderRoute: typeof DashboardIndexLazyRouteImport
|
||||||
parentRoute: typeof DashboardRouteLazyRoute
|
parentRoute: typeof DashboardRouteLazyRoute
|
||||||
}
|
}
|
||||||
|
'/dashboard/withdrawal': {
|
||||||
|
id: '/dashboard/withdrawal'
|
||||||
|
path: '/withdrawal'
|
||||||
|
fullPath: '/dashboard/withdrawal'
|
||||||
|
preLoaderRoute: typeof DashboardWithdrawalLazyRouteImport
|
||||||
|
parentRoute: typeof DashboardRouteLazyRoute
|
||||||
|
}
|
||||||
'/dashboard/servers': {
|
'/dashboard/servers': {
|
||||||
id: '/dashboard/servers'
|
id: '/dashboard/servers'
|
||||||
path: '/servers'
|
path: '/servers'
|
||||||
@@ -797,6 +820,7 @@ declare module '@tanstack/react-router' {
|
|||||||
interface DashboardRouteLazyRouteChildren {
|
interface DashboardRouteLazyRouteChildren {
|
||||||
DashboardNodesLazyRoute: typeof DashboardNodesLazyRoute
|
DashboardNodesLazyRoute: typeof DashboardNodesLazyRoute
|
||||||
DashboardServersLazyRoute: typeof DashboardServersLazyRoute
|
DashboardServersLazyRoute: typeof DashboardServersLazyRoute
|
||||||
|
DashboardWithdrawalLazyRoute: typeof DashboardWithdrawalLazyRoute
|
||||||
DashboardIndexLazyRoute: typeof DashboardIndexLazyRoute
|
DashboardIndexLazyRoute: typeof DashboardIndexLazyRoute
|
||||||
DashboardLogBalanceLazyRoute: typeof DashboardLogBalanceLazyRoute
|
DashboardLogBalanceLazyRoute: typeof DashboardLogBalanceLazyRoute
|
||||||
DashboardLogCommissionLazyRoute: typeof DashboardLogCommissionLazyRoute
|
DashboardLogCommissionLazyRoute: typeof DashboardLogCommissionLazyRoute
|
||||||
@@ -831,6 +855,7 @@ interface DashboardRouteLazyRouteChildren {
|
|||||||
const DashboardRouteLazyRouteChildren: DashboardRouteLazyRouteChildren = {
|
const DashboardRouteLazyRouteChildren: DashboardRouteLazyRouteChildren = {
|
||||||
DashboardNodesLazyRoute: DashboardNodesLazyRoute,
|
DashboardNodesLazyRoute: DashboardNodesLazyRoute,
|
||||||
DashboardServersLazyRoute: DashboardServersLazyRoute,
|
DashboardServersLazyRoute: DashboardServersLazyRoute,
|
||||||
|
DashboardWithdrawalLazyRoute: DashboardWithdrawalLazyRoute,
|
||||||
DashboardIndexLazyRoute: DashboardIndexLazyRoute,
|
DashboardIndexLazyRoute: DashboardIndexLazyRoute,
|
||||||
DashboardLogBalanceLazyRoute: DashboardLogBalanceLazyRoute,
|
DashboardLogBalanceLazyRoute: DashboardLogBalanceLazyRoute,
|
||||||
DashboardLogCommissionLazyRoute: DashboardLogCommissionLazyRoute,
|
DashboardLogCommissionLazyRoute: DashboardLogCommissionLazyRoute,
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import WithdrawalManagementPage from "@/sections/withdrawal";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/dashboard/withdrawal")({
|
||||||
|
component: WithdrawalManagementPage,
|
||||||
|
});
|
||||||
@@ -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<typeof useTranslation<"user">>["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> | void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation("user");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [loading, startTransition] = useTransition();
|
||||||
|
const form = useForm<z.infer<typeof rejectionSchema>>({
|
||||||
|
resolver: zodResolver(rejectionSchema),
|
||||||
|
defaultValues: {
|
||||||
|
reason: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (values: z.infer<typeof rejectionSchema>) => {
|
||||||
|
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 (
|
||||||
|
<Dialog
|
||||||
|
onOpenChange={(nextOpen) => {
|
||||||
|
setOpen(nextOpen);
|
||||||
|
if (!nextOpen) form.reset();
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button size="sm" variant="destructive">
|
||||||
|
{t("withdrawal.reject", "Reject")}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t("withdrawal.rejectTitle", "Reject Withdrawal")}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t(
|
||||||
|
"withdrawal.rejectDescription",
|
||||||
|
"Provide a reason that will be shown in the user's withdrawal history."
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="space-y-4"
|
||||||
|
id={`reject-withdrawal-${row.id}`}
|
||||||
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="reason"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("withdrawal.rejectReason", "Reject Reason")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder={t(
|
||||||
|
"withdrawal.rejectReasonPlaceholder",
|
||||||
|
"Explain why this withdrawal cannot be approved"
|
||||||
|
)}
|
||||||
|
rows={4}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{t("cancel", "Cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={loading}
|
||||||
|
form={`reject-withdrawal-${row.id}`}
|
||||||
|
type="submit"
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
{loading && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
{t("withdrawal.rejectConfirm", "Reject Withdrawal")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WithdrawalManagementPage() {
|
||||||
|
const { t } = useTranslation("user");
|
||||||
|
const [approvingId, setApprovingId] = useState<number | null>(null);
|
||||||
|
const tableActionRef = useRef<ProTableActions | undefined>(undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProTable<API.AdminWithdrawalLog, { user_id?: string; status?: string }>
|
||||||
|
action={tableActionRef}
|
||||||
|
actions={{
|
||||||
|
render: (row) => {
|
||||||
|
if (Number(row.status) !== 0) return [];
|
||||||
|
|
||||||
|
const handleApprove = async () => {
|
||||||
|
try {
|
||||||
|
await approveWithdrawal({
|
||||||
|
withdrawal_id: Number(row.id),
|
||||||
|
});
|
||||||
|
toast.success(
|
||||||
|
t("withdrawal.approveSuccess", "Withdrawal approved")
|
||||||
|
);
|
||||||
|
await tableActionRef.current?.refresh();
|
||||||
|
} finally {
|
||||||
|
setApprovingId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
<Button
|
||||||
|
disabled={approvingId === Number(row.id)}
|
||||||
|
key={`approve-${row.id}`}
|
||||||
|
onClick={() => {
|
||||||
|
setApprovingId(Number(row.id));
|
||||||
|
handleApprove();
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{approvingId === Number(row.id) && (
|
||||||
|
<LoaderCircle className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
)}
|
||||||
|
{t("withdrawal.approve", "Approve")}
|
||||||
|
</Button>,
|
||||||
|
<RejectWithdrawalDialog
|
||||||
|
key={`reject-${row.id}`}
|
||||||
|
onSuccess={() => tableActionRef.current?.refresh()}
|
||||||
|
row={row}
|
||||||
|
/>,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: t("withdrawal.column.id", "ID"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "user_id",
|
||||||
|
header: t("withdrawal.column.user", "User"),
|
||||||
|
cell: ({ row }) => <UserDetail id={Number(row.original.user_id)} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "amount",
|
||||||
|
header: t("withdrawal.column.amount", "Amount"),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Display type="currency" value={Number(row.original.amount)} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "content",
|
||||||
|
header: t("withdrawal.column.content", "Withdrawal Info"),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="max-w-[280px] whitespace-pre-wrap break-words text-sm">
|
||||||
|
{row.original.content}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "status",
|
||||||
|
header: t("withdrawal.column.status", "Status"),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const meta = getStatusMeta(Number(row.original.status), t);
|
||||||
|
return <Badge variant={meta.variant}>{meta.label}</Badge>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "reason",
|
||||||
|
header: t("withdrawal.column.reason", "Reject Reason"),
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.original.reason ? (
|
||||||
|
<div className="max-w-[220px] whitespace-pre-wrap break-words text-muted-foreground text-sm">
|
||||||
|
{row.original.reason}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground text-sm">-</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "created_at",
|
||||||
|
header: t("withdrawal.column.createdAt", "Created At"),
|
||||||
|
cell: ({ row }) => formatDate(Number(row.original.created_at)),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
header={{
|
||||||
|
title: t("withdrawal.title", "Withdrawal Management"),
|
||||||
|
}}
|
||||||
|
params={[
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
type: "select",
|
||||||
|
placeholder: t("withdrawal.filter.status", "Status"),
|
||||||
|
options: WITHDRAWAL_STATUS_OPTIONS.map((option) => ({
|
||||||
|
label: t(`withdrawal.statusOption.${option.value}`, option.label),
|
||||||
|
value: option.value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "user_id",
|
||||||
|
placeholder: t("withdrawal.filter.userId", "User ID"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
request={async (pagination, filter) => {
|
||||||
|
const status =
|
||||||
|
filter.status && filter.status !== "all"
|
||||||
|
? Number(filter.status)
|
||||||
|
: undefined;
|
||||||
|
const userId = filter.user_id ? Number(filter.user_id) : undefined;
|
||||||
|
|
||||||
|
const { data } = await getWithdrawalList({
|
||||||
|
page: pagination.page,
|
||||||
|
size: pagination.size,
|
||||||
|
status,
|
||||||
|
user_id: Number.isFinite(userId) ? userId : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = (data.data?.list || []) as API.AdminWithdrawalLog[];
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
total: Number(data.data?.total || list.length),
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Button } from "@workspace/ui/components/button";
|
import { Button } from "@workspace/ui/components/button";
|
||||||
import {
|
import {
|
||||||
@@ -9,29 +10,146 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@workspace/ui/components/card";
|
} from "@workspace/ui/components/card";
|
||||||
|
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 { Input } from "@workspace/ui/components/input";
|
||||||
|
import { Textarea } from "@workspace/ui/components/textarea";
|
||||||
|
import type { ProListActions } from "@workspace/ui/composed/pro-list/pro-list";
|
||||||
import { ProList } from "@workspace/ui/composed/pro-list/pro-list";
|
import { ProList } from "@workspace/ui/composed/pro-list/pro-list";
|
||||||
import {
|
import {
|
||||||
|
commissionWithdraw,
|
||||||
queryUserAffiliate,
|
queryUserAffiliate,
|
||||||
queryUserAffiliateList,
|
queryUserAffiliateList,
|
||||||
|
queryWithdrawalLog,
|
||||||
} from "@workspace/ui/services/user/user";
|
} from "@workspace/ui/services/user/user";
|
||||||
import { formatDate } from "@workspace/ui/utils/formatting";
|
import { formatDate } from "@workspace/ui/utils/formatting";
|
||||||
|
import { unitConversion } from "@workspace/ui/utils/unit-conversions";
|
||||||
import { Copy } from "lucide-react";
|
import { Copy } from "lucide-react";
|
||||||
|
import { useRef, useState, useTransition } from "react";
|
||||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
import { Display } from "@/components/display";
|
import { Display } from "@/components/display";
|
||||||
import { useGlobalStore } from "@/stores/global";
|
import { useGlobalStore } from "@/stores/global";
|
||||||
|
|
||||||
|
function getWithdrawalStatusMeta(
|
||||||
|
status: number,
|
||||||
|
t: ReturnType<typeof useTranslation<"affiliate">>["t"]
|
||||||
|
) {
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
return {
|
||||||
|
label: t("withdrawal.status.approved", "Approved"),
|
||||||
|
className: "bg-emerald-500/10 text-emerald-600",
|
||||||
|
};
|
||||||
|
case 2:
|
||||||
|
return {
|
||||||
|
label: t("withdrawal.status.rejected", "Rejected"),
|
||||||
|
className: "bg-destructive/10 text-destructive",
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
label: t("withdrawal.status.pending", "Pending"),
|
||||||
|
className: "bg-amber-500/10 text-amber-600",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function Affiliate() {
|
export default function Affiliate() {
|
||||||
const { t } = useTranslation("affiliate");
|
const { t } = useTranslation("affiliate");
|
||||||
const { user, common } = useGlobalStore();
|
const { user, common, getUserInfo } = useGlobalStore();
|
||||||
const { data } = useQuery({
|
const [withdrawalOpen, setWithdrawalOpen] = useState(false);
|
||||||
|
const [submitting, startTransition] = useTransition();
|
||||||
|
const withdrawalListActionRef = useRef<ProListActions | undefined>(undefined);
|
||||||
|
|
||||||
|
const affiliateQuery = useQuery({
|
||||||
queryKey: ["queryUserAffiliate"],
|
queryKey: ["queryUserAffiliate"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await queryUserAffiliate();
|
const response = await queryUserAffiliate();
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const availableCommission = Number(
|
||||||
|
user?.commission ?? affiliateQuery.data?.total_commission ?? 0
|
||||||
|
);
|
||||||
|
const withdrawalFormSchema = z.object({
|
||||||
|
amount: z
|
||||||
|
.number()
|
||||||
|
.int(t("withdrawal.validation.amountInvalid", "Amount is invalid"))
|
||||||
|
.positive(
|
||||||
|
t(
|
||||||
|
"withdrawal.validation.amountPositive",
|
||||||
|
"Amount must be greater than 0"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.max(
|
||||||
|
availableCommission,
|
||||||
|
t(
|
||||||
|
"withdrawal.validation.amountExceeded",
|
||||||
|
"Amount cannot exceed available commission"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
content: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(
|
||||||
|
1,
|
||||||
|
t(
|
||||||
|
"withdrawal.validation.contentRequired",
|
||||||
|
"Withdrawal info is required"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.max(
|
||||||
|
1000,
|
||||||
|
t("withdrawal.validation.contentTooLong", "Withdrawal info is too long")
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const withdrawalForm = useForm<z.infer<typeof withdrawalFormSchema>>({
|
||||||
|
resolver: zodResolver(withdrawalFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
amount: 0,
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitWithdrawal = (values: z.infer<typeof withdrawalFormSchema>) => {
|
||||||
|
startTransition(async () => {
|
||||||
|
await commissionWithdraw({
|
||||||
|
amount: values.amount,
|
||||||
|
content: values.content.trim(),
|
||||||
|
});
|
||||||
|
toast.success(
|
||||||
|
t("withdrawal.submitSuccess", "Withdrawal request submitted")
|
||||||
|
);
|
||||||
|
await Promise.all([
|
||||||
|
affiliateQuery.refetch(),
|
||||||
|
getUserInfo(),
|
||||||
|
withdrawalListActionRef.current?.refresh(),
|
||||||
|
]);
|
||||||
|
setWithdrawalOpen(false);
|
||||||
|
withdrawalForm.reset({
|
||||||
|
amount: 0,
|
||||||
|
content: "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@@ -43,15 +161,146 @@ export default function Affiliate() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||||
<span className="font-bold text-3xl">
|
<div className="flex items-baseline gap-2">
|
||||||
<Display type="currency" value={data?.total_commission} />
|
<span className="font-bold text-3xl">
|
||||||
</span>
|
<Display
|
||||||
<span className="text-muted-foreground text-sm">
|
type="currency"
|
||||||
({t("commissionRate", "Commission Rate")}:{" "}
|
value={affiliateQuery.data?.total_commission}
|
||||||
{user?.referral_percentage || common?.invite?.referral_percentage}
|
/>
|
||||||
%)
|
</span>
|
||||||
</span>
|
<span className="text-muted-foreground text-sm">
|
||||||
|
({t("commissionRate", "Commission Rate")}:{" "}
|
||||||
|
{user?.referral_percentage ||
|
||||||
|
common?.invite?.referral_percentage}
|
||||||
|
%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-start gap-2 md:items-end">
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
{t(
|
||||||
|
"withdrawal.availableCommission",
|
||||||
|
"Available for withdrawal"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="font-semibold text-xl">
|
||||||
|
<Display type="currency" value={availableCommission} />
|
||||||
|
</div>
|
||||||
|
<Dialog onOpenChange={setWithdrawalOpen} open={withdrawalOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>
|
||||||
|
{t("withdrawal.apply", "Apply for Withdrawal")}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t("withdrawal.applyTitle", "Apply for Withdrawal")}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t(
|
||||||
|
"withdrawal.applyDescription",
|
||||||
|
"Enter the withdrawal amount and payout details."
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...withdrawalForm}>
|
||||||
|
<form
|
||||||
|
className="space-y-4"
|
||||||
|
id="withdrawal-form"
|
||||||
|
onSubmit={withdrawalForm.handleSubmit(submitWithdrawal)}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={withdrawalForm.control}
|
||||||
|
name="amount"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("withdrawal.amount", "Amount")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
inputMode="decimal"
|
||||||
|
min="0"
|
||||||
|
onChange={(event) => {
|
||||||
|
const cents = unitConversion(
|
||||||
|
"dollarsToCents",
|
||||||
|
Number(event.target.value || 0)
|
||||||
|
);
|
||||||
|
field.onChange(Number(cents) || 0);
|
||||||
|
}}
|
||||||
|
placeholder={t(
|
||||||
|
"withdrawal.amountPlaceholder",
|
||||||
|
"Enter withdrawal amount"
|
||||||
|
)}
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
value={
|
||||||
|
field.value
|
||||||
|
? unitConversion(
|
||||||
|
"centsToDollars",
|
||||||
|
Number(field.value)
|
||||||
|
)
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{t("withdrawal.amountHint", "Available")}:{" "}
|
||||||
|
<Display
|
||||||
|
type="currency"
|
||||||
|
value={availableCommission}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={withdrawalForm.control}
|
||||||
|
name="content"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("withdrawal.content", "Payout Information")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder={t(
|
||||||
|
"withdrawal.contentPlaceholder",
|
||||||
|
"Bank account, wallet address, recipient name, or other payout details"
|
||||||
|
)}
|
||||||
|
rows={4}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
disabled={submitting}
|
||||||
|
onClick={() => setWithdrawalOpen(false)}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{t("withdrawal.cancel", "Cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={submitting || availableCommission <= 0}
|
||||||
|
form="withdrawal-form"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{submitting
|
||||||
|
? t("withdrawal.submitting", "Submitting...")
|
||||||
|
: t("withdrawal.submit", "Submit Withdrawal")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -82,6 +331,67 @@ export default function Affiliate() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<ProList<API.WithdrawalLog, Record<string, unknown>>
|
||||||
|
action={withdrawalListActionRef}
|
||||||
|
header={{
|
||||||
|
title: t("withdrawal.records", "Withdrawal Records"),
|
||||||
|
}}
|
||||||
|
renderItem={(item) => {
|
||||||
|
const statusMeta = getWithdrawalStatusMeta(item.status, t);
|
||||||
|
return (
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
<CardContent className="space-y-3 p-4 text-sm">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="font-semibold text-lg">
|
||||||
|
<Display type="currency" value={item.amount} />
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`inline-flex rounded-full px-3 py-1 font-medium text-xs ${statusMeta.className}`}
|
||||||
|
>
|
||||||
|
{statusMeta.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul className="grid gap-3 md:grid-cols-2">
|
||||||
|
<li className="flex flex-col gap-1">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("withdrawal.record.content", "Payout Information")}
|
||||||
|
</span>
|
||||||
|
<span className="whitespace-pre-wrap break-words">
|
||||||
|
{item.content}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex flex-col gap-1">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("withdrawal.record.createdAt", "Created At")}
|
||||||
|
</span>
|
||||||
|
<time>{formatDate(item.created_at)}</time>
|
||||||
|
</li>
|
||||||
|
{item.reason ? (
|
||||||
|
<li className="flex flex-col gap-1 md:col-span-2">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("withdrawal.record.reason", "Reject Reason")}
|
||||||
|
</span>
|
||||||
|
<span className="whitespace-pre-wrap break-words text-destructive">
|
||||||
|
{item.reason}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
request={async (pagination) => {
|
||||||
|
const response = await queryWithdrawalLog({
|
||||||
|
page: pagination.page,
|
||||||
|
size: pagination.size,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
list: response.data.data?.list || [],
|
||||||
|
total: response.data.data?.total || 0,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<ProList<API.UserAffiliate, Record<string, unknown>>
|
<ProList<API.UserAffiliate, Record<string, unknown>>
|
||||||
header={{
|
header={{
|
||||||
title: t("inviteRecords", "Invite Records"),
|
title: t("inviteRecords", "Invite Records"),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import * as system from "./system";
|
|||||||
import * as ticket from "./ticket";
|
import * as ticket from "./ticket";
|
||||||
import * as tool from "./tool";
|
import * as tool from "./tool";
|
||||||
import * as user from "./user";
|
import * as user from "./user";
|
||||||
|
import * as withdrawal from "./withdrawal";
|
||||||
export default {
|
export default {
|
||||||
ads,
|
ads,
|
||||||
announcement,
|
announcement,
|
||||||
@@ -43,4 +44,5 @@ export default {
|
|||||||
ticket,
|
ticket,
|
||||||
tool,
|
tool,
|
||||||
user,
|
user,
|
||||||
|
withdrawal,
|
||||||
};
|
};
|
||||||
|
|||||||
+32
@@ -28,6 +28,21 @@ declare namespace API {
|
|||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AdminWithdrawalLog = {
|
||||||
|
id: number;
|
||||||
|
user_id: number;
|
||||||
|
amount: number;
|
||||||
|
content: string;
|
||||||
|
status: number;
|
||||||
|
reason?: string;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApproveWithdrawalRequest = {
|
||||||
|
withdrawal_id: number;
|
||||||
|
};
|
||||||
|
|
||||||
type AnyTLS = {
|
type AnyTLS = {
|
||||||
port: number;
|
port: number;
|
||||||
security_config: SecurityConfig;
|
security_config: SecurityConfig;
|
||||||
@@ -71,6 +86,18 @@ declare namespace API {
|
|||||||
is_default: boolean;
|
is_default: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GetWithdrawalListParams = {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
user_id?: number;
|
||||||
|
status?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GetWithdrawalListResponse = {
|
||||||
|
list: AdminWithdrawalLog[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
type AppUserSubcbribe = {
|
type AppUserSubcbribe = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -3047,6 +3074,11 @@ declare namespace API {
|
|||||||
confirm: boolean;
|
confirm: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RejectWithdrawalRequest = {
|
||||||
|
withdrawal_id: number;
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
|
||||||
type PreviewUserNodesRequest = {
|
type PreviewUserNodesRequest = {
|
||||||
user_id: number;
|
user_id: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
import request from "@workspace/ui/lib/request";
|
||||||
|
|
||||||
|
/** Get withdrawal list GET /v1/admin/user/withdrawal/list */
|
||||||
|
export async function getWithdrawalList(
|
||||||
|
params: API.GetWithdrawalListParams,
|
||||||
|
options?: { [key: string]: any }
|
||||||
|
) {
|
||||||
|
return request<API.Response & { data?: API.GetWithdrawalListResponse }>(
|
||||||
|
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/user/withdrawal/list`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
...(options || {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Approve withdrawal POST /v1/admin/user/withdrawal/approve */
|
||||||
|
export async function approveWithdrawal(
|
||||||
|
body: API.ApproveWithdrawalRequest,
|
||||||
|
options?: { [key: string]: any }
|
||||||
|
) {
|
||||||
|
return request<API.Response>(
|
||||||
|
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/user/withdrawal/approve`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reject withdrawal POST /v1/admin/user/withdrawal/reject */
|
||||||
|
export async function rejectWithdrawal(
|
||||||
|
body: API.RejectWithdrawalRequest,
|
||||||
|
options?: { [key: string]: any }
|
||||||
|
) {
|
||||||
|
return request<API.Response>(
|
||||||
|
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/user/withdrawal/reject`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
+8
@@ -846,6 +846,14 @@ declare namespace API {
|
|||||||
order_no: string;
|
order_no: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RedeemCodeRequest = {
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RedeemCodeResponse = {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
type RegisterConfig = {
|
type RegisterConfig = {
|
||||||
stop_register: boolean;
|
stop_register: boolean;
|
||||||
enable_trial: boolean;
|
enable_trial: boolean;
|
||||||
|
|||||||
@@ -251,6 +251,24 @@ export async function updateUserPassword(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Redeem Code POST /v1/public/redemption/ */
|
||||||
|
export async function redeemCode(
|
||||||
|
body: API.RedeemCodeRequest,
|
||||||
|
options?: { [key: string]: any }
|
||||||
|
) {
|
||||||
|
return request<API.Response & { data?: API.RedeemCodeResponse }>(
|
||||||
|
`${import.meta.env.VITE_API_PREFIX || ""}/v1/public/redemption/`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Update User Rules PUT /v1/public/user/rules */
|
/** Update User Rules PUT /v1/public/user/rules */
|
||||||
export async function updateUserRules(
|
export async function updateUserRules(
|
||||||
body: API.UpdateUserRulesRequest,
|
body: API.UpdateUserRulesRequest,
|
||||||
|
|||||||
Reference in New Issue
Block a user