瀏覽代碼

✨ feat(admin): 新增支付记录管理功能

- 在后台管理菜单中添加【支付记录】入口,使用钱包图标
- 创建完整的支付记录管理页面,支持列表、搜索、创建、编辑、删除功能
- 集成支付记录API路由,支持分页和关键字搜索
- 支持按状态(待支付/已完成/失败/已取消)和类型(充值/消费/退款)筛选
yourname 3 月之前
父節點
當前提交
1f6794de5f

+ 9 - 1
src/client/admin/menu.tsx

@@ -10,7 +10,8 @@ import {
   LayoutDashboard,
   File,
   FileText,
-  CreditCard
+  CreditCard,
+  Wallet
 } from 'lucide-react';
 
 export interface MenuItem {
@@ -110,6 +111,13 @@ export const useMenu = () => {
       path: '/admin/membership-plans',
       permission: 'membership:manage'
     },
+    {
+      key: 'payments',
+      label: '支付记录',
+      icon: <Wallet className="h-4 w-4" />,
+      path: '/admin/payments',
+      permission: 'payment:manage'
+    },
     {
       key: 'settings',
       label: '系统设置',

+ 705 - 0
src/client/admin/pages/Payments.tsx

@@ -0,0 +1,705 @@
+import { useState } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { format } from 'date-fns';
+import { zhCN } from 'date-fns/locale';
+import { toast } from 'sonner';
+import { z } from 'zod';
+
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from '@/client/components/ui/table';
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from '@/client/components/ui/card';
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Badge } from '@/client/components/ui/badge';
+import { Skeleton } from '@/client/components/ui/skeleton';
+import {
+  Form,
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from '@/client/components/ui/form';
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from '@/client/components/ui/dialog';
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@/client/components/ui/select';
+import { Search, Eye, Edit, Trash2, Plus } from 'lucide-react';
+
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import { paymentClient } from '@/client/api';
+import { CreatePaymentDto, UpdatePaymentDto } from '@/server/modules/payments/payment.schema';
+import { PaymentStatus, PaymentType } from '@/server/modules/payments/payment.schema';
+
+// 类型定义
+type PaymentResponse = InferResponseType<typeof paymentClient.$get, 200>['data'][0];
+type CreateRequest = InferRequestType<typeof paymentClient.$post>['json'];
+type UpdateRequest = InferRequestType<typeof paymentClient[':id']['$put']>['json'];
+
+// 表单schema
+const createFormSchema = CreatePaymentDto;
+const updateFormSchema = UpdatePaymentDto;
+
+export const PaymentsPage = () => {
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    search: '',
+    status: undefined as number | undefined,
+    paymentType: undefined as string | undefined,
+  });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  const [editingPayment, setEditingPayment] = useState<PaymentResponse | null>(null);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [paymentToDelete, setPaymentToDelete] = useState<number | null>(null);
+
+  // 创建表单
+  const createForm = useForm<CreateRequest>({
+    resolver: zodResolver(createFormSchema),
+    defaultValues: {
+      userId: 0,
+      amount: 0,
+      documentCount: 1,
+      paymentType: PaymentType.RECHARGE,
+      paymentMethod: 'alipay',
+      description: '',
+    },
+  });
+
+  // 更新表单
+  const updateForm = useForm<UpdateRequest>({
+    resolver: zodResolver(updateFormSchema),
+  });
+
+  // 获取支付记录列表
+  const { data, isLoading, refetch } = useQuery({
+    queryKey: ['payments', searchParams],
+    queryFn: async () => {
+      const res = await paymentClient.$get({
+        query: {
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.search,
+          status: searchParams.status,
+          paymentType: searchParams.paymentType,
+        },
+      });
+      if (res.status !== 200) throw new Error('获取支付记录失败');
+      return await res.json();
+    },
+  });
+
+  // 打开创建模态框
+  const handleCreatePayment = () => {
+    setIsCreateForm(true);
+    setEditingPayment(null);
+    createForm.reset();
+    setIsModalOpen(true);
+  };
+
+  // 打开编辑模态框
+  const handleEditPayment = (payment: PaymentResponse) => {
+    setIsCreateForm(false);
+    setEditingPayment(payment);
+    updateForm.reset({
+      amount: payment.amount,
+      documentCount: payment.documentCount,
+      paymentType: payment.paymentType,
+      status: payment.status,
+      paymentMethod: payment.paymentMethod || undefined,
+      transactionId: payment.transactionId || undefined,
+      description: payment.description || undefined,
+    });
+    setIsModalOpen(true);
+  };
+
+  // 打开删除确认
+  const handleDeletePayment = (id: number) => {
+    setPaymentToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  // 确认删除
+  const confirmDelete = async () => {
+    if (!paymentToDelete) return;
+
+    try {
+      const res = await paymentClient[':id']['$delete']({
+        param: { id: paymentToDelete.toString() },
+      });
+      
+      if (res.status === 204) {
+        toast.success('删除成功');
+        setDeleteDialogOpen(false);
+        refetch();
+      } else {
+        throw new Error('删除失败');
+      }
+    } catch (error) {
+      toast.error('删除失败,请重试');
+    }
+  };
+
+  // 创建支付记录
+  const handleCreateSubmit = async (data: CreateRequest) => {
+    try {
+      const res = await paymentClient.$post({ json: data });
+      if (res.status === 201) {
+        toast.success('创建成功');
+        setIsModalOpen(false);
+        refetch();
+      } else {
+        const error = await res.json();
+        toast.error(error.message || '创建失败');
+      }
+    } catch (error) {
+      toast.error('操作失败,请重试');
+    }
+  };
+
+  // 更新支付记录
+  const handleUpdateSubmit = async (data: UpdateRequest) => {
+    if (!editingPayment) return;
+
+    try {
+      const res = await paymentClient[':id']['$put']({
+        param: { id: editingPayment.id.toString() },
+        json: data,
+      });
+      
+      if (res.status === 200) {
+        toast.success('更新成功');
+        setIsModalOpen(false);
+        refetch();
+      } else {
+        const error = await res.json();
+        toast.error(error.message || '更新失败');
+      }
+    } catch (error) {
+      toast.error('操作失败,请重试');
+    }
+  };
+
+  // 状态映射
+  const getStatusBadge = (status: number) => {
+    const statusMap = {
+      [PaymentStatus.PENDING]: { label: '待支付', variant: 'secondary' },
+      [PaymentStatus.COMPLETED]: { label: '已完成', variant: 'default' },
+      [PaymentStatus.FAILED]: { label: '失败', variant: 'destructive' },
+      [PaymentStatus.CANCELLED]: { label: '已取消', variant: 'outline' },
+    };
+    const config = statusMap[status] || { label: '未知', variant: 'secondary' };
+    return <Badge variant={config.variant as any}>{config.label}</Badge>;
+  };
+
+  // 支付类型映射
+  const getPaymentTypeLabel = (type: string) => {
+    const typeMap = {
+      [PaymentType.RECHARGE]: '充值',
+      [PaymentType.CONSUME]: '消费',
+      [PaymentType.REFUND]: '退款',
+    };
+    return typeMap[type as PaymentType] || type;
+  };
+
+  // 搜索
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+  };
+
+  // 骨架屏
+  if (isLoading) {
+    return (
+      <div className="space-y-4">
+        <div className="flex justify-between items-center">
+          <Skeleton className="h-8 w-48" />
+          <Skeleton className="h-10 w-32" />
+        </div>
+        <Card>
+          <CardHeader>
+            <Skeleton className="h-6 w-1/4" />
+          </CardHeader>
+          <CardContent>
+            <div className="space-y-3">
+              {[...Array(5)].map((_, i) => (
+                <div key={i} className="flex gap-4">
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 w-20" />
+                </div>
+              ))}
+            </div>
+          </CardContent>
+        </Card>
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-4">
+      {/* 页面标题 */}
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">支付记录管理</h1>
+        <Button onClick={handleCreatePayment}>
+          <Plus className="mr-2 h-4 w-4" />
+          创建支付
+        </Button>
+      </div>
+
+      {/* 搜索区域 */}
+      <Card>
+        <CardHeader>
+          <CardTitle>支付记录列表</CardTitle>
+          <CardDescription>管理所有用户的支付记录</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <form onSubmit={handleSearch} className="mb-4">
+            <div className="flex gap-2">
+              <div className="relative flex-1 max-w-sm">
+                <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
+                <Input
+                  placeholder="搜索订单号、描述..."
+                  value={searchParams.search}
+                  onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
+                  className="pl-8"
+                />
+              </div>
+              <Select
+                value={searchParams.status?.toString() || ''}
+                onValueChange={(value) => setSearchParams(prev => ({ 
+                  ...prev, 
+                  status: value ? parseInt(value) : undefined,
+                  page: 1 
+                }))}
+              >
+                <SelectTrigger className="w-32">
+                  <SelectValue placeholder="状态" />
+                </SelectTrigger>
+                <SelectContent>
+                  <SelectItem value="">全部状态</SelectItem>
+                  <SelectItem value={PaymentStatus.PENDING.toString()}>待支付</SelectItem>
+                  <SelectItem value={PaymentStatus.COMPLETED.toString()}>已完成</SelectItem>
+                  <SelectItem value={PaymentStatus.FAILED.toString()}>失败</SelectItem>
+                  <SelectItem value={PaymentStatus.CANCELLED.toString()}>已取消</SelectItem>
+                </SelectContent>
+              </Select>
+              <Select
+                value={searchParams.paymentType || ''}
+                onValueChange={(value) => setSearchParams(prev => ({ 
+                  ...prev, 
+                  paymentType: value || undefined,
+                  page: 1 
+                }))}
+              >
+                <SelectTrigger className="w-32">
+                  <SelectValue placeholder="类型" />
+                </SelectTrigger>
+                <SelectContent>
+                  <SelectItem value="">全部类型</SelectItem>
+                  <SelectItem value={PaymentType.RECHARGE}>充值</SelectItem>
+                  <SelectItem value={PaymentType.CONSUME}>消费</SelectItem>
+                  <SelectItem value={PaymentType.REFUND}>退款</SelectItem>
+                </SelectContent>
+              </Select>
+              <Button type="submit" variant="outline">
+                搜索
+              </Button>
+            </div>
+          </form>
+
+          {/* 数据表格 */}
+          <div className="rounded-md border">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>订单号</TableHead>
+                  <TableHead>用户</TableHead>
+                  <TableHead>金额</TableHead>
+                  <TableHead>文档次数</TableHead>
+                  <TableHead>类型</TableHead>
+                  <TableHead>状态</TableHead>
+                  <TableHead>支付方式</TableHead>
+                  <TableHead>创建时间</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {data?.data.map((payment) => (
+                  <TableRow key={payment.id}>
+                    <TableCell className="font-medium">
+                      {payment.orderNo}
+                    </TableCell>
+                    <TableCell>
+                      {payment.user?.nickname || payment.user?.username || `用户${payment.userId}`}
+                    </TableCell>
+                    <TableCell>¥{payment.amount.toFixed(2)}</TableCell>
+                    <TableCell>{payment.documentCount}</TableCell>
+                    <TableCell>{getPaymentTypeLabel(payment.paymentType)}</TableCell>
+                    <TableCell>{getStatusBadge(payment.status)}</TableCell>
+                    <TableCell>{payment.paymentMethod || '-'}</TableCell>
+                    <TableCell>
+                      {format(new Date(payment.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
+                    </TableCell>
+                    <TableCell className="text-right">
+                      <div className="flex justify-end gap-2">
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleEditPayment(payment)}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleDeletePayment(payment.id)}
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </div>
+
+          {data?.data.length === 0 && (
+            <div className="text-center py-8">
+              <p className="text-muted-foreground">暂无支付记录</p>
+            </div>
+          )}
+
+          {/* 分页 */}
+          {data && data.data.length > 0 && (
+            <div className="flex items-center justify-between mt-4">
+              <p className="text-sm text-muted-foreground">
+                共 {data.pagination.total} 条记录
+              </p>
+              <div className="flex gap-2">
+                <Button
+                  variant="outline"
+                  size="sm"
+                  onClick={() => setSearchParams(prev => ({ ...prev, page: prev.page - 1 }))}
+                  disabled={searchParams.page <= 1}
+                >
+                  上一页
+                </Button>
+                <Button
+                  variant="outline"
+                  size="sm"
+                  onClick={() => setSearchParams(prev => ({ ...prev, page: prev.page + 1 }))}
+                  disabled={searchParams.page >= Math.ceil(data.pagination.total / searchParams.limit)}
+                >
+                  下一页
+                </Button>
+              </div>
+            </div>
+          )}
+        </CardContent>
+      </Card>
+
+      {/* 创建/编辑模态框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{isCreateForm ? '创建支付记录' : '编辑支付记录'}</DialogTitle>
+            <DialogDescription>
+              {isCreateForm ? '创建一个新的支付记录' : '编辑现有支付记录信息'}
+            </DialogDescription>
+          </DialogHeader>
+
+          {isCreateForm ? (
+            <Form {...createForm}>
+              <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
+                <FormField
+                  control={createForm.control}
+                  name="userId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>用户ID <span className="text-red-500">*</span></FormLabel>
+                      <FormControl>
+                        <Input 
+                          type="number" 
+                          placeholder="请输入用户ID" 
+                          {...field} 
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                <FormField
+                  control={createForm.control}
+                  name="amount"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>金额 <span className="text-red-500">*</span></FormLabel>
+                      <FormControl>
+                        <Input 
+                          type="number" 
+                          step="0.01"
+                          placeholder="请输入金额" 
+                          {...field} 
+                          onChange={(e) => field.onChange(parseFloat(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                <FormField
+                  control={createForm.control}
+                  name="documentCount"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>文档次数 <span className="text-red-500">*</span></FormLabel>
+                      <FormControl>
+                        <Input 
+                          type="number" 
+                          placeholder="请输入文档次数" 
+                          {...field} 
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                <FormField
+                  control={createForm.control}
+                  name="paymentType"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>支付类型 <span className="text-red-500">*</span></FormLabel>
+                      <Select onValueChange={field.onChange} defaultValue={field.value}>
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder="请选择支付类型" />
+                          </SelectTrigger>
+                        </FormControl>
+                        <SelectContent>
+                          <SelectItem value={PaymentType.RECHARGE}>充值</SelectItem>
+                          <SelectItem value={PaymentType.CONSUME}>消费</SelectItem>
+                          <SelectItem value={PaymentType.REFUND}>退款</SelectItem>
+                        </SelectContent>
+                      </Select>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                <FormField
+                  control={createForm.control}
+                  name="paymentMethod"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>支付方式</FormLabel>
+                      <Select onValueChange={field.onChange} defaultValue={field.value}>
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder="请选择支付方式" />
+                          </SelectTrigger>
+                        </FormControl>
+                        <SelectContent>
+                          <SelectItem value="alipay">支付宝</SelectItem>
+                          <SelectItem value="wechat">微信支付</SelectItem>
+                        </SelectContent>
+                      </Select>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                <FormField
+                  control={createForm.control}
+                  name="description"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>描述</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入支付描述" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit">创建</Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          ) : (
+            <Form {...updateForm}>
+              <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
+                <FormField
+                  control={updateForm.control}
+                  name="amount"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>金额</FormLabel>
+                      <FormControl>
+                        <Input 
+                          type="number" 
+                          step="0.01"
+                          placeholder="请输入金额" 
+                          {...field} 
+                          onChange={(e) => field.onChange(parseFloat(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                <FormField
+                  control={updateForm.control}
+                  name="documentCount"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>文档次数</FormLabel>
+                      <FormControl>
+                        <Input 
+                          type="number" 
+                          placeholder="请输入文档次数" 
+                          {...field} 
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                <FormField
+                  control={updateForm.control}
+                  name="status"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>状态</FormLabel>
+                      <Select onValueChange={(value) => field.onChange(parseInt(value))} defaultValue={field.value?.toString()}>
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder="请选择状态" />
+                          </SelectTrigger>
+                        </FormControl>
+                        <SelectContent>
+                          <SelectItem value={PaymentStatus.PENDING.toString()}>待支付</SelectItem>
+                          <SelectItem value={PaymentStatus.COMPLETED.toString()}>已完成</SelectItem>
+                          <SelectItem value={PaymentStatus.FAILED.toString()}>失败</SelectItem>
+                          <SelectItem value={PaymentStatus.CANCELLED.toString()}>已取消</SelectItem>
+                        </SelectContent>
+                      </Select>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                <FormField
+                  control={updateForm.control}
+                  name="paymentMethod"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>支付方式</FormLabel>
+                      <Select onValueChange={field.onChange} defaultValue={field.value}>
+                        <FormControl>
+                          <SelectTrigger>
+                            <SelectValue placeholder="请选择支付方式" />
+                          </SelectTrigger>
+                        </FormControl>
+                        <SelectContent>
+                          <SelectItem value="alipay">支付宝</SelectItem>
+                          <SelectItem value="wechat">微信支付</SelectItem>
+                        </SelectContent>
+                      </Select>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                <FormField
+                  control={updateForm.control}
+                  name="transactionId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>交易ID</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入第三方交易ID" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                <FormField
+                  control={updateForm.control}
+                  name="description"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>描述</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入支付描述" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit">更新</Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          )}
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>确认删除</DialogTitle>
+            <DialogDescription>
+              确定要删除这个支付记录吗?此操作无法撤销。
+            </DialogDescription>
+          </DialogHeader>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+              取消
+            </Button>
+            <Button variant="destructive" onClick={confirmDelete}>
+              删除
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+};

+ 6 - 0
src/client/admin/routes.tsx

@@ -9,6 +9,7 @@ import { UsersPage } from './pages/Users';
 import { LoginPage } from './pages/Login';
 import { FilesPage } from './pages/Files';
 import MembershipPlans from './pages/MembershipPlans';
+import { PaymentsPage } from './pages/Payments';
 
 export const router = createBrowserRouter([
   {
@@ -51,6 +52,11 @@ export const router = createBrowserRouter([
         element: <MembershipPlans />,
         errorElement: <ErrorPage />
       },
+      {
+        path: 'payments',
+        element: <PaymentsPage />,
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,

+ 5 - 1
src/client/api.ts

@@ -2,7 +2,7 @@
 import { hc } from 'hono/client'
 import type {
   AuthRoutes, UserRoutes, RoleRoutes,
-  FileRoutes, MembershipPlanRoutes
+  FileRoutes, MembershipPlanRoutes, PaymentRoutes
 } from '@/server/api';
 import { axiosFetch } from './utils/axios-fetch';
 
@@ -25,3 +25,7 @@ export const fileClient = hc<FileRoutes>('/', {
 export const membershipPlanClient = hc<MembershipPlanRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1['membership-plans'];
+
+export const paymentClient = hc<PaymentRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.payments;

+ 3 - 0
src/server/api.ts

@@ -6,6 +6,7 @@ import authRoute from './api/auth/index'
 import rolesRoute from './api/roles/index'
 import fileRoute from './api/files/index'
 import membershipPlanRoute from './api/membership-plans/index'
+import paymentRoute from './api/payments/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 import { Hono } from 'hono'
@@ -105,12 +106,14 @@ const authRoutes = api.route('/api/v1/auth', authRoute)
 const roleRoutes = api.route('/api/v1/roles', rolesRoute)
 const fileRoutes = api.route('/api/v1/files', fileRoute)
 const membershipPlanRoutes = api.route('/api/v1/membership-plans', membershipPlanRoute)
+const paymentRoutes = api.route('/api/v1/payments', paymentRoute)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
 export type RoleRoutes = typeof roleRoutes
 export type FileRoutes = typeof fileRoutes
 export type MembershipPlanRoutes = typeof membershipPlanRoutes
+export type PaymentRoutes = typeof paymentRoutes
 
 app.route('/', api)
 export default app

+ 17 - 0
src/server/api/payments/index.ts

@@ -0,0 +1,17 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { PaymentEntity } from '@/server/modules/payments/payment.entity';
+import { PaymentSchema, CreatePaymentDto, UpdatePaymentDto } from '@/server/modules/payments/payment.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const paymentRoutes = createCrudRoutes({
+  entity: PaymentEntity,
+  createSchema: CreatePaymentDto,
+  updateSchema: UpdatePaymentDto,
+  getSchema: PaymentSchema,
+  listSchema: PaymentSchema,
+  searchFields: ['orderNo', 'description', 'transactionId'],
+  relations: ['user'],
+  middleware: [authMiddleware]
+});
+
+export default paymentRoutes;

+ 1 - 13
src/server/modules/payments/payment.entity.ts

@@ -1,18 +1,6 @@
 import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
 import { UserEntity } from '@/server/modules/users/user.entity';
-
-export enum PaymentStatus {
-  PENDING = 0,
-  COMPLETED = 1,
-  FAILED = 2,
-  CANCELLED = 3
-}
-
-export enum PaymentType {
-  RECHARGE = 'recharge',
-  CONSUME = 'consume',
-  REFUND = 'refund'
-}
+import { PaymentStatus, PaymentType } from './payment.schema';
 
 @Entity('payments')
 export class PaymentEntity {