Explorar o código

✨ feat(contracts): 实现合同审核功能并优化表格展示

- 添加合同状态枚举常量CONTRACT_STATUS
- 实现合同审核功能,包括API调用和状态更新
- 表格新增客户名称、起始时间、总金额、已收款、欠款等字段
- 优化表格布局,调整列宽和对齐方式
- 为合同状态添加颜色标识,有效合同显示绿色,待审显示橙色
- 新增审核时间和录入时间列
- 操作列改为固定在右侧,添加审核按钮
- 优化金额显示格式,使用千分位分隔符

✨ feat(contracts): 完善合同数据模型

- 实体类新增receivedAmount字段记录已收款金额
- 添加auditTime字段记录审核时间
- 设置status字段默认值为"待审"
- 更新相关数据验证Schema,明确状态可选值为"待审"和"合同有效"

💄 style(contracts): 优化合同表单样式

- 将状态输入框改为下拉选择框,限制可选值
- 调整表单布局和样式,提升用户体验
yourname hai 7 meses
pai
achega
9b422cf72f
Modificáronse 2 ficheiros con 157 adicións e 37 borrados
  1. 136 30
      src/client/admin/pages/Contracts.tsx
  2. 21 7
      src/server/modules/contracts/hetong.entity.ts

+ 136 - 30
src/client/admin/pages/Contracts.tsx

@@ -1,13 +1,19 @@
 import React, { useState } from 'react';
-import { Table, Button, Space, Input, Modal, Form, Select, DatePicker, InputNumber } from 'antd';
+import { Table, Button, Space, Input, Modal, Form, Select, DatePicker, InputNumber, Popconfirm } from 'antd';
 import ClientSelect from '@/client/admin/components/ClientSelect';
 import { App } from 'antd';
-import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, FileTextOutlined } from '@ant-design/icons';
+import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, FileTextOutlined, CheckCircleOutlined } from '@ant-design/icons';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { hetongClient } from '@/client/api';
 import type { InferResponseType } from 'hono/client';
 import dayjs from 'dayjs';
 
+// 合同状态枚举
+const CONTRACT_STATUS = {
+  PENDING: '待审',
+  ACTIVE: '合同有效'
+} as const;
+
 // 定义类型
 type HetongItem = InferResponseType<typeof hetongClient.$get, 200>['data'][0];
 type HetongListResponse = InferResponseType<typeof hetongClient.$get, 200>;
@@ -115,6 +121,28 @@ const Contracts: React.FC = () => {
       message.error('操作失败,请重试');
     }
   });
