wuqun
2 years ago
8 changed files with 943 additions and 45 deletions
@ -0,0 +1,89 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
import { ApiTable, basicAction } from '$utils' |
||||
|
|
||||
|
export function addSalesMemberBulk(values) { |
||||
|
return dispatch => basicAction({ |
||||
|
type: 'post', |
||||
|
dispatch: dispatch, |
||||
|
actionType: 'SALES_MEMBER_BULK_ADD', |
||||
|
url: ApiTable.addSalesMemberBulk, |
||||
|
data: values, |
||||
|
msg: { option: '导入销售人员信息' }, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export function postSalesMember(data) {//添加/编辑
|
||||
|
let msg = '' |
||||
|
if (data) { |
||||
|
msg = data.msg |
||||
|
} |
||||
|
return (dispatch) => |
||||
|
basicAction({ |
||||
|
type: "post", |
||||
|
dispatch: dispatch, |
||||
|
data, |
||||
|
actionType: "POST_SALES_MEMBER", |
||||
|
url: `${ApiTable.addSalesMember}`, |
||||
|
msg: { option: msg }, //添加/编辑
|
||||
|
reducer: { name: "" }, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export function getSalesList(query) {//查询
|
||||
|
return (dispatch) => basicAction({ |
||||
|
type: "get", |
||||
|
dispatch: dispatch, |
||||
|
actionType: "GET_SALES_MENBER_LIST", |
||||
|
query: query, |
||||
|
url: `${ApiTable.getSalesList}`, |
||||
|
msg: { option: "查询销售人员列表" }, |
||||
|
reducer: { name: "SalesMemberList", params: { noClear: true } }, |
||||
|
}); |
||||
|
} |
||||
|
export function delSalesMember(data) {//删除
|
||||
|
let msg = '' |
||||
|
if (data) { |
||||
|
msg = data.msg |
||||
|
} |
||||
|
return (dispatch) => |
||||
|
basicAction({ |
||||
|
type: "del", |
||||
|
query: data, |
||||
|
dispatch: dispatch, |
||||
|
actionType: "DEL_SALES_MEMBER", |
||||
|
url: `${ApiTable.delSalesMember}`, |
||||
|
msg: { option: msg }, //删除
|
||||
|
reducer: {}, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// export function getMemberExport(query) {//导出员工信息
|
||||
|
// return (dispatch) => basicAction({
|
||||
|
// type: "get",
|
||||
|
// dispatch: dispatch,
|
||||
|
// actionType: "GET_MemberEXPORT",
|
||||
|
// query: query,
|
||||
|
// url: `${ApiTable.getMemberExport}`,
|
||||
|
// msg: { option: "导出员工信息" },
|
||||
|
// reducer: { name: "MemberExport", params: { noClear: true } },
|
||||
|
// });
|
||||
|
// }
|
||||
|
|
||||
|
|
||||
|
export function editSalesMember(data) {//更新
|
||||
|
let msg = '' |
||||
|
if (data) { |
||||
|
msg = data.msg |
||||
|
} |
||||
|
return (dispatch) => |
||||
|
basicAction({ |
||||
|
type: "put", |
||||
|
dispatch: dispatch, |
||||
|
data, |
||||
|
actionType: "PUT_SALES_MEMBER", |
||||
|
url: `${ApiTable.editSalesMember}`, |
||||
|
msg: { option: msg }, //更新
|
||||
|
reducer: {}, |
||||
|
}); |
||||
|
} |
@ -1,42 +0,0 @@ |
|||||
import React, { useEffect, useState } from 'react'; |
|
||||
import { connect } from 'react-redux'; |
|
||||
import Empty from '../components/empty'; |
|
||||
import '../style.less' |
|
||||
|
|
||||
const PersonnelDistribution = (props) => { |
|
||||
const { dispatch, actions } = props |
|
||||
|
|
||||
return ( |
|
||||
<> |
|
||||
<div style={{ padding: '0px 12px' }}> |
|
||||
<div style={{ display: 'flex' }}> |
|
||||
<div style={{ color: 'rgba(0,0,0,0.45)', fontSize: 14 }}>招聘</div> |
|
||||
<div style={{ color: 'rgba(0,0,0,0.45)', fontSize: 14, margin: '0px 8px' }}>/</div> |
|
||||
<div style={{ color: 'rgba(0,0,0,0.45)', fontSize: 14 }}>销售统计</div> |
|
||||
<div style={{ color: '#033C9A', fontSize: 14, margin: '0px 8px' }}>/</div> |
|
||||
<div style={{ color: '#033C9A', fontSize: 14 }}>销售人员分布</div> |
|
||||
</div> |
|
||||
<div style={{ background: '#FFFFFF', boxShadow: '0px 0px 12px 2px rgba(220,222,224,0.2)', borderRadius: 2, padding: '20px 0px 20px 19px ', marginTop: 12 }}> |
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> |
|
||||
<div style={{ display: 'flex', alignItems: 'baseline' }}> |
|
||||
<div style={{ width: 0, height: 20, borderLeft: '3px solid #0F7EFB', borderTop: '3px solid transparent', borderBottom: '3px solid transparent' }}></div> |
|
||||
<div style={{ fontFamily: "YouSheBiaoTiHei", fontSize: 24, color: '#033C9A', marginLeft: 8 }}>销售人员分布</div> |
|
||||
<div style={{ marginLeft: 6, fontSize: 12, color: '#969799', fontFamily: "DINExp", }}>DISTRIBUTION OF SALES PERSONNEL</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
<Empty /> |
|
||||
</div> |
|
||||
</div> |
|
||||
</> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
function mapStateToProps(state) { |
|
||||
const { auth, global } = state; |
|
||||
return { |
|
||||
user: auth.user, |
|
||||
actions: global.actions, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
export default connect(mapStateToProps)(PersonnelDistribution); |
|
@ -0,0 +1,261 @@ |
|||||
|
'use strict'; |
||||
|
import React, { useState, useEffect } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import { Modal, Form, Button, Notification } from '@douyinfe/semi-ui'; |
||||
|
import { IconUpload } from '@douyinfe/semi-icons'; |
||||
|
import cityData from '../../components/city.json'; |
||||
|
import XLSX from 'xlsx' |
||||
|
import { membersBulkAdd } from '../../actions/personnelFiles' |
||||
|
//下载模板和上传文件读取
|
||||
|
const ImportSalersModal = props => { |
||||
|
const { dispatch, actions, onCancel, rzMembers } = props |
||||
|
const { humanAffairs } = actions; |
||||
|
const [msg, setMsg] = useState('') |
||||
|
const [loading, setLoading] = useState('') |
||||
|
const [postData, setPostData] = useState([]) |
||||
|
const [allProvinces, setAllProvinces] = useState([]) |
||||
|
//初始化
|
||||
|
useEffect(() => { |
||||
|
let allProvinces = [];//所有省
|
||||
|
cityData.map(cd => { |
||||
|
allProvinces.push(cd.name); |
||||
|
}); |
||||
|
setAllProvinces(allProvinces); |
||||
|
}, []); |
||||
|
|
||||
|
const confirm = () => { |
||||
|
if (postData.length) { |
||||
|
setLoading(true) |
||||
|
dispatch(humanAffairs.addSalesMemberBulk(postData)).then(res => { |
||||
|
if (res.success) { |
||||
|
onCancel() |
||||
|
} |
||||
|
setLoading(false) |
||||
|
}) |
||||
|
} else { |
||||
|
Notification.warning({ content: '没有数据可以提交,请上传数据文件', duration: 2 }) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const dldCsvMb = () => { |
||||
|
//表头
|
||||
|
let head = "员工编号,姓名,销售区域(省/直辖市),销售区域(市)\n" |
||||
|
//数据
|
||||
|
//let data = 1 + ',' + 2 + ',' + 3 + ',' + 4 + ',' + 5
|
||||
|
let templateCsv = "data:text/csv;charset=utf-8,\ufeff" + head; |
||||
|
//创建一个a标签
|
||||
|
let link = document.createElement("a"); |
||||
|
//为a标签设置属性
|
||||
|
link.setAttribute("href", templateCsv); |
||||
|
link.setAttribute("download", "人资系统销售人员信息导入模板.csv"); |
||||
|
//点击a标签
|
||||
|
link.click(); |
||||
|
} |
||||
|
const download = () => { |
||||
|
//dldTemplate();
|
||||
|
dldCsvMb(); |
||||
|
// let str = "";
|
||||
|
// rule.forEach((v, i) => {
|
||||
|
// str += `${v}\r\n`
|
||||
|
// })
|
||||
|
// dldText("填写说明.txt", str);
|
||||
|
} |
||||
|
|
||||
|
// const dldText = (filename, text) => {
|
||||
|
// var element = document.createElement('a');
|
||||
|
// element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||
|
// element.setAttribute('download', filename);
|
||||
|
|
||||
|
// element.style.display = 'none';
|
||||
|
// document.body.appendChild(element);
|
||||
|
|
||||
|
// element.click();
|
||||
|
// document.body.removeChild(element);
|
||||
|
// }
|
||||
|
const fileLimit = '.csv'; |
||||
|
|
||||
|
const getFileBlob = (url) => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
let request = new XMLHttpRequest() |
||||
|
request.open("GET", url, true) |
||||
|
request.responseType = "blob" |
||||
|
request.onreadystatechange = e => { |
||||
|
if (request.readyState == 4) { |
||||
|
if (request.status == 200) { |
||||
|
if (window.FileReader) { |
||||
|
let reader = new FileReader(); |
||||
|
reader.readAsBinaryString(request.response); |
||||
|
reader.onload = event => { |
||||
|
try { |
||||
|
const { result } = event.target; |
||||
|
// 以二进制流方式读取得到整份excel表格对象
|
||||
|
const workbook = XLSX.read(result, { |
||||
|
type: "binary", |
||||
|
cellDates: true,//设为true,将天数的时间戳转为时间格式
|
||||
|
codepage: 936//解决了乱码问题
|
||||
|
}); |
||||
|
let data = []; // 存储获取到的数据
|
||||
|
// 遍历每张工作表进行读取(这里默认只读取第一张表)
|
||||
|
for (const sheet in workbook.Sheets) { |
||||
|
if (workbook.Sheets.hasOwnProperty(sheet)) { |
||||
|
data = data.concat(XLSX.utils.sheet_to_json(workbook.Sheets[sheet])); |
||||
|
} |
||||
|
} |
||||
|
resolve(data);//导出数据
|
||||
|
} catch (e) { |
||||
|
reject("失败"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
request.send(); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const judgeProvinces = (provinces) => { |
||||
|
let noMark = 0; |
||||
|
provinces.split('、')?.map(p => { |
||||
|
if (allProvinces.indexOf(p) == -1) { |
||||
|
noMark++ |
||||
|
} |
||||
|
}) |
||||
|
return !noMark; |
||||
|
} |
||||
|
|
||||
|
const judgeCities = (provinces, cities) => { |
||||
|
let nowCities = []; |
||||
|
cityData.filter(cd => provinces.split('、').indexOf(cd.name) != -1).map(d => { |
||||
|
d.children?.map(c => { |
||||
|
nowCities.push(c.name) |
||||
|
}) |
||||
|
}) |
||||
|
let noMark = 0; |
||||
|
cities.split('、')?.map(p => { |
||||
|
if (nowCities.indexOf(p) == -1) { |
||||
|
noMark++ |
||||
|
} |
||||
|
}) |
||||
|
return !noMark; |
||||
|
} |
||||
|
|
||||
|
const judgeNull = (value) => { |
||||
|
return value ? String(value).trim().replace(/\s*/g, "") : null; |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Modal |
||||
|
title="导入" visible={true} |
||||
|
onOk={confirm} |
||||
|
confirmLoading={loading} |
||||
|
onCancel={() => { |
||||
|
setMsg('') |
||||
|
setLoading(false) |
||||
|
setPostData([]) |
||||
|
onCancel() |
||||
|
}} |
||||
|
> |
||||
|
<div style={{ borderBottom: '1px solid #DCDEE0', margin: '0px -24px' }}></div> |
||||
|
<Form> |
||||
|
<Form.Upload |
||||
|
label={'销售人员信息'} labelPosition='left' |
||||
|
action={'/'} accept={fileLimit} |
||||
|
maxSize={200} limit={1} |
||||
|
onRemove={(currentFile, fileList, fileItem) => { |
||||
|
setMsg(''); |
||||
|
setPostData([]); |
||||
|
}} |
||||
|
customRequest={(data) => { |
||||
|
const { file, onSuccess, onError } = data |
||||
|
getFileBlob(file.url).then(res => { |
||||
|
const error = (msg) => { |
||||
|
setMsg(msg) |
||||
|
onError({ message: msg }) |
||||
|
} |
||||
|
if (res.length > 1000) { |
||||
|
error('一次性上传数据行数应小于1000行,请分批上传') |
||||
|
return |
||||
|
} |
||||
|
if (!res.length) { |
||||
|
error('请填写至少一行数据') |
||||
|
return |
||||
|
} |
||||
|
let postData = []; |
||||
|
const zmsz = /^[A-Za-z0-9]+$/;//字母数字组合
|
||||
|
for (let i = 0; i < res.length; i++) { |
||||
|
let d = res[i]; |
||||
|
let number = judgeNull(d['员工编号']); |
||||
|
let name = judgeNull(d['姓名']); |
||||
|
let provinces = judgeNull(d['销售区域(省/直辖市)']); |
||||
|
let cities = judgeNull(d['销售区域(市)']); |
||||
|
if (!number) {//人员编号不为空,唯一,字母和数字
|
||||
|
error(`第${i + 2}行人员编号为空,请填写`) |
||||
|
return |
||||
|
} |
||||
|
let rzExist = rzMembers.find(m => m.userCode == number); |
||||
|
if (!rzExist) { |
||||
|
error(`第${i + 2}行的人员编号无对应的员工信息`) |
||||
|
return |
||||
|
} |
||||
|
if (postData.some(p => p.number == number)) {//人员编号 唯一
|
||||
|
error(`第${i + 2}行人员编号重复,请更改后重新上传`) |
||||
|
return |
||||
|
} |
||||
|
if (!zmsz.test(number)) { |
||||
|
error(`第${i + 2}行人员编号错误,请填写字母和数字的组合`) |
||||
|
return |
||||
|
} |
||||
|
if (!name) {//姓名必填
|
||||
|
error(`第${i + 2}行姓名为空,请填写`) |
||||
|
return |
||||
|
} |
||||
|
if (!provinces) {//销售区域(省/直辖市)必填
|
||||
|
error(`第${i + 2}行销售区域(省/直辖市)为空,请填写`) |
||||
|
return |
||||
|
} |
||||
|
let pValid = judgeProvinces(provinces); |
||||
|
if (!pValid) { |
||||
|
error(`第${i + 2}行销售区域(省/直辖市)错误`) |
||||
|
return |
||||
|
} |
||||
|
let cValid = judgeCities(provinces, cities); |
||||
|
if (!cValid) { |
||||
|
error(`第${i + 2}行销售区域(市)错误`) |
||||
|
return |
||||
|
} |
||||
|
postData.push({//人员编号 待办todotodo
|
||||
|
pepUserId: rzExist.pepUserId, name, |
||||
|
provinces: provinces.split('、'), cities: cities.split('、'), |
||||
|
del: false |
||||
|
}) |
||||
|
} |
||||
|
setPostData(postData) |
||||
|
let msg = '文件解析完成,点击确定按钮上传保存!' |
||||
|
setMsg(msg) |
||||
|
onSuccess({ message: msg }) |
||||
|
}) |
||||
|
}}> |
||||
|
<Button icon={<IconUpload />} theme="light"> |
||||
|
请选择文件 |
||||
|
</Button> |
||||
|
</Form.Upload> |
||||
|
<span>{msg}</span> |
||||
|
<div style={{ color: '#ccc' }}>最大不超过200M,导入文件需与 |
||||
|
<span onClick={() => download()} style={{ cursor: 'pointer', color: '#0066FF' }}>导入模板</span> |
||||
|
一致</div> |
||||
|
</Form> |
||||
|
</Modal > |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { auth, global, MemberList } = state; |
||||
|
return { |
||||
|
user: auth.user, |
||||
|
actions: global.actions, |
||||
|
rzMembers: MemberList.data?.rows || [], |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(ImportSalersModal); |
@ -0,0 +1,339 @@ |
|||||
|
import React, { useEffect, useRef, useState } from 'react'; |
||||
|
import { connect } from 'react-redux'; |
||||
|
import moment from 'moment' |
||||
|
import { Select, Input, Button, Popconfirm, Radio, Tooltip, Table, Pagination, Skeleton } from '@douyinfe/semi-ui'; |
||||
|
import SalesMemberModal from './salesMemberModal' |
||||
|
import ImportSalersModal from './importSalersModal' |
||||
|
import { IconSearch } from '@douyinfe/semi-icons'; |
||||
|
import { SkeletonScreen } from "$components"; |
||||
|
import '../../style.less' |
||||
|
|
||||
|
const PersonnelDistribution = (props) => { |
||||
|
const { dispatch, actions } = props |
||||
|
const { humanAffairs } = actions; |
||||
|
const [keywordTarget, setKeywordTarget] = useState('dep'); |
||||
|
const [keyword, setKeyword] = useState('');//搜索内容 |
||||
|
const [limits, setLimits] = useState()//每页实际条数 |
||||
|
const [query, setQuery] = useState({ limit: 10, page: 0 }); //页码信息 |
||||
|
const [modalV, setModalV] = useState(false); |
||||
|
const [dataToEdit, setDataToEdit] = useState(null); |
||||
|
const [tableData, setTableData] = useState([]); |
||||
|
const [importModalV, setImportModalV] = useState(false); |
||||
|
const page = useRef(query.page); |
||||
|
function seachValueChange(value) { |
||||
|
setKeyword(value) |
||||
|
} |
||||
|
|
||||
|
useEffect(() => { |
||||
|
dispatch(humanAffairs.getMemberList()) |
||||
|
getMemberSearchList() |
||||
|
}, []); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
getMemberSearchList()//查询人员列表 |
||||
|
}, [query]) |
||||
|
|
||||
|
function getMemberSearchList() { |
||||
|
let kt = keywordTarget == 'place' ? '' : keywordTarget; |
||||
|
let k = keywordTarget == 'place' ? '' : keyword; |
||||
|
let placeSearch = keywordTarget == 'place' ? keyword : ''; |
||||
|
dispatch(humanAffairs.getSalesList({ keywordTarget: kt, keyword: k, placeSearch, ...query })).then(r => { |
||||
|
if (r.success) { |
||||
|
setTableData(r.payload?.data?.rows); |
||||
|
setLimits(r.payload?.data?.count) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function handleRow(record, index) {// 给偶数行设置斑马纹 |
||||
|
if (index % 2 === 0) { |
||||
|
return { |
||||
|
style: { |
||||
|
background: '#FAFCFF', |
||||
|
} |
||||
|
}; |
||||
|
} else { |
||||
|
return {}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const closeAndFetch = () => { |
||||
|
setModalV(false) |
||||
|
getMemberSearchList(); |
||||
|
} |
||||
|
|
||||
|
const starHeader = (header) => { |
||||
|
return <div> |
||||
|
<img src="/assets/images/hrImg/V.png" style={{ width: 14, height: 14 }} /> {header} |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
const getMultis = (arrStr) => {//默认展示2个 |
||||
|
return <div style={{ display: 'flex' }}> |
||||
|
{ |
||||
|
arrStr.map((ite, idx) => { |
||||
|
return ( |
||||
|
<div key={idx} style={{ display: 'flex' }}> |
||||
|
{idx < 2 ? |
||||
|
<div style={{ padding: '0px 4px 1px 4px', color: '#FFF', fontSize: 12, background: 'rgba(0,90,189,0.8)', borderRadius: 2, marginRight: 4 }}> |
||||
|
{ite} |
||||
|
</div> : '' |
||||
|
} |
||||
|
{ |
||||
|
arrStr.length > 2 && idx == 2 ? |
||||
|
<Tooltip content={arrStr.join(',')} trigger="click" style={{ lineHeight: 2 }}> |
||||
|
<div style={{ padding: '0px 4px 1px 4px ', color: 'rgba(0,90,189,0.8)', fontSize: 12, marginRight: 4, cursor: "pointer" }}> |
||||
|
+{arrStr.length - 2} |
||||
|
</div> |
||||
|
</Tooltip> |
||||
|
: '' |
||||
|
} |
||||
|
</div> |
||||
|
) |
||||
|
}) |
||||
|
} |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
const columns = [{ |
||||
|
title: '序号', |
||||
|
dataIndex: 'id', |
||||
|
key: 'id', |
||||
|
width: '5%', |
||||
|
render: (text, record, index) => index + 1 |
||||
|
}, { |
||||
|
title: starHeader('姓名'), |
||||
|
dataIndex: 'name', |
||||
|
key: 'name', |
||||
|
width: '5%' |
||||
|
}, |
||||
|
{ |
||||
|
title: starHeader('部门名称'), |
||||
|
dataIndex: 'department', |
||||
|
key: 'department', |
||||
|
width: '15%', |
||||
|
render: (text, r, index) => { |
||||
|
let arrStr = text.map(t => t.name); |
||||
|
return getMultis(arrStr); |
||||
|
} |
||||
|
}, { |
||||
|
title: '销售区域(省/直辖市)', |
||||
|
dataIndex: 'provinces', |
||||
|
key: 'provinces', |
||||
|
width: '15%', |
||||
|
render: (text, record, index) => { |
||||
|
return getMultis(text); |
||||
|
} |
||||
|
}, { |
||||
|
title: '销售区域(市)', |
||||
|
dataIndex: 'cities', |
||||
|
key: 'cities', |
||||
|
width: '15%', |
||||
|
render: (text, record, index) => { |
||||
|
return getMultis(text); |
||||
|
} |
||||
|
}, { |
||||
|
title: starHeader('岗位'), |
||||
|
dataIndex: 'post', |
||||
|
key: 'post', |
||||
|
width: '10%', |
||||
|
}, { |
||||
|
title: starHeader('入职时间'), |
||||
|
dataIndex: 'hireDate', |
||||
|
key: 'hireDate', |
||||
|
width: '10%', |
||||
|
render: (text, record) => <span>{text || '-'}</span> |
||||
|
}, { |
||||
|
title: starHeader('转正时间'), |
||||
|
dataIndex: 'regularDate', |
||||
|
key: 'regularDate', |
||||
|
width: '10%', |
||||
|
render: (text, record) => <span>{text || '-'}</span> |
||||
|
}, { |
||||
|
title: starHeader('工龄'), |
||||
|
dataIndex: 'workYears', |
||||
|
key: 'workYears', |
||||
|
width: '5%', |
||||
|
render: (_, r, index) => { |
||||
|
return (r.hireDate ? <span style={{ color: '#1890FF' }}>{String(moment(new Date()).diff(r.hireDate, 'years', true)).substring(0, 3) + '年'}</span> : '-') |
||||
|
}, |
||||
|
}, { |
||||
|
title: '操作', |
||||
|
dataIndex: 'action', |
||||
|
width: '10%', |
||||
|
render: (text, record) => { |
||||
|
return <div> |
||||
|
<span style={{ color: '#1890FF', cursor: 'pointer' }} onClick={() => onEdit(record)}>编辑</span> |
||||
|
<Popconfirm |
||||
|
title='提示' content="确认删除该销售人员信息?" position='topLeft' |
||||
|
onConfirm={() => confirmDelete(record.pepUserId)} style={{ width: 330 }} |
||||
|
> <span style={{ color: '#1890FF', cursor: 'pointer' }}>删除</span></Popconfirm> |
||||
|
</div> |
||||
|
} |
||||
|
}]; |
||||
|
|
||||
|
const onEdit = (data) => { |
||||
|
setModalV(true); |
||||
|
setDataToEdit(data); |
||||
|
} |
||||
|
|
||||
|
const confirmDelete = (pepUserId) => { |
||||
|
dispatch(humanAffairs.delSalesMember({ pepUserId, msg: '删除销售人员信息' })).then(res => { |
||||
|
if (res.success) { |
||||
|
getMemberSearchList(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return (<div style={{ padding: '0px 12px' }}> |
||||
|
<div style={{ display: 'flex' }}> |
||||
|
<div style={{ color: 'rgba(0,0,0,0.45)', fontSize: 14 }}>招聘</div> |
||||
|
<div style={{ color: 'rgba(0,0,0,0.45)', fontSize: 14, margin: '0px 8px' }}>/</div> |
||||
|
<div style={{ color: 'rgba(0,0,0,0.45)', fontSize: 14 }}>销售统计</div> |
||||
|
<div style={{ color: '#033C9A', fontSize: 14, margin: '0px 8px' }}>/</div> |
||||
|
<div style={{ color: '#033C9A', fontSize: 14 }}>销售人员分布</div> |
||||
|
</div> |
||||
|
<div style={{ background: '#FFFFFF', boxShadow: '0px 0px 12px 2px rgba(220,222,224,0.2)', borderRadius: 2, padding: '20px 0px 20px 19px ', marginTop: 12 }}> |
||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> |
||||
|
<div style={{ display: 'flex', alignItems: 'baseline' }}> |
||||
|
<div style={{ width: 0, height: 20, borderLeft: '3px solid #0F7EFB', borderTop: '3px solid transparent', borderBottom: '3px solid transparent' }}></div> |
||||
|
<div style={{ fontFamily: "YouSheBiaoTiHei", fontSize: 24, color: '#033C9A', marginLeft: 8 }}>销售人员分布</div> |
||||
|
<div style={{ marginLeft: 6, fontSize: 12, color: '#969799', fontFamily: "DINExp", }}>DISTRIBUTION OF SALES PERSONNEL</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div style={{ margin: '18px 0px' }}> |
||||
|
<div style={{ display: 'flex', margin: '16px 0px', justifyContent: 'space-between' }}> |
||||
|
<div style={{ display: 'flex' }}> |
||||
|
<div style={{ padding: '6px 20px', background: '#0F7EFB', color: '#FFFFFF', fontSize: 14, cursor: "pointer", marginRight: 18 }} |
||||
|
onClick={() => { |
||||
|
setModalV(true); |
||||
|
setDataToEdit(null); |
||||
|
}}> |
||||
|
新增 |
||||
|
</div> |
||||
|
<div> |
||||
|
<Select value={keywordTarget} onChange={setKeywordTarget} style={{ width: 100 }} > |
||||
|
<Select.Option value='dep'>部门</Select.Option> |
||||
|
<Select.Option value='place'>地区</Select.Option> |
||||
|
</Select> |
||||
|
</div> |
||||
|
<div style={{ margin: '0px 18px' }}> |
||||
|
<Input suffix={<IconSearch />} |
||||
|
showClear |
||||
|
placeholder='请输入关键词搜索' |
||||
|
value={keyword} |
||||
|
style={{ width: 346 }} |
||||
|
onChange={seachValueChange}> |
||||
|
</Input> |
||||
|
</div> |
||||
|
<Button theme='solid' type='primary' style={{ width: 80, borderRadius: 2, height: 32, background: '#DBECFF', color: '#005ABD' }} |
||||
|
onClick={() => { |
||||
|
setQuery({ limit: 10, page: 0 }) |
||||
|
}}>查询</Button> |
||||
|
</div> |
||||
|
<div style={{ display: 'flex', marginRight: 20 }}> |
||||
|
<div style={{ padding: '6px 20px', background: '#0F7EFB', color: '#FFFFFF', fontSize: 14, cursor: "pointer" }} |
||||
|
onClick={() => { setImportModalV(true); }}> |
||||
|
导入 |
||||
|
</div> |
||||
|
<div style={{ padding: '6px 20px', background: '#00BA85', color: '#FFFFFF', fontSize: 14, marginLeft: 18 }}> |
||||
|
导出 |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div style={{ border: '1px solid #C7E1FF', background: '#F4F8FF', borderRadius: 2, height: 32, width: 669, padding: '8px 0px 7px 12px', display: 'flex', alignItems: 'center', color: '#0F7EFB', fontSize: 12 }}> |
||||
|
<img src="/assets/images/hrImg/!.png" alt="" style={{ width: 14, height: 14, marginRight: 8 }} /> |
||||
|
表格中带有认证标识" |
||||
|
<img src="/assets/images/hrImg/V.png" alt="" style={{ width: 14, height: 14 }} /> |
||||
|
"信息的为系统基础数据,来源于项企PEP、钉钉等系统,其他数据均为导入或自定义数据 |
||||
|
</div> |
||||
|
|
||||
|
<div style={{ marginTop: 20 }}> |
||||
|
<Skeleton |
||||
|
// loading={loading} |
||||
|
loading={false} |
||||
|
active={true} |
||||
|
placeholder={SkeletonScreen()} |
||||
|
> |
||||
|
<Table |
||||
|
columns={columns} |
||||
|
dataSource={tableData} |
||||
|
bordered={false} |
||||
|
empty="暂无数据" |
||||
|
pagination={false} |
||||
|
onChange={({ sorter }) => { |
||||
|
if (sorter.key == 'userCode') { |
||||
|
if (sorter.sortOrder == 'descend') { |
||||
|
setOrder({ orderBy: 'code', orderDirection: 'DESC' }) |
||||
|
} else { |
||||
|
setOrder({ orderBy: 'code', orderDirection: 'ASC' }) |
||||
|
} |
||||
|
} else if (sorter.key == 'age') { |
||||
|
if (sorter.sortOrder == 'descend') { |
||||
|
setOrder({ orderBy: 'age', orderDirection: 'DESC' }) |
||||
|
} else { |
||||
|
setOrder({ orderBy: 'age', orderDirection: 'ASC' }) |
||||
|
} |
||||
|
} else { |
||||
|
if (sorter.sortOrder == 'descend') { |
||||
|
setOrder({ orderBy: 'hiredate', orderDirection: 'DESC' }) |
||||
|
} else { |
||||
|
setOrder({ orderBy: 'hiredate', orderDirection: 'ASC' }) |
||||
|
} |
||||
|
} |
||||
|
}} |
||||
|
onRow={handleRow} |
||||
|
scroll={scroll} |
||||
|
/> |
||||
|
</Skeleton> |
||||
|
<div style={{ |
||||
|
display: "flex", |
||||
|
justifyContent: "space-between", |
||||
|
padding: "20px 20px", |
||||
|
}}> |
||||
|
<div style={{ display: 'flex', }}> |
||||
|
<span style={{ lineHeight: "30px", fontSize: 13, color: 'rgba(0,90,189,0.8)' }}> |
||||
|
共{limits}条信息 |
||||
|
</span> |
||||
|
<Pagination |
||||
|
total={limits} |
||||
|
showSizeChanger |
||||
|
currentPage={query.page + 1} |
||||
|
pageSizeOpts={[10, 20, 30, 40]} |
||||
|
onChange={(currentPage, pageSize) => { |
||||
|
setQuery({ limit: pageSize, page: currentPage - 1 }); |
||||
|
page.current = currentPage - 1 |
||||
|
}} |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
{ |
||||
|
modalV ? <SalesMemberModal |
||||
|
dataToEdit={dataToEdit} getMultis={getMultis} |
||||
|
close={() => closeAndFetch()} |
||||
|
onCancel={() => setModalV(false)} /> : '' |
||||
|
} |
||||
|
{ |
||||
|
importModalV ? <ImportSalersModal |
||||
|
onCancel={() => { |
||||
|
setImportModalV(false); |
||||
|
getMemberSearchList(); |
||||
|
}} /> : '' |
||||
|
} |
||||
|
</div>) |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { auth, global, SalesMemberList } = state; |
||||
|
return { |
||||
|
user: auth.user, |
||||
|
actions: global.actions, |
||||
|
salesMemberList: SalesMemberList.data |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(PersonnelDistribution); |
@ -0,0 +1,244 @@ |
|||||
|
import React, { useEffect, useRef, useState } from 'react'; |
||||
|
import moment from 'moment'; |
||||
|
import { connect } from "react-redux"; |
||||
|
import { Select, Modal, Form, Notification } from "@douyinfe/semi-ui"; |
||||
|
import cityData from '../../components/city.json'; |
||||
|
const SalesMemberModal = (props) => { |
||||
|
const { dispatch, actions, user, meetingList, onConfirm, getMultis, onCancel, close, rzMembers, dataToEdit } = props; |
||||
|
const { humanAffairs } = actions; |
||||
|
const form = useRef();//表单
|
||||
|
const [options, setOptions] = useState([]); |
||||
|
const [cityOptions, setCityOptions] = useState([]); |
||||
|
const [peoplePro, setPeoplePro] = useState({}); //人员信息
|
||||
|
//初始化
|
||||
|
useEffect(() => { |
||||
|
let optionItems = cityData.map(m => { |
||||
|
return <Select.Option value={m.name} key={m.code}> |
||||
|
{m.name} |
||||
|
</Select.Option> |
||||
|
}) |
||||
|
setOptions(optionItems); |
||||
|
if (dataToEdit) { |
||||
|
setPeoplePro(dataToEdit); |
||||
|
onChange(dataToEdit.provinces);//市options
|
||||
|
} |
||||
|
}, []); |
||||
|
|
||||
|
const onChange = (value) => { |
||||
|
let cityOptions = [], citiesRange = []; |
||||
|
cityData.filter(cd => value.indexOf(cd.name) !== -1).map(d => { |
||||
|
d.children?.map(c => { |
||||
|
cityOptions.push(<Select.Option value={c.name} key={c.code}> |
||||
|
{c.name} |
||||
|
</Select.Option>) |
||||
|
citiesRange.push(c.name) |
||||
|
}) |
||||
|
}) |
||||
|
setCityOptions(cityOptions) |
||||
|
|
||||
|
let citiesValue = form?.current?.getValues()?.cities || []; |
||||
|
if (citiesValue.length) { |
||||
|
let newCities = []; |
||||
|
citiesValue?.map(cv => { |
||||
|
if (citiesRange.indexOf(cv) != -1) { |
||||
|
newCities.push(cv); |
||||
|
} |
||||
|
}) |
||||
|
form.current.setValue('cities', newCities) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleOk() { |
||||
|
form.current.validate().then((values) => { |
||||
|
if (peoplePro?.userCode) { |
||||
|
if (values.userCode == peoplePro.userCode) { |
||||
|
if (dataToEdit) { |
||||
|
dispatch(humanAffairs.editSalesMember({ pepUserId: peoplePro.pepUserId, msg: '编辑销售人员信息', ...values })).then((res) => { |
||||
|
if (res.success) { |
||||
|
close(); |
||||
|
} |
||||
|
}) |
||||
|
} else { |
||||
|
dispatch(humanAffairs.postSalesMember({ pepUserId: peoplePro.pepUserId, msg: '新增销售人员', ...values })).then((res) => { |
||||
|
if (res.success) { |
||||
|
close(); |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} else { |
||||
|
Notification.error({ |
||||
|
content: '你填写的员工编号无对应的人员信息', |
||||
|
duration: 2, |
||||
|
}) |
||||
|
} |
||||
|
} else { |
||||
|
Notification.error({ |
||||
|
content: '你填写的员工编号无对应的人员信息', |
||||
|
duration: 2, |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const memberSeach = (id) => {//搜索项企用户
|
||||
|
dispatch(humanAffairs.getMemberSearch({ code: id })).then((res) => {//搜索项企用户
|
||||
|
if (res.success) { |
||||
|
if (res.payload.data.length) { |
||||
|
let user = res.payload.data[0] |
||||
|
let exist = rzMembers.find(m => m.pepUserId == user.pepUserId);//人员档案里面需要有
|
||||
|
if (exist) { |
||||
|
let item = { |
||||
|
pepUserId: user.pepUserId, |
||||
|
name: exist.userName, |
||||
|
department: exist.departmrnt, |
||||
|
hireDate: exist.hiredate, |
||||
|
regularDate: exist.regularDate, |
||||
|
userCode: user.userCode, |
||||
|
post: '岗位todo' |
||||
|
} |
||||
|
setPeoplePro(item) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const renderSimpleInfo = () => { |
||||
|
let arrStr = peoplePro?.department?.map(t => t.name) || []; |
||||
|
return <div> |
||||
|
{ |
||||
|
peoplePro?.name ? ( |
||||
|
<div style={{ width: 749, height: 95, background: '#F4F5FC', border: '1px solid rgba(0, 90, 189, 0.2)', marginTop: 8, padding: '20px 83px 20px 50px' }}> |
||||
|
<div style={{ display: 'flex' }}> |
||||
|
{renderPeopleItem('姓名:', peoplePro.name)} |
||||
|
<div style={{ display: 'flex', flexBasis: '50%' }}> |
||||
|
<div style={{ width: 16, height: 16, marginRight: 9 }}> |
||||
|
<img src="/assets/images/hrImg/department.png" alt="" style={{ width: '100%', height: '100%' }} /> |
||||
|
</div> |
||||
|
<div style={{ color: 'rgba(0,0,0,0.6)', fontSize: 12 }}> |
||||
|
所属部门: |
||||
|
</div> |
||||
|
{getMultis(arrStr)} |
||||
|
</div> |
||||
|
</div> |
||||
|
<div style={{ display: 'flex' }}> |
||||
|
{renderPeopleItem('入职时间:', peoplePro.hireDate)} |
||||
|
{renderPeopleItem('岗位:', peoplePro.post)} |
||||
|
</div> <div style={{ display: 'flex' }}> |
||||
|
{renderPeopleItem('转正时间:', peoplePro.regularDate)} |
||||
|
{renderPeopleItem('工龄:', peoplePro.hireDate ? String(moment(new Date()).diff(peoplePro.hireDate, 'years', true)).substring(0, 3) + '年' : '-')} |
||||
|
</div> |
||||
|
</div> |
||||
|
) : ('') |
||||
|
} |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
const renderPeopleItem = (label, value) => { |
||||
|
return <div style={{ display: 'flex', flexBasis: '50%' }}> |
||||
|
<div style={{ width: 16, height: 16, marginRight: 9 }}> |
||||
|
<img src="/assets/images/hrImg/department.png" style={{ width: '100%', height: '100%' }} /> |
||||
|
</div> |
||||
|
<div style={{ color: 'rgba(0,0,0,0.6)', fontSize: 12 }}> |
||||
|
{label} |
||||
|
</div> |
||||
|
<div style={{ color: '#4A4A4A', fontSize: 12 }}> |
||||
|
{value} |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
// const handleDeselect = (value) => {//删除
|
||||
|
// let ranges = cityData.find(td => td.name == value)?.children || []
|
||||
|
// if (ranges) {
|
||||
|
// let formList = form.current.getValues().cities;
|
||||
|
// }
|
||||
|
|
||||
|
// }
|
||||
|
|
||||
|
const onClear = () => { |
||||
|
form.current.setValue('cities', []) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Modal title={dataToEdit?.pepUserId ? '修改销售人员信息' : '新增销售人员'} |
||||
|
visible={true} |
||||
|
destroyOnClose |
||||
|
okText='保存' width={800} |
||||
|
onOk={handleOk} |
||||
|
onCancel={onCancel}> |
||||
|
<Form getFormApi={(formApi) => (form.current = formApi)} |
||||
|
labelPosition={'left'} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }}> |
||||
|
<Form.Input |
||||
|
field="userCode" |
||||
|
label='人员编号' |
||||
|
initValue={dataToEdit?.userCode || ""} |
||||
|
placeholder="请输入人员编号" |
||||
|
showClear |
||||
|
style={{ width: '100%' }} |
||||
|
rules={[{ required: true, message: "请输入人员编号" }]} |
||||
|
addonAfter={<div style={{ margin: '0px 12px', color: '#005ABD', cursor: "pointer", fontSize: 14 }} onClick={() => { |
||||
|
let formList = form.current.getValues() |
||||
|
if (formList.userCode) { |
||||
|
memberSeach(formList.userCode) |
||||
|
} |
||||
|
}}> |
||||
|
搜索 |
||||
|
</div>} /> |
||||
|
|
||||
|
{peoplePro ? renderSimpleInfo() : ''} |
||||
|
<Form.Select |
||||
|
initValue={dataToEdit?.provinces || []} |
||||
|
label="销售区域(省/直辖市)" |
||||
|
field='provinces' |
||||
|
showClear |
||||
|
rules={[{ required: true, message: '请选择销售区域(省/直辖市)' }]} |
||||
|
placeholder='请选择销售区域(省/直辖市)' |
||||
|
multiple filter |
||||
|
style={{ width: '100%' }} |
||||
|
// optionFilterProp='children'
|
||||
|
// getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||
|
// filterOption={(input, option) => option.props.children
|
||||
|
// .toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
|
// value={selectedKeys || []}
|
||||
|
onClear={() => onClear()} |
||||
|
onChange={value => onChange(value)} |
||||
|
//onDeselect={value => handleDeselect(value)}
|
||||
|
maxTagCount={5} |
||||
|
> |
||||
|
{options} |
||||
|
</Form.Select> |
||||
|
<Form.Select |
||||
|
initValue={dataToEdit?.cities || []} |
||||
|
label="销售区域(市)" |
||||
|
field='cities' |
||||
|
showClear |
||||
|
placeholder='请选择销售区域(市)' |
||||
|
multiple filter |
||||
|
style={{ width: '100%' }} |
||||
|
// optionFilterProp='children'
|
||||
|
// getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||
|
// filterOption={(input, option) => option.props.children
|
||||
|
// .toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
|
// value={selectedKeys || []}
|
||||
|
//onDeselect={value => handleDeselect(value)}
|
||||
|
maxTagCount={5} |
||||
|
> |
||||
|
{cityOptions} |
||||
|
</Form.Select> |
||||
|
</Form> |
||||
|
</Modal> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
function mapStateToProps(state) { |
||||
|
const { auth, global, MemberList } = state; |
||||
|
return { |
||||
|
user: auth.user, |
||||
|
actions: global.actions, |
||||
|
apiRoot: global.apiRoot, |
||||
|
rzMembers: MemberList.data?.rows || [], |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default connect(mapStateToProps)(SalesMemberModal); |
Loading…
Reference in new issue