Browse Source

✨ feat(admin): add linkman select component and enhance order record entity

- ✨ feat(component): create LinkmanSelect component with search and filter capabilities
- 🔧 refactor(order): add client, linkman and user relationships to OrderRecord entity
- 🔧 refactor(order): update OrderRecord schema and DTOs to include new relational fields
- 🔧 refactor(page): update OrderRecords page import statement
yourname 8 months ago
parent
commit
adc6094487

+ 90 - 0
src/client/admin/components/LinkmanSelect.tsx

@@ -0,0 +1,90 @@
+import React from 'react';
+import { Select } from 'antd';
+import { useRequest } from 'ahooks';
+import { contactsClient } from '@/client/api';
+
+const { Option } = Select;
+
+interface LinkmanSelectProps {
+  value?: string;
+  onChange?: (value: string) => void;
+  clientId?: number;
+  placeholder?: string;
+  allowClear?: boolean;
+  disabled?: boolean;
+  mode?: 'multiple' | 'tags';
+  showSearch?: boolean;
+}
+
+interface LinkmanData {
+  id: string;
+  name: string;
+  mobile?: string;
+  position?: string;
+  clientId: number;
+}
+
+export const LinkmanSelect: React.FC<LinkmanSelectProps> = ({
+  value,
+  onChange,
+  clientId,
+  placeholder = '请选择联系人',
+  allowClear = true,
+  disabled,
+  mode,
+  showSearch = true,
+}) => {
+  // 获取联系人列表
+  const { data: linkmanData, loading } = useRequest(
+    async () => {
+      const response = await contactsClient.$get({
+        query: {
+          page: 1,
+          pageSize: 1000,
+          ...(clientId && { filters: JSON.stringify({ clientId }) }),
+        },
+      });
+      
+      if (response.status === 200) {
+        const result = await response.json();
+        return result.data as LinkmanData[];
+      }
+      return [];
+    },
+    {
+      refreshDeps: [clientId],
+    }
+  );
+
+  const handleChange = (newValue: string) => {
+    onChange?.(newValue);
+  };
+
+  const filterOption = (input: string, option?: any) => {
+    const text = option?.children || '';
+    return String(text).toLowerCase().indexOf(input.toLowerCase()) >= 0;
+  };
+
+  return (
+    <Select
+      value={value}
+      onChange={handleChange}
+      placeholder={placeholder}
+      allowClear={allowClear}
+      disabled={disabled || loading}
+      loading={loading}
+      mode={mode}
+      showSearch={showSearch}
+      filterOption={filterOption}
+      style={{ width: '100%' }}
+    >
+      {linkmanData?.map((linkman: LinkmanData) => (
+        <Option key={linkman.id} value={linkman.id}>
+          {linkman.name}
+          {linkman.mobile && ` (${linkman.mobile})`}
+          {linkman.position && ` - ${linkman.position}`}
+        </Option>
+      ))}
+    </Select>
+  );
+};

+ 1 - 1
src/client/admin/pages/OrderRecords.tsx

@@ -4,7 +4,7 @@ import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant
 import type { ColumnsType } from 'antd/es/table';
 import { InferRequestType, InferResponseType } from 'hono/client';
 import { orderRecordClient } from '@/client/api';
-import dayjs from 'dayjs';
+import { ClientSelect } from '@/client/admin/components/ClientSelect';
 
 const { RangePicker } = DatePicker;
 const { Option } = Select;

+ 59 - 4
src/server/modules/orders/order-record.entity.ts

@@ -1,5 +1,8 @@
-import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
 import { z } from '@hono/zod-openapi';