+
+  // 审核合同
+  const auditContract = useMutation({
+    mutationFn: async (id: number) => {
+      const response = await hetongClient[':id'].$put({
+        param: { id },
+        json: {
+          status: '合同有效',
+          auditTime: new Date().toISOString()
+        }
+      });
+      if (!response.ok) throw new Error('Failed to audit contract');
+      return response.json();
+    },
+    onSuccess: () => {
+      message.success('合同审核成功');
+      queryClient.invalidateQueries({ queryKey: ['contracts'] });
+    },
+    onError: () => {
+      message.error('审核失败,请重试');
+    }
+  });
   
   // 删除合同记录
   const deleteContract = useMutation({
@@ -156,59 +184,131 @@ const Contracts: React.FC = () => {
   // 表格列定义
   const columns = [
     {
-      title: '合同ID',
+      title: '编号',
       dataIndex: 'id',
       key: 'id',
+      width: 80,
+      align: 'center' as const,
     },
     {
-      title: '合同编号',
+      title: '客户名称',
+      dataIndex: ['client', 'companyName'],
+      key: 'clientName',
+      ellipsis: true,
+    },
+    {
+      title: '合同名称',
       dataIndex: 'contractNumber',
-      key: 'contractNumber',
+      key: 'contractName',
+      ellipsis: true,
     },
     {
-      title: '客户',
-      dataIndex: ['client','companyName'],
-      key: 'clientId',
+      title: '起始时间',
+      dataIndex: 'startDate',
+      key: 'startDate',
+      width: 120,
+      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD') : '-',
     },
     {
-      title: '合同类型',
-      dataIndex: 'type',
-      key: 'type',
+      title: '预计签约时间',
+      dataIndex: 'contractDate',
+      key: 'contractDate',
+      width: 120,
+      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD') : '-',
     },
     {
-      title: '合同金额',
+      title: '金额',
       dataIndex: 'amount',
       key: 'amount',
-      render: (amount: number, record: HetongItem) => 
-        `${record.currency || 'CNY'} ${Number(amount).toFixed(2)}`,
+      width: 120,
+      render: (amount: number, record: HetongItem) =>
+        `${record.currency || 'CNY'} ${Number(amount).toLocaleString()}`,
+      align: 'right' as const,
+    },
+    {
+      title: '已收款',
+      dataIndex: 'receivedAmount',
+      key: 'receivedAmount',
+      width: 100,
+      render: (received: number, record: HetongItem) =>
+        `${record.currency || 'CNY'} ${Number(received || 0).toLocaleString()}`,
+      align: 'right' as const,
+    },
+    {
+      title: '欠款',
+      key: 'debt',
+      width: 100,
+      render: (_: any, record: HetongItem) => {
+        const debt = Number(record.amount || 0) - Number(record.receivedAmount || 0);
+        return `${record.currency || 'CNY'} ${debt.toLocaleString()}`;
+      },
+      align: 'right' as const,
     },
     {
-      title: '状态',
+      title: '合同状态',
       dataIndex: 'status',
       key: 'status',
+      width: 100,
+      render: (status: string) => (
+        <span style={{
+          color: status === '合同有效' ? 'green' : 'orange',
+          fontWeight: 'bold'
+        }}>
+          {status}
+        </span>
+      ),
+    },
+    {
+      title: '审核时间',
+      dataIndex: 'auditTime',
+      key: 'auditTime',
+      width: 120,
+      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD') : '-',
     },
     {
-      title: '有效期',
-      key: 'dateRange',
-      render: (_: any, record: HetongItem) =>
-        `${record.startDate ? new Date(record.startDate).toLocaleDateString() : '-'} ~ ${record.endDate ? new Date(record.endDate).toLocaleDateString() : '-'}`
+      title: '录入时间',
+      dataIndex: 'createdAt',
+      key: 'createdAt',
+      width: 120,
+      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD') : '-',
     },
     {
-      title: '操作',
+      title: '管理',
       key: 'action',
+      width: 180,
+      fixed: 'right' as const,
       render: (_: any, record: HetongItem) => (
-        <Space size="middle">
-          <Button 
-            type="text" 
-            icon={<EditOutlined />} 
+        <Space size="small">
+          {record.status === '待审' && (
+            <Popconfirm
+              title="确认审核此合同?"
+              description={`将合同"${record.contractNumber}"状态设为"合同有效"`}
+              onConfirm={() => auditContract.mutate(record.id)}
+              okText="确认"
+              cancelText="取消"
+            >
+              <Button
+                type="primary"
+                size="small"
+                icon={<CheckCircleOutlined />}
+              >
+                审核
+              </Button>
+            </Popconfirm>
+          )}
+          <Button
+            type="text"
+            size="small"
+            icon={<EditOutlined />}
             onClick={() => showModal(record)}
           >
             编辑
           </Button>
-          <Button 
-            type="text" 
-            danger 
-            icon={<DeleteOutlined />} 
+          <Button
+            type="text"
+            danger
+            size="small"
+            icon={<DeleteOutlined />}
             onClick={() => deleteContract.mutate(record.id.toString())}
           >
             删除
@@ -362,9 +462,15 @@ const Contracts: React.FC = () => {
             <Form.Item
               name="status"
               label="合同状态"
-              rules={[{ required: true, message: '请输入合同状态' }]}
+              rules={[{ required: true, message: '请选择合同状态' }]}
             >
-              <Input placeholder="请输入合同状态:如生效中、已结束等" />
+              <Select
+                placeholder="请选择合同状态"
+                options={[
+                  { label: '待审', value: '待审' },
+                  { label: '合同有效', value: '合同有效' }
+                ]}
+              />
             </Form.Item>
           </div>
           

+ 21 - 7
src/server/modules/contracts/hetong.entity.ts

@@ -3,6 +3,14 @@ import { HetongRenew } from './hetong-renew.entity';
 import { Client } from '../clients/client.entity';
 import { z } from '@hono/zod-openapi';
 
+// 合同状态枚举
+export const CONTRACT_STATUS = {
+  PENDING: '待审',
+  ACTIVE: '合同有效'
+} as const;
+
+export type ContractStatusType = typeof CONTRACT_STATUS[keyof typeof CONTRACT_STATUS];
+
 @Entity('hetong')
 export class Hetong {
   @PrimaryGeneratedColumn({ name: 'id', type: 'int', unsigned: true })
@@ -36,9 +44,15 @@ export class Hetong {
   @Column({ name: 'type', type: 'varchar', length: 50 })
   type!: string;
 
-  @Column({ name: 'status', type: 'varchar', length: 50 })
+  @Column({ name: 'status', type: 'varchar', length: 50, default: '待审' })
   status!: string;
 
+  @Column({ name: 'received_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
+  receivedAmount!: number;
+
+  @Column({ name: 'audit_time', type: 'timestamp', nullable: true })
+  auditTime?: Date;
+
   @Column({ name: 'start_date', type: 'date' })
   startDate!: Date;
 
@@ -116,8 +130,8 @@ export const HetongSchema = z.object({
     example: '服务合同'
   }),
   status: z.string().max(50).openapi({
-    description: '合同状态:如生效中、已结束等',
-    example: '生效中'
+    description: '合同状态:待审/合同有效',
+    example: '待审'
   }),
   startDate: z.date().openapi({
     description: '开始日期',
@@ -183,8 +197,8 @@ export const CreateHetongDto = z.object({
     example: '服务合同'
   }),
   status: z.string().max(50).openapi({
-    description: '合同状态:如生效中、已结束等',
-    example: '生效中'
+    description: '合同状态:待审/合同有效',
+    example: '待审'
   }),
   startDate: z.coerce.date().openapi({
     description: '开始日期',
@@ -242,8 +256,8 @@ export const UpdateHetongDto = z.object({
     example: '服务合同'
   }),
   status: z.string().max(50).optional().openapi({
-    description: '合同状态:如生效中、已结束等',
-    example: '生效中'
+    description: '合同状态:待审/合同有效',
+    example: '待审'
   }),
   startDate: z.coerce.date().optional().openapi({
     description: '开始日期',