diff --git a/apps/server/src/models/staff/staff.module.ts b/apps/server/src/models/staff/staff.module.ts index fa681dc..835c5ee 100755 --- a/apps/server/src/models/staff/staff.module.ts +++ b/apps/server/src/models/staff/staff.module.ts @@ -1,15 +1,16 @@ import { Module } from '@nestjs/common'; import { StaffService } from './staff.service'; -import { StaffRouter } from './staff.router'; -import { TrpcService } from '@server/trpc/trpc.service'; -import { DepartmentModule } from '../department/department.module'; import { StaffController } from './staff.controller'; -import { StaffRowService } from './staff.row.service'; +import { DepartmentModule } from '../department/department.module'; +import { TrpcModule } from '../../trpc/trpc.module'; +import { StaffRouter } from './staff.router'; +import { StaffRowService } from './staff.row.service'; +import { TrpcService } from '@server/trpc/trpc.service'; @Module({ imports: [DepartmentModule], - providers: [StaffService, StaffRouter, TrpcService, StaffRowService], - exports: [StaffService, StaffRouter, StaffRowService], - controllers: [StaffController], + providers: [StaffService, StaffRouter, StaffRowService, TrpcService], + exports: [StaffService, StaffRouter], + controllers: [StaffController, ], }) -export class StaffModule { } +export class StaffModule {} diff --git a/apps/server/src/models/staff/staff.router.ts b/apps/server/src/models/staff/staff.router.ts index d93821e..609474a 100755 --- a/apps/server/src/models/staff/staff.router.ts +++ b/apps/server/src/models/staff/staff.router.ts @@ -95,5 +95,50 @@ export class StaffRouter { .query(async ({ input }) => { return await this.staffService.findUnique(input); }), + addCustomField: this.trpc.procedure + .input(z.object({ + name: z.string(), + label: z.string().optional(), + type: z.string(), // text, number, date, select 等 + required: z.boolean().optional(), + order: z.number().optional(), + options: z.any().optional(), // 对于选择类型字段的可选值 + group: z.string().optional(), // 字段分组 + })) + .mutation(({ input }) => { + return this.staffService.addCustomField(input as any); + }), + updateCustomField: this.trpc.procedure + .input(z.object({ + id: z.string(), + name: z.string().optional(), + label: z.string().optional(), + type: z.string().optional(), + required: z.boolean().optional(), + order: z.number().optional(), + options: z.any().optional(), + group: z.string().optional(), + })) + .mutation(({ input }) => { + return this.staffService.updateCustomField(input as any); + }), + deleteCustomField: this.trpc.procedure + .input(z.string()) + .mutation(({ input }) => { + return this.staffService.deleteCustomField(input as any); + }), + getCustomFields: this.trpc.procedure + .query(() => { + return this.staffService.getCustomFields(); + }), + setCustomFieldValue: this.trpc.procedure + .input(z.object({ + staffId: z.string(), + fieldId: z.string(), + value: z.string().optional(), + })) + .mutation(({ input }) => { + return this.staffService.setCustomFieldValue(input as any); + }), }); } diff --git a/apps/server/src/models/staff/staff.service.ts b/apps/server/src/models/staff/staff.service.ts index 57cddd2..c8d75d1 100755 --- a/apps/server/src/models/staff/staff.service.ts +++ b/apps/server/src/models/staff/staff.service.ts @@ -17,6 +17,7 @@ export class StaffService extends BaseService { constructor(private readonly departmentService: DepartmentService) { super(db, ObjectType.STAFF, true); } + /** * 获取某一单位下所有staff的记录 * @param deptId 单位的id @@ -36,30 +37,78 @@ export class StaffService extends BaseService { }); return result; } + async create(args: Prisma.StaffCreateArgs) { - const { data } = args; - await this.validateUniqueFields(data); - const createData = { - ...data, - password: await argon2.hash((data.password || '123456') as string), - }; - const result = await super.create({ ...args, data: createData }); - this.emitDataChangedEvent(result, CrudOperation.CREATED); - return result; + const { data, select } = args; + const { fieldValues, ...staffData } = data as any; + + // 创建员工基本信息 + const staff = await super.create({ + ...args, + data: { + ...staffData, + password: await argon2.hash((staffData.password || '123456') as string), + }, + }); + + // 如果有自定义字段值,创建它们 + if (fieldValues) { + await db.staffFieldValue.createMany({ + data: Object.entries(fieldValues).map(([fieldId, value]) => ({ + staffId: staff.id, + fieldId, + value: String(value), + })), + }); + } + + this.emitDataChangedEvent(staff, CrudOperation.CREATED); + return staff; } + async update(args: Prisma.StaffUpdateArgs) { const { data, where } = args; - await this.validateUniqueFields(data, where.id); - const updateData = { - ...data, - ...(data.password && { - password: await argon2.hash(data.password as string), - }), - }; - const result = await super.update({ ...args, data: updateData }); - this.emitDataChangedEvent(result, CrudOperation.UPDATED); - return result; + const { fieldValues, ...staffData } = data as any; + + // 更新员工基本信息 + const staff = await super.update({ + ...args, + data: { + ...staffData, + ...(staffData.password && { + password: await argon2.hash(staffData.password as string), + }), + }, + }); + + // 如果有自定义字段值,更新它们 + if (fieldValues) { + await Promise.all( + Object.entries(fieldValues).map(([fieldId, value]) => + db.staffFieldValue.upsert({ + where: { + staffId_fieldId: { + staffId: staff.id, + fieldId, + }, + }, + create: { + staffId: staff.id, + fieldId, + value: String(value), + }, + update: { + value: String(value), + }, + }), + ), + ); + } + + this.emitDataChangedEvent(staff, CrudOperation.UPDATED); + return staff; } + private async validateUniqueFields(data: any, excludeId?: string) { const uniqueFields = [ { @@ -119,72 +168,91 @@ export class StaffService extends BaseService { } } - // /** - // * 根据关键词或ID集合查找员工 - // * @param data 包含关键词、域ID和ID集合的对象 - // * @returns 匹配的员工记录列表 - // */ - // async findMany(data: z.infer) { - // const { keyword, domainId, ids, deptId, limit = 30 } = data; - // const idResults = ids - // ? await db.staff.findMany({ - // where: { - // id: { in: ids }, - // deletedAt: null, - // domainId, - // deptId, - // }, - // select: { - // id: true, - // showname: true, - // username: true, - // deptId: true, - // domainId: true, - // department: true, - // domain: true, - // }, - // }) - // : []; + async findUnique(args: Prisma.StaffFindUniqueArgs) { + const staff = await super.findUnique(args); + if (!staff) return null; - // const mainResults = await db.staff.findMany({ - // where: { - // deletedAt: null, - // domainId, - // deptId, - // OR: (keyword || ids) && [ - // { showname: { contains: keyword } }, - // { - // username: { - // contains: keyword, - // }, - // }, - // { phoneNumber: { contains: keyword } }, - // // { - // // id: { in: ids }, - // // }, - // ], - // }, - // select: { - // id: true, - // showname: true, - // username: true, - // deptId: true, - // domainId: true, - // department: true, - // domain: true, - // }, - // orderBy: { order: 'asc' }, - // take: limit !== -1 ? limit : undefined, - // }); - // // Combine results, ensuring no duplicates - // const combinedResults = [ - // ...mainResults, - // ...idResults.filter( - // (idResult) => - // !mainResults.some((mainResult) => mainResult.id === idResult.id), - // ), - // ]; + // 获取自定义字段值 + const fieldValues = await db.staffFieldValue.findMany({ + where: { staffId: staff.id }, + include: { field: true }, + }); - // return combinedResults; - // } + return { + ...staff, + fieldValues: fieldValues.reduce((acc, { field, value }) => ({ + ...acc, + [field.name]: value, + }), {}), + }; + } + + async addCustomField(data: { + name: string; + label?: string; + type: string; + required?: boolean; + order?: number; + options?: any; + group?: string; + }) { + return this.prisma.staffField.create({ + data: { + ...data, + }, + }); + } + + async updateCustomField(data: { + id: string; + name?: string; + label?: string; + type?: string; + required?: boolean; + order?: number; + options?: any; + group?: string; + }) { + const { id, ...updateData } = data; + return this.prisma.staffField.update({ + where: { id }, + data: updateData, + }); + } + + async deleteCustomField(id: string) { + return this.prisma.staffField.delete({ + where: { id }, + }); + } + + async getCustomFields() { + return this.prisma.staffField.findMany({ + orderBy: { order: 'asc' }, + }); + } + + async setCustomFieldValue(data: { + staffId: string; + fieldId: string; + value?: string; + }) { + const { staffId, fieldId, value } = data; + return this.prisma.staffFieldValue.upsert({ + where: { + staffId_fieldId: { + staffId, + fieldId, + } + }, + create: { + staffId, + fieldId, + value, + }, + update: { + value, + }, + }); + } } diff --git a/apps/server/src/trpc/trpc.router.ts b/apps/server/src/trpc/trpc.router.ts index dec98f9..959c61c 100755 --- a/apps/server/src/trpc/trpc.router.ts +++ b/apps/server/src/trpc/trpc.router.ts @@ -37,7 +37,7 @@ export class TrpcRouter { private readonly resource: ResourceRouter, private readonly trainContent: TrainContentRouter, private readonly trainSituation:TrainSituationRouter, - private readonly dailyTrain:DailyTrainRouter + private readonly dailyTrain:DailyTrainRouter, ) {} getRouter() { return; diff --git a/apps/web/src/app/admin/staffinfo-manage/defaultFieldInitializer.tsx b/apps/web/src/app/admin/staffinfo-manage/defaultFieldInitializer.tsx new file mode 100644 index 0000000..4a5745a --- /dev/null +++ b/apps/web/src/app/admin/staffinfo-manage/defaultFieldInitializer.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; +import { message, Modal, Button } from 'antd'; +import { useStaff } from '@nice/client'; +import { defaultFields } from './defaultFields'; + +type DefaultFieldInitializerProps = { + fields: any[] | undefined; + isProcessing: boolean; + setIsProcessing: (isProcessing: boolean) => void; + isInitConfirmVisible: boolean; + setIsInitConfirmVisible: (isVisible: boolean) => void; +}; + +const checkFieldExists = (fields: any[] | undefined, name: string) => { + return fields?.some(field => field.name === name); +}; + +const DefaultFieldInitializer: React.FC = ({ + fields, + isProcessing, + setIsProcessing, + isInitConfirmVisible, + setIsInitConfirmVisible +}) => { + const { addCustomField } = useStaff(); + + const initializeDefaultFields = async () => { + try { + setIsProcessing(true); + + // 检查是否已有数据 + if (fields && fields.length > 0) { + message.warning('数据库中已存在字段数据,请先清空数据后再初始化'); + return; + } + // 使用 Promise.all 批量处理 + await Promise.all( + defaultFields.map(async (field) => { + try { + if (!checkFieldExists(fields, field.name)) { + await addCustomField.mutateAsync(field); + } + } catch (error) { + console.error(`添加字段 ${field.name} 失败:`, error); + } + }) + ); + + message.success('默认字段初始化成功'); + setIsInitConfirmVisible(false); + } catch (error) { + message.error('初始化失败:' + (error instanceof Error ? error.message : '未知错误')); + } finally { + setIsProcessing(false); + } + }; + + return ( + <> + + setIsInitConfirmVisible(false)} + confirmLoading={isProcessing} + > +

初始化将导入所有预设字段,确定要继续吗?

+
+ + ); +}; + +export default DefaultFieldInitializer; \ No newline at end of file diff --git a/apps/web/src/app/admin/staffinfo-manage/defaultFields.ts b/apps/web/src/app/admin/staffinfo-manage/defaultFields.ts new file mode 100644 index 0000000..db931e8 --- /dev/null +++ b/apps/web/src/app/admin/staffinfo-manage/defaultFields.ts @@ -0,0 +1,174 @@ +export const defaultFields = [ + // 基本信息组 + { name: 'username', label: '用户名', type: 'text',required: true, group: '基本信息', order: 1 }, + { name: 'showname', label: '显示名称', type: 'text', group: '基本信息', order: 2 }, + { name: 'idNumber', label: '身份证号', type: 'text', group: '基本信息', order: 3 }, + { name: 'officerId', label: '警号', type: 'text', group: '基本信息', order: 4 }, + { name: 'phoneNumber', label: '手机号', type: 'text', group: '基本信息', order: 5 }, + { name: 'age', label: '年龄', type: 'number', group: '基本信息', order: 6 }, + { name: 'sex', + label: '性别', + type: 'radio', + options: [ + { label: '男', value: 'male' }, + { label: '女', value: 'female' }, + ], + group: '基本信息', + order: 7 }, + { name: 'bloodType', + label: '血型', + type: 'select', + options: [ + { label: 'A', value: 'A' }, + { label: 'B', value: 'B' }, + { label: 'AB', value: 'AB' }, + { label: 'O', value: 'O' }, + ], + group: '基本信息', + order: 8 }, + { name: 'birthplace', label: '籍贯', type: 'text', group: '基本信息', order: 9 }, + { name: 'source', label: '来源', type: 'text', group: '基本信息', order: 10 }, + + // 政治信息组 + { name: 'politicalStatus', + label: '政治面貌', + type: 'select', + options: [ + { label: '中共党员', value: '中共党员' }, + { label: '中共预备党员', value: '中共预备党员' }, + { label: '共青团员', value: '共青团员' }, + { label: '群众', value: '群众' }, + ], + group: '政治信息', + order: 11 }, + { name: 'partyPosition', label: '党内职务', type: 'text', group: '政治信息', order: 12 }, + + // 职务信息组 + { name: 'rank', + label: '衔职级别', + type: 'select', + options: [ + { label: '', value: '' }, + { label: '', value: '' }, + ], + group: '职务信息', + order: 13 }, + { name: 'rankDate', label: '衔职时间', type: 'date', group: '职务信息', order: 14 }, + { name: 'proxyPosition', label: '代理职务', type: 'text', group: '职务信息', order: 15 }, + { name: 'post', label: '岗位', type: 'text', group: '职务信息', order: 16 }, + + // 入职信息组 + { name: 'hireDate', label: '入职时间', type: 'date', group: '入职信息', order: 17 }, + { name: 'seniority', label: '工龄认定时间', type: 'date', group: '入职信息', order: 18 }, + { name: 'sourceType', label: '来源类型', type: 'text', group: '入职信息', order: 19 }, + { name: 'isReentry', + label: '是否二次入职', + type: 'radio', + options: [ + { label: '是', value: '是' }, + { label: '否', value: '否' }, + ], + group: '入职信息', + order: 20 }, + { name: 'isExtended', + label: '是否延期服役', + type: 'radio', + options: [ + { label: '是', value: '是' }, + { label: '否', value: '否' }, + ], + group: '入职信息', + order: 21 }, + { name: 'currentPositionDate', label: '现岗位开始时间', type: 'date', group: '入职信息', order: 22 }, + + // 教育信息组 + { name: 'education', + label: '学历', + type: 'select', + options: [ + { label: '博士', value: '博士' }, + { label: '硕士', value: '硕士' }, + { label: '本科', value: '本科' }, + { label: '专科', value: '专科' }, + { label: '中专', value: '中专' }, + { label: '高中', value: '高中' }, + { label: '初中', value: '初中' }, + { label: '小学', value: '小学' }, + ], + group: '教育信息', + order: 23 }, + { name: 'educationType', + label: '学历形式', + type: 'select', + options: [ + { label: '全日制', value: '全日制' }, + { label: '非全日制', value: '非全日制' }, + ], + group: '教育信息', + order: 24 }, + { name: 'isGraduated', + label: '是否毕业', + type: 'radio', + options: [ + { label: '是', value: '是' }, + { label: '否', value: '否' }, + ], + group: '教育信息', + order: 25 }, + { name: 'major', label: '专业', type: 'text', group: '教育信息', order: 26 }, + { name: 'foreignLang', label: '外语能力', type: 'text', group: '教育信息', order: 27 }, + + // 培训信息组 + { name: 'trainType', label: '培训类型', type: 'text', group: '培训信息', order: 28 }, + { name: 'trainInstitute', label: '培训机构', type: 'text', group: '培训信息', order: 29 }, + { name: 'trainMajor', label: '培训专业', type: 'text', group: '培训信息', order: 30 }, + { name: 'hasTrain', + label: '是否参加培训', + type: 'radio', + options: [ + { label: '是', value: '是' }, + { label: '否', value: '否' }, + ], + group: '培训信息', + order: 31 }, + + // 鉴定信息组 + { name: 'certRank', label: '鉴定等级', type: 'text', group: '鉴定信息', order: 32 }, + { name: 'certWork', label: '鉴定工种', type: 'text', group: '鉴定信息', order: 33 }, + { name: 'hasCert', + label: '是否参加鉴定', + type: 'radio', + options: [ + { label: '是', value: '是' }, + { label: '否', value: '否' }, + ], + group: '鉴定信息', + order: 34 }, + + // 工作信息组 + { name: 'equipment', label: '操作维护装备', type: 'textarea', group: '工作信息', order: 35 }, + { name: 'projects', label: '演训任务经历', type: 'textarea', group: '工作信息', order: 36 }, + { name: 'awards', label: '奖励信息', type: 'textarea', group: '工作信息', order: 37 }, + { name: 'punishments', label: '处分信息', type: 'textarea', group: '工作信息', order: 38 }, + ]; + + export const FieldTypeOptions = [ + { label: '文本', value: 'text' }, + { label: '数字', value: 'number' }, + { label: '日期', value: 'date' }, + { label: '选择', value: 'select' }, + { label: '单选', value: 'radio' }, + { label: '多行文本', value: 'textarea' }, + ]; + + export const GroupOptions = [ + { label: '个人基本信息', value: '个人基本信息' }, + { label: '政治信息', value: '政治信息' }, + { label: '教育背景', value: '教育背景' }, + { label: '职务信息', value: '职务信息' }, + { label: '入职信息', value: '入职信息' }, + { label: '培训信息', value: '培训信息' }, + { label: '鉴定信息', value: '鉴定信息' }, + { label: '工作信息', value: '工作信息' }, + { label: '其他信息', value: '其他信息' }, + ]; \ No newline at end of file diff --git a/apps/web/src/app/admin/staffinfo-manage/optionCreator.tsx b/apps/web/src/app/admin/staffinfo-manage/optionCreator.tsx new file mode 100644 index 0000000..740ccc3 --- /dev/null +++ b/apps/web/src/app/admin/staffinfo-manage/optionCreator.tsx @@ -0,0 +1,77 @@ +import React, { useState, useEffect } from 'react'; +import { Button, Input, Space, Tag } from 'antd'; +import { DeleteOutlined } from '@ant-design/icons'; + +type OptionCreatorProps = { + options: { [key: string]: string } | { label: string; value: string }[] | undefined; + onChange: (newOptions: { label: string; value: string }[]) => void; +}; + +const OptionCreator: React.FC = ({ options, onChange }) => { + const [localOptions, setLocalOptions] = useState<{ label: string; value: string }[]>([]); + const [inputValue, setInputValue] = useState(''); + + // 监听传入的 options 变化,更新本地状态 + useEffect(() => { + let validOptions: { label: string; value: string }[] = []; + if (Array.isArray(options)) { + validOptions = options; + } else if (typeof options === 'object' && options !== null) { + validOptions = Object.entries(options).map(([value, label]) => ({ label, value })); + } + setLocalOptions(validOptions); + }, [options]); + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleAddOption = () => { + if (inputValue.trim()) { + const newOption = { label: inputValue, value: inputValue }; + // 检查新选项是否已存在,避免重复添加 + const isDuplicate = localOptions.some(option => option.value === newOption.value); + if (!isDuplicate) { + const newOptions = [...localOptions, newOption]; + setLocalOptions(newOptions); + // 即时调用 onChange 通知父组件更新表单值 + onChange(newOptions); + } + setInputValue(''); + } + }; + + const handleDeleteOption = (value: string) => { + const newOptions = localOptions.filter(option => option.value !== value); + setLocalOptions(newOptions); + // 即时调用 onChange 通知父组件更新表单值 + onChange(newOptions); + }; + + return ( +
+ + + + +
+ {localOptions.map(option => ( + handleDeleteOption(option.value)} + icon={} + > + {option.label} + + ))} +
+
+ ); +}; + +export default OptionCreator; diff --git a/apps/web/src/app/admin/staffinfo-manage/staffFieldManage.tsx b/apps/web/src/app/admin/staffinfo-manage/staffFieldManage.tsx new file mode 100644 index 0000000..d79edff --- /dev/null +++ b/apps/web/src/app/admin/staffinfo-manage/staffFieldManage.tsx @@ -0,0 +1,162 @@ +import React, { useEffect } from 'react'; +import { Button, Space, message } from 'antd'; +import { useState } from 'react'; +import { useStaff } from '@nice/client'; +import { PlusOutlined } from '@ant-design/icons'; +import DefaultFieldInitializer from './defaultFieldInitializer'; +import StaffFieldTable from './staffFieldTable'; +import StaffFieldModal from './staffFieldModal'; +import { useWatch } from 'antd/es/form/Form'; +import { Form } from 'antd'; +const StaffFieldManage = () => { + const [form] = Form.useForm(); + const [isModalVisible, setIsModalVisible] = useState(false); + const [editingField, setEditingField] = useState(null); + const { + addCustomField, + updateCustomField, + deleteCustomField, + useCustomFields + } = useStaff(); + const { data: fields, isLoading } = useCustomFields(); + const optionFieldTypes = ['select', 'radio']; + const [showOptions, setShowOptions] = useState(false); + const typeValue = useWatch('type', form); + + useEffect(() => { + setShowOptions(optionFieldTypes.includes(typeValue)); + }, [typeValue, optionFieldTypes]); + + const [isInitConfirmVisible, setIsInitConfirmVisible] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + const checkFieldExists = (name: string, excludeId?: string) => { + return fields?.some(field => + field.name === name && field.id !== excludeId + ); + }; + + const handleAdd = () => { + setEditingField(null); + form.resetFields(); + form.setFieldsValue({ options: [] }); + setIsModalVisible(true); + }; + + const handleEdit = (record: any) => { + setEditingField(record); + let optionsToSet = record.options; + if (typeof record.options === 'object' && record.options !== null && !Array.isArray(record.options)) { + optionsToSet = Object.entries(record.options).map(([value, label]) => ({ label, value })); + } + form.setFieldsValue({ ...record, options: optionsToSet }); + setIsModalVisible(true); + }; + + const handleDelete = async (id: string) => { + try { + await deleteCustomField.mutateAsync(id); + message.success('字段删除成功'); + } catch (error) { + message.error('字段删除失败'); + } + }; + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + + if (checkFieldExists(values.name, editingField?.id)) { + message.error('字段标识已存在,请使用其他标识'); + return; + } + + setIsProcessing(true); + + if (editingField) { + await updateCustomField.mutateAsync({ + id: editingField.id, + ...values, + }); + message.success('字段更新成功'); + } else { + await addCustomField.mutateAsync(values); + message.success('字段添加成功'); + } + setIsModalVisible(false); + } catch (error) { + message.error('操作失败:' + (error instanceof Error ? error.message : '未知错误')); + } finally { + setIsProcessing(false); + } + }; + + const handleBatchDelete = async () => { + if (selectedRowKeys.length === 0) { + message.warning('请选择要删除的字段'); + return; + } + try { + await Promise.all( + selectedRowKeys.map(async (id: string) => { + await deleteCustomField.mutateAsync(id); + }) + ); + message.success('批量删除成功'); + setSelectedRowKeys([]); + } catch (error) { + message.error('批量删除失败'); + } + }; + + return ( +
+
+

字段管理

+ + + + + +
+ + + + +
+ ); +}; + +export default StaffFieldManage; \ No newline at end of file diff --git a/apps/web/src/app/admin/staffinfo-manage/staffFieldModal.tsx b/apps/web/src/app/admin/staffinfo-manage/staffFieldModal.tsx new file mode 100644 index 0000000..614076d --- /dev/null +++ b/apps/web/src/app/admin/staffinfo-manage/staffFieldModal.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { Form, Input, Select, InputNumber, Modal, message, FormInstance } from 'antd'; +import OptionCreator from './optionCreator'; +import { FieldTypeOptions, GroupOptions } from './defaultFields'; + +type StaffFieldModalProps = { + form: FormInstance; + isModalVisible: boolean; + setIsModalVisible: (visible: boolean) => void; + editingField: any; + isProcessing: boolean; + handleSubmit: () => Promise; + optionFieldTypes: string[]; + showOptions: boolean; +}; + +const StaffFieldModal: React.FC = ({ + form, + isModalVisible, + setIsModalVisible, + editingField, + isProcessing, + handleSubmit, + optionFieldTypes, + showOptions +}) => { + return ( + setIsModalVisible(false)} + confirmLoading={isProcessing} + > +
+ + + + + + + + + + + + + + ; + case 'number': + return ; + case 'date': + return ; + case 'select': + // 检查 field.options 是否存在且有数据 + if (field.options && field.options.length > 0) { + return ; + case 'radio': + // 检查 field.options 是否存在且有数据 + if (field.options && field.options.length > 0) { + return ; + } + return ; + case 'textarea': + return ; + default: + return ; + } + }; + + const onFinish = async (values: any) => { + try { + setLoading(true); + const formattedValues = { + ...values, + birthplace: values.birthplace?.join('/'), + }; + + await create.mutateAsync({ + data: formattedValues + }); + + message.success("信息提交成功"); + } catch (error) { + console.error('提交出错:', error); + message.error("提交失败,请重试"); + } finally { + setLoading(false); + } + }; + + if (fieldsLoading) { + return
加载中...
; + } + + return ( +
+

人员信息管理

+ + {/* 信息填报表单 */} + + {Object.entries(fieldGroups).map(([groupName, groupFields]) => ( +
+

{groupName}

+
+ {groupFields.map((field: any) => ( + + {renderField(field)} + + ))} +
+
+ ))} + +
+ + +
+ +
+ ); +}; + +export default StaffInfoWrite; \ No newline at end of file diff --git a/apps/web/src/app/main/staffpage/staffmodal/page.tsx b/apps/web/src/app/main/staffpage/staffmodal/page.tsx deleted file mode 100644 index 50d09b3..0000000 --- a/apps/web/src/app/main/staffpage/staffmodal/page.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { api, useStaff } from "@nice/client"; -import { useMainContext } from "../../layout/MainProvider"; -import toast from "react-hot-toast"; -import { Button, Form, Input, Modal, Select } from "antd"; -import { useEffect } from "react"; -import TrainContentTreeSelect from "@web/src/components/models/trainContent/train-content-tree-select"; -import DepartmentChildrenSelect from "@web/src/components/models/department/department-children-select"; - -export default function StaffModal() { - const { data: traincontents} = api.trainSituation.findMany.useQuery({ - select:{ - id: true, - trainContent:{ - select:{ - id: true, - title: true, - type: true, - } - }, - } - - }); - // useEffect(() => { - // traincontents?.forEach((situation)=>{ - // console.log(situation.id); - // }); - // }, [traincontents]); - - const { form, formValue, setVisible, visible, editingRecord, setEditingRecord } = useMainContext() - const { create, update } = useStaff(); - const handleOk = async () => { - const values = await form.getFieldsValue(); - // console.log(values); - // console.log(values.position); - try { - if (editingRecord && editingRecord.id) { - await update.mutateAsync({ - where: { id: editingRecord.id }, - data: { - username: values.username, - department: { - connect: { id: values.deptId } - }, - rank: values.rank, - type: values.type, - showname: values.username, - updatedAt: new Date() - } , - }); - // console.log(result); - } else { - const result = await create.mutateAsync( - { - data: { - username: values.username, - department: { - connect: { id: values.deptId } - }, - createdAt: new Date(), - showname: values.username, - rank: values.rank, - type: values.type, - } as any, - } - ) ; - } - toast.success("保存成功"); - setVisible(false); - setEditingRecord(null); - } catch (error) { - toast.error("保存失败"); - throw error; - } - }; - - const handleCancel = () => { - setVisible(false); - setEditingRecord(null); - form.resetFields(); - }; - - useEffect(() => { - if (visible&&editingRecord) { - form.setFieldsValue(editingRecord); - } - }, [visible,editingRecord]); - return ( - <> - -
- - - - - - - - - - - - - -
-
- {/* */} - - ) -} \ No newline at end of file diff --git a/apps/web/src/routes/admin-route.tsx b/apps/web/src/routes/admin-route.tsx index 62a0f0a..97f16c8 100755 --- a/apps/web/src/routes/admin-route.tsx +++ b/apps/web/src/routes/admin-route.tsx @@ -15,6 +15,7 @@ import WithAuth from "../components/utils/with-auth"; import { CustomRouteObject } from "./types"; import StaffPage from "../app/admin/staff/page"; import AdminLayout from "../components/layout/admin/AdminLayout"; +import StaffFieldManage from "../app/admin/staffinfo-manage/staffFieldManage"; export const adminRoute: CustomRouteObject = { path: "admin", @@ -120,5 +121,11 @@ export const adminRoute: CustomRouteObject = { }, }, }, + { + path: "staffinfo", + name: "人员信息管理", + icon: , + element: , + }, ], }; diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 6b89a3c..2744f81 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -8,12 +8,12 @@ import { import ErrorPage from "../app/error"; import LoginPage from "../app/login"; import HomePage from "../app/main/home/page"; -import StaffMessage from "../app/main/staffpage/page"; +import StaffMessage from "../app/main/staffinfo_show/page"; import MainLayout from "../app/main/layout/MainLayout"; import DailyPage from "../app/main/daily/page"; import Dashboard from "../app/main/home/page"; import WeekPlanPage from "../app/main/plan/weekplan/page"; -import StaffInformation from "../app/main/staffinformation/page"; +import StaffInformation from "../app/main/staffinfo_write/page"; import DeptSettingPage from "../app/main/admin/deptsettingpage/page"; import { adminRoute } from "./admin-route"; import AdminLayout from "../components/layout/admin/AdminLayout"; diff --git a/packages/client/src/api/hooks/useStaff.ts b/packages/client/src/api/hooks/useStaff.ts index aed3aa7..4c70184 100755 --- a/packages/client/src/api/hooks/useStaff.ts +++ b/packages/client/src/api/hooks/useStaff.ts @@ -4,7 +4,37 @@ import { useQueryClient } from "@tanstack/react-query"; import { ObjectType, Staff } from "@nice/common"; import { findQueryData } from "../utils"; import { CrudOperation, emitDataChange } from "../../event"; -export function useStaff() { + +export interface CustomField { + name: string; + label?: string; + type: string; + required?: boolean; + order?: number; + options?: any; + group?: string; +} + +export interface CustomFieldValue { + staffId: string; + fieldId: string; + value?: string; +} + +interface StaffHookReturn { + create: ReturnType; + update: ReturnType; + softDeleteByIds: ReturnType; + getStaff: (key: string) => Staff | undefined; + updateUserDomain: ReturnType; + addCustomField: ReturnType; + updateCustomField: ReturnType; + deleteCustomField: ReturnType; + useCustomFields: () => ReturnType; + setCustomFieldValue: ReturnType; +} + +export function useStaff(): StaffHookReturn { const queryClient = useQueryClient(); const queryKey = getQueryKey(api.staff); @@ -19,8 +49,9 @@ export function useStaff() { }, }); const updateUserDomain = api.staff.updateUserDomain.useMutation({ - onSuccess: async (result) => { + onSuccess: (result) => { queryClient.invalidateQueries({ queryKey }); + emitDataChange(ObjectType.STAFF, result as any, CrudOperation.UPDATED); }, }); const update = api.staff.update.useMutation({ @@ -39,14 +70,45 @@ export function useStaff() { queryClient.invalidateQueries({ queryKey }); }, }); + const addCustomField = api.staff.addCustomField.useMutation({ + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: getQueryKey(api.staff.getCustomFields) }); + }, + }); + const updateCustomField = api.staff.updateCustomField.useMutation({ + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: getQueryKey(api.staff.getCustomFields) }); + }, + }); + const deleteCustomField = api.staff.deleteCustomField.useMutation({ + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: getQueryKey(api.staff.getCustomFields) }); + }, + }); + const useCustomFields = () => { + return api.staff.getCustomFields.useQuery(undefined, { + staleTime: 1000 * 60 * 5, // 5分钟缓存 + }); + }; + const setCustomFieldValue = api.staff.setCustomFieldValue.useMutation({ + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey }); + }, + }); const getStaff = (key: string) => { return findQueryData(queryClient, api.staff, key); }; + return { create, update, softDeleteByIds, getStaff, updateUserDomain, + addCustomField, + updateCustomField, + deleteCustomField, + useCustomFields, + setCustomFieldValue, }; } diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index c4148d8..2437871 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -420,6 +420,41 @@ model Department { @@map("department") } +model StaffField { + id String @id @default(cuid()) + name String @unique // 字段名称 + label String? // 字段显示名称 + type String // 字段类型 (text, number, date, select 等) + required Boolean? @default(false) + order Float? // 显示顺序 + options Json? // 对于选择类型字段的可选值 + group String? // 字段分组 (基本信息、工作信息等) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") + + @@index([group]) + @@index([order]) + @@map("staff_field") + StaffFieldValue StaffFieldValue[] +} + +model StaffFieldValue { + id String @id @default(cuid()) + staffId String @map("staff_id") + fieldId String @map("field_id") + value String? // 字段值 + staff Staff @relation(fields: [staffId], references: [id]) + field StaffField @relation(fields: [fieldId], references: [id]) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([staffId, fieldId]) + @@index([staffId]) + @@index([fieldId]) + @@map("staff_field_value") +} + model Staff { // 基础信息 id String @id @default(cuid()) @@ -510,6 +545,9 @@ model Staff { deletedAt DateTime? @map("deleted_at") absent Boolean? @default(false) @map("absent") + // 添加自定义字段值关联 + fieldValues StaffFieldValue[] + @@index([officerId]) @@index([deptId]) @@index([domainId])