+import { Client } from '@/server/modules/clients/client.entity';
+import { Linkman } from '@/server/modules/contacts/linkman.entity';
+import { UserEntity } from '@/server/modules/users/user.entity';
 
 @Entity('order_record')
 export class OrderRecord {
@@ -33,6 +36,28 @@ export class OrderRecord {
   @Column({ name: 'salesperson', type: 'varchar', length: 50, comment: '业务员' })
   salesperson!: string;
 
+  // 外键关系
+  @Column({ name: 'client_id', type: 'int', unsigned: true, nullable: true, comment: '客户ID' })
+  clientId?: number;
+
+  @ManyToOne(() => Client, { nullable: true })
+  @JoinColumn({ name: 'client_id', referencedColumnName: 'id' })
+  client?: Client;
+
+  @Column({ name: 'linkman_id', type: 'varchar', length: 50, nullable: true, comment: '联系人ID' })
+  linkmanId?: string;
+
+  @ManyToOne(() => Linkman, { nullable: true })
+  @JoinColumn({ name: 'linkman_id', referencedColumnName: 'id' })
+  linkman?: Linkman;
+
+  @Column({ name: 'user_id', type: 'int', unsigned: true, nullable: true, comment: '业务员用户ID' })
+  userId?: number;
+
+  @ManyToOne(() => UserEntity, { nullable: true })
+  @JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
+  user?: UserEntity;
+
   @Column({ name: 'is_deleted', type: 'tinyint', default: 0, comment: '删除状态(0-未删除,1-已删除)' })
   isDeleted!: number;
 
@@ -55,9 +80,33 @@ export const OrderRecordSchema = z.object({
   orderAmount: z.coerce.number().multipleOf(0.01).openapi({ description: '订单金额', example: 5000.00 }),
   orderStatus: z.coerce.number().int().min(0).max(1).openapi({ description: '订单状态(0-未处理,1-已完成)', example: 0 }),
   salesperson: z.string().max(50).openapi({ description: '业务员', example: '李四' }),
+  clientId: z.number().int().positive().nullable().openapi({ description: '客户ID', example: 1 }),
+  linkmanId: z.string().max(50).nullable().openapi({ description: '联系人ID', example: 'LM001' }),
+  userId: z.number().int().positive().nullable().openapi({ description: '业务员用户ID', example: 1 }),
   isDeleted: z.coerce.number().int().min(0).max(1).default(0).openapi({ description: '删除状态', example: 0 }),
   createdAt: z.string().datetime().openapi({ description: '录入时间', example: '2024-07-15T12:00:00Z' }),
-  updatedAt: z.string().datetime().openapi({ description: '更新时间', example: '2024-07-15T12:00:00Z' })
+  updatedAt: z.string().datetime().openapi({ description: '更新时间', example: '2024-07-15T12:00:00Z' }),
+  client: z.object({
+    id: z.number(),
+    companyName: z.string(),
+    contactPerson: z.string().nullable()
+  }).nullable().optional().openapi({
+    description: '关联客户信息'
+  }),
+  linkman: z.object({
+    id: z.string(),
+    name: z.string(),
+    mobile: z.string().nullable()
+  }).nullable().optional().openapi({
+    description: '关联联系人信息'
+  }),
+  user: z.object({
+    id: z.number(),
+    username: z.string(),
+    name: z.string().nullable()
+  }).nullable().optional().openapi({
+    description: '关联业务员信息'
+  })
 });
 
 // 创建DTO
@@ -70,7 +119,10 @@ export const CreateOrderRecordDto = z.object({
   advancePayment: z.coerce.number().multipleOf(0.01).default(0).openapi({ description: '预付款', example: 1000.00 }),
   orderAmount: z.coerce.number().multipleOf(0.01).default(0).openapi({ description: '订单金额', example: 5000.00 }),
   orderStatus: z.coerce.number().int().min(0).max(1).default(0).openapi({ description: '订单状态', example: 0 }),
-  salesperson: z.string().max(50).openapi({ description: '业务员', example: '李四' })
+  salesperson: z.string().max(50).openapi({ description: '业务员', example: '李四' }),
+  clientId: z.coerce.number().int().positive().nullable().optional().openapi({ description: '客户ID', example: 1 }),
+  linkmanId: z.string().max(50).nullable().optional().openapi({ description: '联系人ID', example: 'LM001' }),
+  userId: z.coerce.number().int().positive().nullable().optional().openapi({ description: '业务员用户ID', example: 1 })
 });
 
 // 更新DTO
@@ -83,5 +135,8 @@ export const UpdateOrderRecordDto = z.object({
   advancePayment: z.coerce.number().multipleOf(0.01).optional().openapi({ description: '预付款', example: 1000.00 }),
   orderAmount: z.coerce.number().multipleOf(0.01).optional().openapi({ description: '订单金额', example: 5000.00 }),
   orderStatus: z.coerce.number().int().min(0).max(1).optional().openapi({ description: '订单状态', example: 0 }),
-  salesperson: z.string().max(50).optional().openapi({ description: '业务员', example: '李四' })
+  salesperson: z.string().max(50).optional().openapi({ description: '业务员', example: '李四' }),
+  clientId: z.coerce.number().int().positive().nullable().optional().openapi({ description: '客户ID', example: 1 }),
+  linkmanId: z.string().max(50).nullable().optional().openapi({ description: '联系人ID', example: 'LM001' }),
+  userId: z.coerce.number().int().positive().nullable().optional().openapi({ description: '业务员用户ID', example: 1 })
 });