任务修改回显
This commit is contained in:
@ -1,10 +1,32 @@
|
||||
import React, { useState, useEffect, forwardRef, useImperativeHandle, useMemo, useCallback } from 'react';
|
||||
import { Card, Table, Tag, Switch, Space, Button, message, Modal, Radio, Checkbox, Row, Col, Spin } from 'antd';
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Tag,
|
||||
Switch,
|
||||
Space,
|
||||
Button,
|
||||
message,
|
||||
Modal,
|
||||
Radio,
|
||||
Checkbox,
|
||||
Row,
|
||||
Col,
|
||||
Spin,
|
||||
} from 'antd';
|
||||
import type { PersonnelItem } from '@/servers/types/evaluator';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { CheckboxValueType } from 'antd/es/checkbox/Group';
|
||||
import EvaluateTemplateTable from '@/components/EvaluateTemplateTable';
|
||||
import { getTemplateDetail } from '@/servers/api/supplierEvaluate';
|
||||
import type { TaskAddRequest } from '@/servers/types/supplierEvaluateTask';
|
||||
|
||||
// 评价指标类型定义
|
||||
interface IndicatorItem {
|
||||
@ -15,8 +37,8 @@ interface IndicatorItem {
|
||||
|
||||
// 组件接收的Props定义
|
||||
interface DivisionStepProps {
|
||||
formData: any;
|
||||
onFormDataChange?: (values: any) => void;
|
||||
formData: Partial<TaskAddRequest>;
|
||||
onFormDataChange?: (values: Partial<TaskAddRequest>) => void;
|
||||
}
|
||||
|
||||
// 评价方式枚举
|
||||
@ -27,26 +49,23 @@ enum EvaluateType {
|
||||
INDICATOR = 1, // 按指标评价(部分指标)
|
||||
}
|
||||
|
||||
// 模拟的评价指标数据 - 仅作为备用
|
||||
const mockIndicators: IndicatorItem[] = [
|
||||
{ id: 'I001', name: '产品质量', description: '评估供应商产品质量' },
|
||||
{ id: 'I002', name: '交货及时性', description: '评估供应商交货的及时性' },
|
||||
{ id: 'I003', name: '服务水平', description: '评估供应商提供的服务水平' },
|
||||
{ id: 'I004', name: '价格竞争力', description: '评估供应商产品价格的竞争力' },
|
||||
{ id: 'I005', name: '技术能力', description: '评估供应商的技术创新能力' },
|
||||
];
|
||||
|
||||
const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataChange }, ref) => {
|
||||
// 从上一步获取的评价人员列表
|
||||
const [evaluators, setEvaluators] = useState<PersonnelItem[]>([]);
|
||||
|
||||
// 评价人员指标分配数据
|
||||
const [indicatorAssignments, setIndicatorAssignments] = useState<{
|
||||
const indicatorAssignments = useRef<{
|
||||
[userId: string]: {
|
||||
type: EvaluateType;
|
||||
indicatorIds: string[];
|
||||
};
|
||||
}>({});
|
||||
// const [indicatorAssignments, setIndicatorAssignments] = useState<{
|
||||
// [userId: string]: {
|
||||
// type: EvaluateType;
|
||||
// indicatorIds: string[];
|
||||
// };
|
||||
// }>({});
|
||||
|
||||
// 选中的行keys
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
@ -78,8 +97,66 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
// 选中的指标项
|
||||
const [selectedTemplateItems, setSelectedTemplateItems] = useState<any[]>([]);
|
||||
|
||||
// 查看指标分工弹窗可见性
|
||||
const [viewModalVisible, setViewModalVisible] = useState(false);
|
||||
|
||||
// 查看模式下的过滤后指标数据
|
||||
const [filteredIndicators, setFilteredIndicators] = useState<any[]>([]);
|
||||
|
||||
// 统一获取用户指标ID的函数
|
||||
const getUserIndicatorIds = useCallback((userId: string) => {
|
||||
if (!userId) return [];
|
||||
|
||||
const assignment = indicatorAssignments.current[userId];
|
||||
if (!assignment) return [];
|
||||
|
||||
// 如果是按评价单评价(全部指标),返回空数组
|
||||
if (assignment.type == EvaluateType.ALL) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 如果是按指标评价,返回指标ID列表
|
||||
return assignment.indicatorIds || [];
|
||||
}, []);
|
||||
|
||||
// 获取当前用户的已分配指标ID
|
||||
const getCurrentUserSelectedIds = useCallback(() => {
|
||||
// 使用统一的getUserIndicatorIds函数获取当前用户的指标ID
|
||||
return getUserIndicatorIds(currentUserId);
|
||||
}, [currentUserId, getUserIndicatorIds]);
|
||||
|
||||
// 根据指标ID过滤模板数据
|
||||
const filterTemplateDataByIds = useCallback((data: any[], indicatorIds: string[]) => {
|
||||
// 如果indicatorIds为空,表示显示所有模板数据(按评价单评价)
|
||||
if (!indicatorIds || indicatorIds.length === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 按指标ID过滤
|
||||
const filtered = [];
|
||||
for (const stItem of data) {
|
||||
const ndItems = [];
|
||||
if (stItem.indicatorNdList) {
|
||||
for (const ndItem of stItem.indicatorNdList) {
|
||||
const matched = indicatorIds.some((id) => String(id) === String(ndItem.id));
|
||||
if (matched) {
|
||||
ndItems.push(ndItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ndItems.length > 0) {
|
||||
filtered.push({
|
||||
...stItem,
|
||||
indicatorNdList: ndItems,
|
||||
});
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}, []);
|
||||
|
||||
// 获取模板详情 先写死 "1937123786334322688" 省的一步一步操作
|
||||
const fetchTemplateDetail = async (templateId: string = "1937123786334322688") => {
|
||||
const fetchTemplateDetail = async (templateId: string) => {
|
||||
if (!templateId) return;
|
||||
|
||||
try {
|
||||
@ -113,7 +190,7 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
|
||||
// 监听templateId变化,获取模板详情
|
||||
useEffect(() => {
|
||||
fetchTemplateDetail(formData.templateId);
|
||||
fetchTemplateDetail(formData.templateId as string);
|
||||
}, []);
|
||||
|
||||
// 处理行选择变化
|
||||
@ -147,15 +224,15 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
// 批量设置指标分工
|
||||
const handleBatchSetDivision = () => {
|
||||
// 获取选中的评价人员
|
||||
const selectedEvaluators = evaluators.filter(
|
||||
evaluator => selectedRowKeys.includes(evaluator.id)
|
||||
const selectedEvaluators = evaluators.filter((evaluator) =>
|
||||
selectedRowKeys.includes(evaluator.id),
|
||||
);
|
||||
|
||||
// 提取所有选中指标的ID
|
||||
const selectedIndicatorIds: string[] = [];
|
||||
|
||||
// 从选中的模板项中提取所有指标ID
|
||||
batchSelectedTemplateItems.forEach(stItem => {
|
||||
batchSelectedTemplateItems.forEach((stItem) => {
|
||||
// 添加二级指标ID
|
||||
if (stItem.indicatorNdList && stItem.indicatorNdList.length > 0) {
|
||||
stItem.indicatorNdList.forEach((ndItem: any) => {
|
||||
@ -165,9 +242,9 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
});
|
||||
|
||||
// 更新指标分配数据
|
||||
const newAssignments = { ...indicatorAssignments };
|
||||
const newAssignments = { ...indicatorAssignments.current };
|
||||
|
||||
selectedEvaluators.forEach(evaluator => {
|
||||
selectedEvaluators.forEach((evaluator) => {
|
||||
newAssignments[evaluator.id] = {
|
||||
// 评价类型:如果用户关联了指标则为1(按指标),否则为0(按评价单)
|
||||
type: selectedIndicatorIds.length > 0 ? EvaluateType.INDICATOR : EvaluateType.ALL,
|
||||
@ -175,7 +252,7 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
};
|
||||
});
|
||||
|
||||
setIndicatorAssignments(newAssignments);
|
||||
indicatorAssignments.current = newAssignments;
|
||||
setBatchTemplateModalVisible(false);
|
||||
message.success(`已为${selectedRowKeys.length}名评价人员设置分工`);
|
||||
};
|
||||
@ -197,23 +274,10 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
|
||||
// 处理模板指标选择
|
||||
const handleTemplateItemsSelect = (selectedItems: any[]) => {
|
||||
console.log(selectedItems)
|
||||
console.log(selectedItems);
|
||||
setSelectedTemplateItems(selectedItems);
|
||||
};
|
||||
|
||||
// 获取当前用户的已分配指标ID
|
||||
const getCurrentUserSelectedIds = useCallback(() => {
|
||||
if (!currentUserId) return [];
|
||||
|
||||
const userAssignment = indicatorAssignments[currentUserId];
|
||||
if (userAssignment && userAssignment.type === EvaluateType.INDICATOR) {
|
||||
const selectedIds = userAssignment.indicatorIds || [];
|
||||
return selectedIds;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [currentUserId, indicatorAssignments]);
|
||||
|
||||
// 保存指标分配
|
||||
const handleSaveIndicatorAssignment = () => {
|
||||
if (!currentUserId) {
|
||||
@ -225,7 +289,7 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
const selectedIndicatorIds: string[] = [];
|
||||
|
||||
// 从选中的模板项中提取所有指标ID
|
||||
selectedTemplateItems.forEach(stItem => {
|
||||
selectedTemplateItems.forEach((stItem) => {
|
||||
// 添加二级指标ID
|
||||
if (stItem.indicatorNdList && stItem.indicatorNdList.length > 0) {
|
||||
stItem.indicatorNdList.forEach((ndItem: any) => {
|
||||
@ -235,46 +299,43 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
});
|
||||
|
||||
// 更新指标分配
|
||||
const newAssignments = { ...indicatorAssignments };
|
||||
const newAssignments = { ...indicatorAssignments.current };
|
||||
newAssignments[currentUserId] = {
|
||||
// 评价类型:如果用户关联了指标则为1(按指标),否则为0(按评价单)
|
||||
type: selectedIndicatorIds.length > 0 ? EvaluateType.INDICATOR : EvaluateType.ALL,
|
||||
indicatorIds: selectedIndicatorIds,
|
||||
};
|
||||
|
||||
setIndicatorAssignments(newAssignments);
|
||||
indicatorAssignments.current = newAssignments;
|
||||
setTemplateViewModalVisible(false);
|
||||
message.success('已设置评价人员指标分工');
|
||||
};
|
||||
|
||||
// 查看评价人员的指标分工
|
||||
const handleViewAssignment = (userId: string) => {
|
||||
const assignment = indicatorAssignments[userId];
|
||||
const assignment = indicatorAssignments.current[userId];
|
||||
if (!assignment) {
|
||||
message.info('该评价人员尚未设置分工');
|
||||
return;
|
||||
}
|
||||
|
||||
const assignedIndicators = indicators.filter(ind =>
|
||||
assignment.type === EvaluateType.ALL ||
|
||||
assignment.indicatorIds.includes(ind.id)
|
||||
);
|
||||
setCurrentUserId(userId);
|
||||
setLoading(true);
|
||||
|
||||
Modal.info({
|
||||
title: '查看评价指标分工',
|
||||
content: (
|
||||
<div>
|
||||
<p>评价方式: {assignment.type === EvaluateType.ALL ? '按评价单评价' : '按指标评价'}</p>
|
||||
<p>评价指标: </p>
|
||||
<ul>
|
||||
{assignedIndicators.map(ind => (
|
||||
<li key={ind.id}>{ind.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
okText: '确定',
|
||||
});
|
||||
// 获取该评价人员的指标ID
|
||||
const indicatorIds = getUserIndicatorIds(userId);
|
||||
|
||||
// 过滤模板数据
|
||||
const filtered = filterTemplateDataByIds(templateData, indicatorIds);
|
||||
setFilteredIndicators(filtered);
|
||||
|
||||
setLoading(false);
|
||||
setViewModalVisible(true);
|
||||
};
|
||||
|
||||
// 关闭查看模态框
|
||||
const handleCloseViewModal = () => {
|
||||
setViewModalVisible(false);
|
||||
};
|
||||
|
||||
// 删除评价人员
|
||||
@ -286,12 +347,12 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
// 更新评价人员列表
|
||||
setEvaluators(prev => prev.filter(e => e.id !== userId));
|
||||
setEvaluators((prev) => prev.filter((e) => e.id !== userId));
|
||||
|
||||
// 更新指标分配数据
|
||||
const newAssignments = { ...indicatorAssignments };
|
||||
const newAssignments = { ...indicatorAssignments.current };
|
||||
delete newAssignments[userId];
|
||||
setIndicatorAssignments(newAssignments);
|
||||
indicatorAssignments.current = newAssignments;
|
||||
|
||||
message.success('已删除评价人员');
|
||||
},
|
||||
@ -300,7 +361,7 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
|
||||
// 初始化:从formData中提取指标分配数据
|
||||
useEffect(() => {
|
||||
if (formData.indicatorList) {
|
||||
if (formData.indicatorList && formData.indicatorList.length > 0) {
|
||||
// 如果已有指标分配数据,直接使用
|
||||
const assignments: any = {};
|
||||
formData.indicatorList.forEach((item: any) => {
|
||||
@ -309,7 +370,7 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
indicatorIds: item.indicatorIds || [],
|
||||
};
|
||||
});
|
||||
setIndicatorAssignments(assignments);
|
||||
indicatorAssignments.current = assignments;
|
||||
}
|
||||
}, [formData.indicatorList]);
|
||||
|
||||
@ -324,7 +385,7 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
if (supplier.evaluators && supplier.evaluators.length > 0) {
|
||||
supplier.evaluators.forEach((evaluator: PersonnelItem) => {
|
||||
// 检查是否已存在(避免重复)
|
||||
if (!allEvaluators.some(e => e.id === evaluator.id)) {
|
||||
if (!allEvaluators.some((e) => e.id === evaluator.id)) {
|
||||
allEvaluators.push({
|
||||
...evaluator,
|
||||
// 添加单位和员工编号,假设这些字段可能不存在
|
||||
@ -343,11 +404,11 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
// 为评价人员初始化指标分配数据 - 作为单独的效果处理
|
||||
useEffect(() => {
|
||||
// 检查是否有新的评价人员需要初始化
|
||||
const newAssignments = { ...indicatorAssignments };
|
||||
const newAssignments = { ...indicatorAssignments.current };
|
||||
let hasNewAssignments = false;
|
||||
|
||||
evaluators.forEach(evaluator => {
|
||||
if (!indicatorAssignments[evaluator.id]) {
|
||||
evaluators.forEach((evaluator) => {
|
||||
if (!indicatorAssignments.current[evaluator.id]) {
|
||||
newAssignments[evaluator.id] = {
|
||||
type: EvaluateType.ALL,
|
||||
indicatorIds: [],
|
||||
@ -355,22 +416,26 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
hasNewAssignments = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasNewAssignments) {
|
||||
setIndicatorAssignments(newAssignments);
|
||||
indicatorAssignments.current = newAssignments;
|
||||
}
|
||||
}, [evaluators, indicatorAssignments]);
|
||||
|
||||
// 同步数据回表单 - 使用防抖确保不会频繁触发
|
||||
const previousValueRef = React.useRef<string>("");
|
||||
const previousValueRef = React.useRef<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
// 若当前还未初始化完成(没有任何指标数据),不应向父组件同步
|
||||
if (evaluators.length === 0 || Object.keys(indicatorAssignments.current).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 将评价人员列表和指标分配数据同步回表单
|
||||
const indicatorList = evaluators.map(evaluator => ({
|
||||
const indicatorList = evaluators.map((evaluator) => ({
|
||||
userId: evaluator.id,
|
||||
userName: evaluator.name,
|
||||
type: indicatorAssignments[evaluator.id]?.type || EvaluateType.ALL,
|
||||
indicatorIds: indicatorAssignments[evaluator.id]?.indicatorIds || [],
|
||||
type: indicatorAssignments.current[evaluator.id]?.type ?? EvaluateType.ALL,
|
||||
indicatorIds: indicatorAssignments.current[evaluator.id]?.indicatorIds ?? [],
|
||||
}));
|
||||
|
||||
// 使用JSON字符串比较确保只有在真正变化时才更新
|
||||
@ -382,7 +447,7 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
indicatorList,
|
||||
});
|
||||
}
|
||||
}, [evaluators, indicatorAssignments, formData, onFormDataChange]);
|
||||
}, [evaluators, formData, onFormDataChange]);
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
@ -397,6 +462,12 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
},
|
||||
}));
|
||||
|
||||
// 获取当前评价人员名称
|
||||
const getCurrentEvaluatorName = () => {
|
||||
const evaluator = evaluators.find((e) => e.id === currentUserId);
|
||||
return evaluator ? evaluator.name : currentUserId;
|
||||
};
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType<PersonnelItem> = [
|
||||
{
|
||||
@ -404,27 +475,23 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '所属单位',
|
||||
dataIndex: 'company',
|
||||
key: 'company',
|
||||
},
|
||||
{
|
||||
title: '所属部门',
|
||||
dataIndex: 'department',
|
||||
key: 'department',
|
||||
dataIndex: 'userDept',
|
||||
key: 'userDept',
|
||||
},
|
||||
{
|
||||
title: '员工编号',
|
||||
dataIndex: 'employeeNumber',
|
||||
key: 'employeeNumber',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
title: '是否设置分工',
|
||||
key: 'hasDivision',
|
||||
render: (_: any, record: PersonnelItem) => {
|
||||
const assignment = indicatorAssignments[record.id];
|
||||
if (!assignment) return <Tag color="red">未设置</Tag>;
|
||||
const assignment = indicatorAssignments.current[record.id];
|
||||
if (!assignment || assignment.indicatorIds.length === 0)
|
||||
return <Tag color="red">未设置</Tag>;
|
||||
return <Tag color="green">已设置</Tag>;
|
||||
},
|
||||
},
|
||||
@ -433,9 +500,15 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
key: 'action',
|
||||
render: (_: any, record: PersonnelItem) => (
|
||||
<Space size="middle">
|
||||
<Button type="link" onClick={() => handleAssignIndicators(record.id)}>评价指标分工</Button>
|
||||
<Button type="link" onClick={() => handleViewAssignment(record.id)}>查看</Button>
|
||||
<Button type="link" onClick={() => handleRemoveEvaluator(record.id)}>删除</Button>
|
||||
<Button type="link" onClick={() => handleAssignIndicators(record.id)}>
|
||||
评价指标分工
|
||||
</Button>
|
||||
<Button type="link" onClick={() => handleViewAssignment(record.id)}>
|
||||
查看
|
||||
</Button>
|
||||
<Button type="link" onClick={() => handleRemoveEvaluator(record.id)}>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@ -469,14 +542,14 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
title={`批量设置评价指标分工 (已选择 ${selectedRowKeys.length} 名评价人员)`}
|
||||
visible={batchTemplateModalVisible}
|
||||
onCancel={handleCloseBatchTemplateModal}
|
||||
width={800}
|
||||
width={1200}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={handleCloseBatchTemplateModal}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="save" type="primary" onClick={handleBatchSetDivision}>
|
||||
确定并保存
|
||||
</Button>
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
@ -507,7 +580,7 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
</Button>,
|
||||
<Button key="save" type="primary" onClick={handleSaveIndicatorAssignment}>
|
||||
保存
|
||||
</Button>
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
@ -527,6 +600,29 @@ const DivisionStep = forwardRef<any, DivisionStepProps>(({ formData, onFormDataC
|
||||
)}
|
||||
</Spin>
|
||||
</Modal>
|
||||
|
||||
{/* 查看评价人员指标分工弹窗 */}
|
||||
<Modal
|
||||
title={`评价人员 ${getCurrentEvaluatorName()} 的指标分工`}
|
||||
visible={viewModalVisible}
|
||||
onCancel={handleCloseViewModal}
|
||||
width={1200}
|
||||
footer={[
|
||||
<Button key="close" onClick={handleCloseViewModal}>
|
||||
关闭
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
{filteredIndicators.length > 0 ? (
|
||||
<EvaluateTemplateTable value={filteredIndicators} isDetail={true} />
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '20px 0' }}>
|
||||
{loading ? '加载中...' : '暂无指标分工数据'}
|
||||
</div>
|
||||
)}
|
||||
</Spin>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
Reference in New Issue
Block a user