巴林闲侠 2 years ago
parent
commit
48caac405a
  1. 41
      api/app/lib/controllers/member/index.js
  2. 336
      api/app/lib/controllers/salesDistribution/index.js
  3. 71
      api/app/lib/models/sales_distribution.js
  4. 24
      api/app/lib/routes/salesDistribution/index.js
  5. 8
      api/app/lib/utils/constant.js
  6. 20
      api/app/lib/utils/member.js
  7. 2
      api/app/lib/utils/xlsxDownload.js
  8. 11
      doc/scripts/0.0.4/schema/1.sales_distribution.sql
  9. 4
      doc/scripts/PEP V3.0.0/schema/1.sales_distribution_modify.sql
  10. 2
      web/client/index.ejs
  11. 2
      web/client/index.html
  12. 5
      web/client/src/layout/components/header/contant.js
  13. 5
      web/client/src/sections/humanAffairs/actions/index.js
  14. 89
      web/client/src/sections/humanAffairs/actions/salesDistribution.js
  15. 27
      web/client/src/sections/humanAffairs/components/personnelModal.jsx
  16. 105
      web/client/src/sections/humanAffairs/containers/employeeInformation.jsx
  17. 2
      web/client/src/sections/humanAffairs/containers/import-members-modal.js
  18. 3
      web/client/src/sections/humanAffairs/containers/index.js
  19. 27
      web/client/src/sections/humanAffairs/containers/personnelFiles.jsx
  20. 58
      web/client/src/sections/humanAffairs/containers/personnelFilesDetail.jsx
  21. 272
      web/client/src/sections/humanAffairs/containers/salersDistribution/importSalersModal.js
  22. 350
      web/client/src/sections/humanAffairs/containers/salersDistribution/personnelDistribution.jsx
  23. 263
      web/client/src/sections/humanAffairs/containers/salersDistribution/salesMemberModal.js
  24. 8
      web/client/src/sections/humanAffairs/nav-item.jsx
  25. 12
      web/client/src/sections/humanAffairs/routes.js
  26. 3
      web/client/src/utils/index.js
  27. 8
      web/client/src/utils/userAttribute.js
  28. 6
      web/client/src/utils/webapi.js

41
api/app/lib/controllers/member/index.js

@ -104,6 +104,7 @@ async function searchPepMember (ctx) {
SELECT
user.id AS pepUserId,
user.people_code AS userCode,
basicdata_post.name AS userPost,
user.name AS userName,
role.name AS roleName,
role.id AS roleId,
@ -115,6 +116,8 @@ async function searchPepMember (ctx) {
ON user_role.user = user.id
LEFT JOIN role
ON role.id = user_role.role
LEFT JOIN basicdata_post
ON basicdata_post.id = user.post
LEFT JOIN department_user
ON department_user.user = user.id
LEFT JOIN department
@ -146,6 +149,7 @@ async function searchPepMember (ctx) {
pepUserId: u.pepUserId,
name: u.userName,
userCode: u.userCode,
userPost: u.userPost,
departmrnt: u.depId ? [{
id: u.depId,
name: u.depName
@ -153,7 +157,7 @@ async function searchPepMember (ctx) {
role: u.roleId ? [{
id: u.roleId,
name: u.roleName
}] : [],
}] : []
})
}
})
@ -181,7 +185,13 @@ async function del (ctx) {
pepUserId,
}
})
await models.SalesDistribution.update({//顺便把销售人员删了
del: true,
}, {
where: {
pepUserId,
}
})
ctx.status = 204;
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
@ -482,7 +492,7 @@ async function exportData (ctx) {
try {
const { models } = ctx.fs.dc;
const { clickHouse, opts: { qiniu } } = ctx.app.fs
const { simpleExcelDown, memberList, packageUserData } = ctx.app.fs.utils
const { simpleExcelDown, memberList, packageUserData, UserAttribute } = ctx.app.fs.utils
const {
keywordTarget, keyword, limit, page, state, keys = '',
hiredateStart, hiredateEnd, marital, native, workPlace,
@ -507,6 +517,27 @@ async function exportData (ctx) {
}, {
title: '姓名',
key: 'userName',
}, {
title: '所属部门',
key: 'departmrnt',
}, {
title: '职位',
key: 'userJob',
}, {
title: '岗位',
key: 'userPost',
}, {
title: '在职状态',
key: 'userActiveStatus',
}, {
title: '绩点',
key: 'point',
}, {
title: '归属机构',
key: 'userOrganization',
}, {
title: '技术职级等级',
key: 'technicalGrade',
}]
let header = [].concat(preHeader)
for (let k in tableAttributes) {
@ -570,6 +601,10 @@ async function exportData (ctx) {
d.departmrnt = d.departmrnt.map(dep => dep.name).join('、')
d.role = d.role.map(r => r.name).join('、')
d.userJob = d.userJob? UserAttribute.jobDataSource[d.userJob - 1] : '';
d.userActiveStatus = d.userActiveStatus? UserAttribute.activeStatusDataSource[d.userActiveStatus - 1] : '';
d.userOrganization = d.userOrganization? UserAttribute.organizationDataSource[d.userOrganization - 1] : '';
d.idPhoto ? d.idPhoto = qiniu.domain + '/' + d.idPhoto : ''
d.vitae ? d.vitae = qiniu.domain + '/' + d.vitae : ''

336
api/app/lib/controllers/salesDistribution/index.js

@ -0,0 +1,336 @@
'use strict';
const moment = require('moment')
const fs = require('fs');
async function salesList(ctx) {
try {
const { memberList, packageUserData } = ctx.app.fs.utils
const {
keywordTarget, keyword, limit, page, state,
hiredateStart, hiredateEnd, marital, native, workPlace,
orderBy, orderDirection, placeSearch
} = ctx.query
const userRes = await memberList({
keywordTarget, keyword, limit: '', page: '', state,
hiredateStart, hiredateEnd, marital, native, workPlace,
orderBy, orderDirection,
nowAttendanceTime: true
})
let { packageUser: members } = await packageUserData(userRes, {
state: true,
})
const { models } = ctx.fs.dc;
let mIds = members.map(m => m.pepUserId);
let where = {
del: false,
pepUserId: { $in: mIds }
}
if (placeSearch) {
where.$or = [{
provinces: { $like: `%${placeSearch}%` }
}, {
cities: { $like: `%${placeSearch}%` }
}]
}
let res = await models.SalesDistribution.findAndCountAll({
where: where,
offset: Number(page) * Number(limit),
limit: Number(limit),
order: [['id', 'DESC']]
})
let rslt = []
res.rows.map(d => {
let info = members.find(m => m.pepUserId == d.dataValues.pepUserId);
let item = {
name: info.userName,
userCode: info.userCode,
post: info.userPost,
department: info.departmrnt,
hireDate: info.hiredate,//入职时间
regularDate: info.regularDate,//转正时间
...d.dataValues
}
rslt.push(item);
})
ctx.status = 200;
ctx.body = {
count: res.count,
rows: rslt
};
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : undefined
}
}
}
async function add(ctx) {
try {
const { models } = ctx.fs.dc;
const { pepUserId, provinces, cities, businessLines } = ctx.request.body
const existRes = await models.SalesDistribution.findOne({
where: { pepUserId }
})
if (existRes && !existRes.del) {
throw '当前销售人员信息已存在'
}
let storageData = { pepUserId, provinces, cities, businessLines, del: false }
if (existRes && existRes.del) {
await models.SalesDistribution.update(storageData, {
where: { pepUserId }
})
} else {
await models.SalesDistribution.create(storageData)
}
ctx.status = 204;
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : undefined
}
}
}
async function edit(ctx) {
try {
const { models } = ctx.fs.dc;
const { pepUserId, provinces, cities, businessLines } = ctx.request.body
const existRes = await models.SalesDistribution.findOne({
where: { pepUserId }
})
if (!existRes) {
throw '当前销售人员信息不存在'
}
let storageData = { pepUserId, provinces, cities, businessLines, del: false }
await models.SalesDistribution.update(storageData, {
where: {
pepUserId: pepUserId
}
})
ctx.status = 204;
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : undefined
}
}
}
async function del(ctx) {
try {
const { models } = ctx.fs.dc;
const { pepUserId } = ctx.query
await models.SalesDistribution.update({
del: true,
}, {
where: {
pepUserId,
}
})
ctx.status = 204;
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : undefined
}
}
}
// async function exportData(ctx) {
// try {
// const { models } = ctx.fs.dc;
// const { clickHouse, opts: { qiniu } } = ctx.app.fs
// const { simpleExcelDown, memberList, packageUserData } = ctx.app.fs.utils
// const {
// keywordTarget, keyword, limit, page, state, keys = '',
// hiredateStart, hiredateEnd, marital, native, workPlace,
// orderBy, orderDirection,
// } = ctx.query
// const userRes = await memberList({
// keywordTarget, keyword, limit, page, state,
// hiredateStart, hiredateEnd, marital, native, workPlace,
// orderBy, orderDirection,
// nowAttendanceTime: true
// })
// const tableAttributes = models['Member'].tableAttributes
// const optionKeys = keys.split(',')
// let { packageUser: exportD, pepUserIds } = await packageUserData(userRes)
// let preHeader = [{
// title: '员工编号',
// key: 'userCode',
// }, {
// title: '姓名',
// key: 'userName',
// }]
// let header = [].concat(preHeader)
// for (let k in tableAttributes) {
// const comment = tableAttributes[k].comment
// if (k != 'id' && k != 'pepUserId' && comment) {
// if ([].includes(k)) {
// // 截住不想导出的字段
// continue
// }
// header.push({
// title: comment || '-',
// key: k,
// // index: tableAttributes[k].index,
// })
// }
// }
// if (optionKeys.includes('overtimeStatistic')) {
// header = header.concat([{
// title: '累计加班次数',
// key: 'overTimeCount',
// }, {
// title: '累计加班总时长 / h',
// key: 'overTimeDuration',
// },])
// }
// if (optionKeys.includes('vacateStatistic')) {
// header = header.concat([{
// title: '累计请假次数',
// key: 'vacateCount',
// }, {
// title: '累计请假总时长 / h',
// key: 'vacateDuration',
// },])
// }
// // 查询累计加班次数及总时长
// const statisticOvertimeRes = await clickHouse.hr.query(`
// SELECT
// pep_user_id AS pepUserId,
// count(id) AS count,
// sum(duration) AS duration
// FROM
// overtime
// WHERE pep_user_id IN (${pepUserIds.join(',')})
// GROUP BY pep_user_id
// `).toPromise()
// const statisticVacateRes = await clickHouse.hr.query(`
// SELECT
// pep_user_id AS pepUserId,
// count(id) AS count,
// sum(duration) AS duration
// FROM
// vacate
// WHERE pep_user_id IN (${pepUserIds.join(',')})
// GROUP BY pep_user_id
// `).toPromise()
// exportD.forEach(d => {
// d.departmrnt = d.departmrnt.map(dep => dep.name).join('、')
// d.role = d.role.map(r => r.name).join('、')
// d.idPhoto ? d.idPhoto = qiniu.domain + '/' + d.idPhoto : ''
// d.vitae ? d.vitae = qiniu.domain + '/' + d.vitae : ''
// const corOverTime = statisticOvertimeRes.find(so => so.pepUserId == d.pepUserId)
// d.overTimeCount = corOverTime ? corOverTime.count : 0
// d.overTimeDuration = corOverTime ? (corOverTime.duration / 3600).toFixed(1) : 0
// const corVacate = statisticVacateRes.find(so => so.pepUserId == d.pepUserId)
// d.vacateCount = corVacate ? corVacate.count : 0
// d.vacateDuration = corVacate ? (corVacate.duration / 3600).toFixed(1) : 0
// })
// const fileName = `人员信息_${moment().format('YYYYMMDDHHmmss')}` + '.csv'
// const filePath = await simpleExcelDown({ data: exportD, header, fileName: fileName })
// const fileData = fs.readFileSync(filePath);
// ctx.status = 200;
// ctx.set('Content-Type', 'application/x-xls');
// ctx.set('Content-disposition', 'attachment; filename=' + encodeURI(fileName));
// ctx.body = fileData;
// } catch (error) {
// ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
// ctx.status = 400;
// ctx.body = {
// message: typeof error == 'string' ? error : undefined
// }
// }
// }
async function addSalesMemberBulk(ctx) {
let errorMsg = { message: '导入销售人员信息失败' };
const transaction = await ctx.fs.dc.orm.transaction();
try {
const models = ctx.fs.dc.models;
const data = ctx.request.body;
let addArr = [];
let editArr = [];
let list = await models.SalesDistribution.findAll({
attributes: ['pepUserId']
});
data.map(d => {
let exist = list.find(m => m.pepUserId == d.pepUserId);//项企的人员编号字段还没有
if (exist) {
editArr.push(d);
} else {
addArr.push(d);
}
})
//处理新增的
if (addArr.length) {
await models.SalesDistribution.bulkCreate(addArr);
}
//处理编辑的
if (editArr.length) {
for (let i in editArr) {
let { pepUserId, provinces, cities, businessLines, del = false } = editArr[i];
let dataToUpdate = {
provinces,
cities,
businessLines,
del
}
await models.SalesDistribution.update(dataToUpdate, { where: { pepUserId: pepUserId } });
}
}
await transaction.commit();
ctx.status = 204;
} catch (error) {
await transaction.rollback();
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = errorMsg;
}
}
module.exports = {
salesList,
add,
edit,
del,
//exportData,
addSalesMemberBulk,
}

71
api/app/lib/models/sales_distribution.js

@ -0,0 +1,71 @@
/* eslint-disable*/
'use strict';
module.exports = dc => {
const DataTypes = dc.ORM;
const sequelize = dc.orm;
const SalesDistribution = sequelize.define("salesDistribution", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: true,
field: "id",
autoIncrement: true,
unique: "sales_distribution_id_uindex"
},
pepUserId: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: false,
field: "pep_user_id",
autoIncrement: false
},
provinces: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: false,
field: "provinces",
autoIncrement: false
},
cities: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: null,
primaryKey: false,
field: "cities",
autoIncrement: false
},
del: {
type: DataTypes.BOOLEAN,
allowNull: true,
defaultValue: null,
comment: null,
primaryKey: false,
field: "del",
autoIncrement: false
},
businessLines: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: null,
primaryKey: false,
field: "business_lines",
autoIncrement: false
}
}, {
tableName: "sales_distribution",
comment: "",
indexes: []
});
dc.models.SalesDistribution = SalesDistribution;
return SalesDistribution;
};

24
api/app/lib/routes/salesDistribution/index.js

@ -0,0 +1,24 @@
'use strict';
const salesDistribution = require('../../controllers/salesDistribution');
module.exports = function (app, router, opts) {
app.fs.api.logAttr['GET/sales/member/list'] = { content: '查询销售人员列表', visible: true };
router.get('/sales/member/list', salesDistribution.salesList);
app.fs.api.logAttr['POST/sales/member/add'] = { content: '添加销售人员信息', visible: true };
router.post('/sales/member/add', salesDistribution.add);
app.fs.api.logAttr['PUT/sales/member/modify'] = { content: '编辑销售人员信息', visible: true };
router.put('/sales/member/modify', salesDistribution.edit);
app.fs.api.logAttr['DEL/sales/member/del'] = { content: '删除销售人员信息', visible: true };
router.del('/sales/member/del', salesDistribution.del);
app.fs.api.logAttr['POST/add/sales/members/bulk'] = { content: '导入销售人员信息', visible: true };
router.post('/add/sales/members/bulk', salesDistribution.addSalesMemberBulk);
// app.fs.api.logAttr['GET/sales/members/export'] = { content: '导出销售人员信息', visible: true };
// router.get('/sales/members/export', salesDistribution.exportData);
};

8
api/app/lib/utils/constant.js

@ -12,9 +12,15 @@ module.exports = function (app, opts) {
'发放加班补偿': '折算',
'调休': '调休'
}
const UserAttribute = {
jobDataSource: ['普通员工', '中层', '高层'],
activeStatusDataSource: ['在职', '离职', '特殊状态-特殊账号'],
organizationDataSource: ['江西飞尚科技有限公司', '江西飞尚工程质量检测有限公司',
'江西飞尚科技有限公司江苏分公司', '江西汇派科技有限公司'],
};
return {
dayType,
overtimeType,
UserAttribute
}
}

20
api/app/lib/utils/member.js

@ -200,13 +200,11 @@ module.exports = function (app, opts) {
`: ''}
WHERE
member.del = '0'
${keywordTarget == 'role' && keyword ? `
AND user.id IN (
SELECT user_role.user
FROM ${pepEmis}.user_role AS user_role
INNER JOIN ${pepEmis}.role AS role
ON role.id = user_role.role
AND role.name LIKE '%${keyword}%'
${keywordTarget == 'post' && keyword ? `
AND user.post IN (
SELECT basicDataPost.id
FROM ${pepEmis}.basicdata_post AS basicDataPost
where basicDataPost.name LIKE '%${keyword}%'
)
` : ''}
${keywordTarget == 'dep' && keyword ? `
@ -244,10 +242,14 @@ module.exports = function (app, opts) {
hrMember.*,
user.name AS userName,
user.people_code AS userCode,
basicDataPost.name AS userPost,
role.name AS roleName,
role.id AS roleId,
department.name AS depName,
department.id AS depId
department.id AS depId,
user.job AS userJob,
user.active_status AS userActiveStatus,
user.organization AS userOrganization
FROM (
SELECT
${orderBy == 'overtimeTakeRestSum'
@ -287,6 +289,8 @@ module.exports = function (app, opts) {
ON ${pepEmis}.user_role.user = user.id
LEFT JOIN ${pepEmis}.role AS role
ON ${pepEmis}.role.id = user_role.role
LEFT JOIN ${pepEmis}.basicdata_post AS basicDataPost
ON ${pepEmis}.basicdata_post.id = user.post
LEFT JOIN ${pepEmis}.department_user AS department_user
ON department_user.user = user.id
LEFT JOIN ${pepEmis}.department AS department

2
api/app/lib/utils/xlsxDownload.js

@ -59,7 +59,7 @@ module.exports = function (app, opts) {
indexCell.style = headerStyle
for (let h of header) {
const cell = row.addCell();
cell.value = data[i][h.key] || h.defaultValue || '';
cell.value = data[i][h.key] || h.defaultValue || '-';
cell.style = style
}
}

11
doc/scripts/0.0.4/schema/1.sales_distribution.sql

@ -0,0 +1,11 @@
CREATE TABLE sales_distribution
(
id serial PRIMARY KEY NOT NULL,
pep_user_id int NOT NULL,
provinces text NOT NULL,
cities text,
del boolean DEFAULT false NULL
);
CREATE UNIQUE INDEX sales_distribution_id_uindex ON sales_distribution (id);

4
doc/scripts/PEP V3.0.0/schema/1.sales_distribution_modify.sql

@ -0,0 +1,4 @@
alter table sales_distribution
add "business_lines" text;

2
web/client/index.ejs

@ -9,7 +9,7 @@
<!-- <link rel="shortcut icon" href="/assets/images/favicon.ico"> -->
<script src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/icons_20231_12.7f8fd1546294d369f0a85f7d68afb538.es5.js"></script>
<script src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/icons_20231_13.3484caadabc6f822796451f94a83fc64.es5.js"></script>
</head>

2
web/client/index.html

@ -7,7 +7,7 @@
<!-- <link rel="shortcut icon" href="/assets/images/favicon.ico"> -->
<script src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/icons_20231_12.7f8fd1546294d369f0a85f7d68afb538.es5.js"></script>
<script src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/icons_20231_13.3484caadabc6f822796451f94a83fc64.es5.js"></script>
</head>
<body>

5
web/client/src/layout/components/header/contant.js

@ -60,6 +60,11 @@ const headerItems = [{
itemKey: "recruitRecord",
text: "招聘记录",
to: "/humanAffairs/recruit/recruitRecord/appointmentRecords"
}, {
fatherKey: "recruit",
itemKey: "salesStatistics",
text: "销售统计",
to: "/humanAffairs/recruit/salesStatistics/personnelDistribution"
}]
}, {
itemKey: "employeeRelationship",

5
web/client/src/sections/humanAffairs/actions/index.js

@ -2,8 +2,9 @@
import * as personnelFiles from './personnelFiles'
import * as employeeInformation from './employeeInformation'
import * as salesDistribution from './salesDistribution'
export default {
...personnelFiles,
...employeeInformation
...employeeInformation,
...salesDistribution
}

89
web/client/src/sections/humanAffairs/actions/salesDistribution.js

@ -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: {},
});
}

27
web/client/src/sections/humanAffairs/components/personnelModal.jsx

@ -240,33 +240,10 @@ function pushModal (props) {
<img src="/assets/images/hrImg/position.png" alt="" style={{ width: '100%', height: '100%' }} />
</div>
<div style={{ color: 'rgba(0,0,0,0.6)', fontSize: 12 }}>
</div>
<div style={{ color: '#4A4A4A', fontSize: 12, display: 'flex', alignItems: 'center' }}>
{
peoplePro.role.map((ite, idx) => {
let roleArr = []
for (let i = 0; i < peoplePro.role.length; i++) {
roleArr.push(peoplePro.role[i].name)
}
return (
<div key={idx} style={{ display: 'flex', alignItems: 'center' }}>
{idx == 0 ?
(ite.name) : ('')
}
{
peoplePro.role.length > 1 && idx == 1 ? (
<Tooltip content={roleArr.join(',')} trigger="click" style={{ lineHeight: 2 }}>
<div style={{ color: 'rgba(0,90,189,0.8)', fontSize: 12, marginRight: 4, cursor: "pointer", }}>
+{peoplePro.role.length - 1}
</div>
</Tooltip>
) : ('')
}
</div>
)
})
}
{peoplePro.userPost || '-'}
</div>
</div>
</div>

105
web/client/src/sections/humanAffairs/containers/employeeInformation.jsx

@ -7,6 +7,7 @@ import '../style.less'
import { Setup } from "$components";
import moment from 'moment'
import { set } from 'nprogress';
import { UserAttribute } from '$utils'
const employeeInformation = (props) => {
const { dispatch, actions, history, user, loading, socket, xqMembers } = props
@ -33,7 +34,12 @@ const employeeInformation = (props) => {
list: [
{ name: "姓名", value: "userName" },
{ name: "所属部门", value: "departmrnt" },
{ name: "职位", value: "roleName" },
{ name: "职位", value: "userJob" },
{ name: "岗位", value: "userPost" },
{ name: "在职状态", value: "userActiveStatus" },
{ name: "绩点", value: "point" },
{ name: "所属机构", value: "userOrganization" },
{ name: "技术职级等级", value: "technicalGrade" },
{ name: "证件号", value: "idNumber" },
{ name: "性别", value: "gender" },
{ name: "籍贯", value: "nativePlace" },
@ -77,7 +83,8 @@ const employeeInformation = (props) => {
localStorage.getItem(EMPLOYEEINFORMATION) == null
? localStorage.setItem(
EMPLOYEEINFORMATION,
JSON.stringify(['userName', 'departmrnt', 'roleName', 'hiredate', 'age', 'phoneNumber', 'marital', 'politicsStatus', 'educationBackground',
JSON.stringify(['userName', 'departmrnt', 'userJob', 'userPost', 'userActiveStatus', 'point', 'userOrganization', 'technicalGrade',
'hiredate', 'age', 'phoneNumber', 'marital', 'politicsStatus', 'educationBackground',
'graduatedFrom', 'employmentLife', 'occupationalHistory'])
)
: "";
@ -203,38 +210,60 @@ const employeeInformation = (props) => {
<img src="/assets/images/hrImg/V.png" alt="" style={{ width: 14, height: 14 }} /> 职位
</div>
),
width: 150,
dataIndex: "roleName",
key: "roleName",
width: 100,
dataIndex: "userJob",
key: "userJob",
render: (_, r, index) => {
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
{
r.role.map((ite, idx) => {
let roleArr = []
for (let i = 0; i < r.role.length; i++) {
roleArr.push(r.role[i].name)
}
return (
<div key={idx} style={{ display: 'flex', alignItems: 'center' }}>
{idx == 0 ?
(ite.name) : ('')
}
{
r.role.length > 1 && idx == 1 ? (
<Tooltip content={roleArr.join(',')} trigger="click" style={{ lineHeight: 2 }}>
<div style={{ color: 'rgba(0,90,189,0.8)', fontSize: 12, marginRight: 4, cursor: "pointer", }}>
+{r.role.length - 1}
return r.userJob ? UserAttribute.jobDataSource[r.userJob - 1] : '-';
},
}, {
title: (
<div>
<img src="/assets/images/hrImg/V.png" alt="" style={{ width: 14, height: 14 }} /> 岗位
</div>
</Tooltip>
) : ('')
}
),
width: 120,
dataIndex: "userPost",
key: "userPost",
render: (_, r, index) => {
return r.userPost || '-';
},
}, {
title: (
<div>
<img src="/assets/images/hrImg/V.png" alt="" style={{ width: 14, height: 14 }} /> 在职状态
</div>
)
})
}
</div>);
),
width: 110,
dataIndex: "userActiveStatus",
key: "userActiveStatus",
render: (_, r, index) => {
return r.userActiveStatus ? UserAttribute.activeStatusDataSource[r.userActiveStatus - 1] : '-';
},
}, {
title: '绩点',
width: 100,
dataIndex: "point",
key: "point",
render: (_, r, index) => <span>-</span>,
}, {
title: (
<div>
<img src="/assets/images/hrImg/V.png" alt="" style={{ width: 14, height: 14 }} /> 所属机构
</div>
),
width: 150,
dataIndex: "userOrganization",
key: "userOrganization",
render: (_, r, index) => {
return r.userOrganization ? UserAttribute.organizationDataSource[r.userOrganization - 1] : '-';
},
}, {
title: '技术职级等级',
width: 150,
dataIndex: "technicalGrade",
key: "technicalGrade",
render: (_, r, index) => <span>-</span>,
}, {
title: '证件号',
width: 180,
@ -427,13 +456,13 @@ const employeeInformation = (props) => {
},
},
];
for (let i = 0; i < arr.length; i++) {
let colum = column.filter((item) => {
return item.key === arr[i];
});
columns.splice(i + 2, 0, colum[0]);
let newColumns = columns;
for (let i = 0; i < column.length; i++) {
if (arr.indexOf(column[i].key) > -1) {
newColumns.push(column[i]);
}
}
setSetupp(columns);
setSetupp(newColumns);
}
function handleRow(record, index) {//
//
@ -485,7 +514,7 @@ const employeeInformation = (props) => {
style={{ width: 200 }}
initValue={"name"}
>
<Form.Select.Option value='role'></Form.Select.Option>
<Form.Select.Option value='post'></Form.Select.Option>
<Form.Select.Option value='dep'>部门</Form.Select.Option>
<Form.Select.Option value='number'>编号</Form.Select.Option>
<Form.Select.Option value='name'>姓名</Form.Select.Option>
@ -668,7 +697,7 @@ const employeeInformation = (props) => {
<Setup
tableType={EMPLOYEEINFORMATION}
tableList={tableList}
length={25}
length={30}
close={() => {
setSetup(false);
attribute();

2
web/client/src/sections/humanAffairs/containers/import-members-modal.js

@ -343,7 +343,7 @@ const ImportMembersModal = props => {
error(`${i + 2}行工作经验(年)错误,请填写非负数`)
return
}
postData.push({//人员编号 待办todotodo
postData.push({
pepUserId: xqExist.pepUserId, name, idNumber, gender, birthday, nativePlace, marital,
politicsStatus, phoneNumber, workPlace, graduatedFrom, educationBackground, specialty, graduationDate,
hiredate, turnProbationPeriod, regularDate, dimissionDate, experienceYear, occupationalHistory,

3
web/client/src/sections/humanAffairs/containers/index.js

@ -9,6 +9,7 @@ import LeaveStatistics from './leaveStatistics';
import OvertimeStatistics from './overtimeStatistics';
//招聘
import AppointmentRecords from './appointmentRecords';
import PersonnelDistribution from './salersDistribution/personnelDistribution';
//培训
import ResourceRepository from './resourceRepository';
//绩效考核
@ -32,7 +33,7 @@ import PersonnelFilesDetail from './personnelFilesDetail';
export {
PersonnelFiles, EmployeeInformation, DeptArchives,
AttendanceStatistics, LeaveStatistics, OvertimeStatistics,
AppointmentRecords,
AppointmentRecords, PersonnelDistribution,
ResourceRepository,
WeeklyManagement, SaleLog, PMLog,
ProbationerKPI, RegularKPI,

27
web/client/src/sections/humanAffairs/containers/personnelFiles.jsx

@ -87,7 +87,7 @@ const Rest = (props) => {
<div style={{ display: 'flex', marginTop: 16, marginBottom: 17 }}>
<div>
<Select value={keywordTarget} onChange={setKeywordTarget} placeholder='请选择搜索类型' style={{ width: 200 }} >
<Select.Option value='role'></Select.Option>
<Select.Option value='post'></Select.Option>
<Select.Option value='dep'>部门</Select.Option>
<Select.Option value='number'>编号</Select.Option>
<Select.Option value='name'>姓名</Select.Option>
@ -232,30 +232,7 @@ const Rest = (props) => {
<img src="/assets/images/hrImg/post.png" alt="" style={{ width: '100%', height: '100%' }} />
</div>
<div style={{ fontSize: 14, color: '#282828', marginLeft: 12, marginRight: 9, display: 'flex', }}>
{
item.role.map((ite, idx) => {
let roleArr = []
for (let i = 0; i < item.role.length; i++) {
roleArr.push(item.role[i].name)
}
return (
<div key={idx} style={{ display: 'flex', alignItems: 'center' }}>
{idx == 0 ?
(ite.name.substring(0, 6)) : ('')
}
{
item.role.length > 1 && idx == 1 ? (
<Tooltip content={roleArr.join(',')} trigger="click" style={{ lineHeight: 2 }}>
<div style={{ color: 'rgba(0,90,189,0.8)', fontSize: 12, marginRight: 4, cursor: "pointer", }}>
+{item.role.length - 1}
</div>
</Tooltip>
) : ('')
}
</div>
)
})
}
{item.userPost || '暂无'}
</div>
</div>
<div style={{ color: 'rgba(0,0,0,0.65)', fontSize: 12 }}>

58
web/client/src/sections/humanAffairs/containers/personnelFilesDetail.jsx

@ -7,6 +7,7 @@ import * as echarts from 'echarts';
import DeleteModal from '../components/deleteModal';
import PersonnelModal from '../components/personnelModal';
import moment from 'moment'
import { UserAttribute } from '$utils'
import '../style.less'
@ -610,7 +611,7 @@ const Rest = (props) => {
职务信息
</div>
<div style={{ marginTop: 13, display: 'flex' }}>
<div style={{ display: 'flex', width: '14.715%' }}>
<div style={{ display: 'flex', width: '15.715%' }}>
<div style={{ color: 'rgba(0, 0, 0,0.6)', fontSize: 12, width: 60 }}>
员工编号
</div>
@ -618,7 +619,7 @@ const Rest = (props) => {
{pepObj.userCode || '暂无'}
</div>
</div>
<div style={{ display: 'flex', width: '19.072%' }}>
<div style={{ display: 'flex', width: '17.072%' }}>
<div style={{ color: 'rgba(0, 0, 0,0.6)', fontSize: 12, width: 60 }}>
入职时间
</div>
@ -626,7 +627,7 @@ const Rest = (props) => {
{pepObj.hiredate || '暂无'}
</div>
</div>
<div style={{ display: 'flex', width: '21.395%' }}>
<div style={{ display: 'flex', width: '32.395%' }}>
<div style={{ color: 'rgba(0, 0, 0,0.6)', fontSize: 12, width: 84 }}>
转试用期时间
</div>
@ -644,43 +645,38 @@ const Rest = (props) => {
</div>
</div>
<div style={{ marginTop: 9, display: 'flex' }}>
<div style={{ display: 'flex', width: '33.787%' }}>
<div style={{ display: 'flex', width: '15.715%' }}>
<div style={{ color: 'rgba(0, 0, 0,0.6)', fontSize: 12, width: 60 }}>
员工职位
</div>
<div style={{ color: '#005ABD', fontSize: 13, display: 'flex', alignItems: 'center' }}>
<div style={{ color: '#005ABD', fontSize: 13 }}>
{
pepObj?.role?.map((item, index) => {
let roleArr = []
for (let i = 0; i < pepObj.role.length; i++) {
roleArr.push(pepObj.role[i].name)
}
return (
<div key={index} style={{ display: 'flex', alignItems: 'center' }}>
{index < 2 ?
(item.name) : ('')
pepObj.userJob? UserAttribute.jobDataSource[pepObj.userJob - 1] : '暂无'
}
</div>
</div>
<div style={{ display: 'flex', width: '17.072%'}}>
<div style={{ color: 'rgba(0, 0, 0,0.6)', fontSize: 12, width: 60 }}>
员工岗位
</div>
<div style={{ color: '#4A4A4A', fontSize: 13 }}>
{
pepObj.role.length > 1 && index == 0 ?
(',') : ('')
pepObj.userPost || '暂无'
}
{
pepObj.role.length > 2 && index == 2 ? (
<Tooltip content={roleArr.join(',')} trigger="click" style={{ lineHeight: 2 }}>
<div style={{ color: 'rgba(0,90,189,0.8)', fontSize: 12, marginRight: 4, cursor: "pointer", }}>
+{pepObj.role.length - 2}
</div>
</Tooltip>
) : ('')
}
</div>
)
})
<div style={{ display: 'flex', width: '32.395%'}}>
<div style={{ color: 'rgba(0, 0, 0,0.6)', fontSize: 12, width: 84, textAlign: 'end' }}>
所属机构
</div>
<div style={{ color: '#4A4A4A', fontSize: 13 }}>
{
pepObj.userOrganization? UserAttribute.organizationDataSource[pepObj.userOrganization - 1] : '暂无'
}
</div>
</div>
<div style={{ display: 'flex', width: '66.213%' }}>
<div style={{ color: 'rgba(0, 0, 0,0.6)', fontSize: 12, width: 84, textAlign: 'end' }}>
<div style={{ display: 'flex', width: '' }}>
<div style={{ color: 'rgba(0, 0, 0,0.6)', fontSize: 12, width: 60 }}>
所属部门
</div>
{
@ -712,7 +708,7 @@ const Rest = (props) => {
</div>
</div>
<div style={{ display: 'flex' }}>
<div style={{ marginTop: 9, display: 'flex', width: '14.715%' }}>
<div style={{ marginTop: 9, display: 'flex', width: '15.715%' }}>
<div style={{ color: 'rgba(0, 0, 0,0.6)', fontSize: 12, width: 60 }}>
工作经验
</div>
@ -720,7 +716,7 @@ const Rest = (props) => {
{pepObj.experienceYear ? pepObj.experienceYear + '年' : '暂无'}
</div>
</div>
<div style={{ marginTop: 9, display: 'flex', width: '19.072%' }}>
<div style={{ marginTop: 9, display: 'flex', width: '17.072%' }}>
<div style={{ color: 'rgba(0, 0, 0,0.6)', fontSize: 12, width: 60 }}>
入职年限
</div>
@ -728,7 +724,7 @@ const Rest = (props) => {
{pepObj.hiredate ? String(moment(new Date()).diff(pepObj.hiredate, 'years',true)).substring(0,3) + '年' : '暂无'}
</div>
</div>
<div style={{ marginTop: 9, display: 'flex', width: '39.4%' }}>
<div style={{ marginTop: 9, display: 'flex', width: '32.395%' }}>
<div style={{ color: 'rgba(0, 0, 0,0.6)', fontSize: 12, width: 84, textAlign: 'end' }}>
试用期
</div>

272
web/client/src/sections/humanAffairs/containers/salersDistribution/importSalersModal.js

@ -0,0 +1,272 @@
'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'
//下载模板和上传文件读取
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) => {
if (!cities) {//可以不填
return true;
}
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} width={620}
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['销售区域(市)']);
let businessLines = 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
}
//todo 业务线判断
postData.push({
pepUserId: rzExist.pepUserId, name, number,
provinces: provinces || '', cities: cities || '', businessLines: businessLines || '',
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>
<div style={{ marginTop: 20, border: '1px solid #C7E1FF', background: '#F4F8FF', borderRadius: 2, padding: '8px 0px 7px 12px', alignItems: 'center', color: '#0F7EFB', fontSize: 12 }}>
<div>填写要求</div>
<div>员工编号必填唯一数字和字母的组合</div>
<div>姓名必填若与员工编号对应的项企用户姓名不同将以项企数据为准</div>
<div>销售区域/直辖市必填省或直辖市顿号隔开北京市江西省江苏省</div>
<div>销售区域非必填归属所填省的地级市顿号隔开南昌市镇江市</div>
</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);

350
web/client/src/sections/humanAffairs/containers/salersDistribution/personnelDistribution.jsx

@ -0,0 +1,350 @@
import React, { useEffect, useRef, useState, useMemo } 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.length ?
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: 60,
render: (text, record, index) => index + 1
}, {
title: starHeader('姓名'),
dataIndex: 'name',
key: 'name',
width: 80
},
{
title: starHeader('部门名称'),
dataIndex: 'department',
key: 'department',
width: 200,
render: (text, r, index) => {
let arrStr = text.map(t => t.name);
return getMultis(arrStr);
}
}, {
title: '销售区域(省/直辖市)',
dataIndex: 'provinces',
key: 'provinces',
width: 160,
render: (text, record, index) => {
return getMultis(text?.split('、') || []);
}
}, {
title: '销售区域(市)',
dataIndex: 'cities',
key: 'cities',
width: 160,
render: (text, record, index) => {
return text ? getMultis(text?.split('、') || []) : '-';
}
}, {
title: '业务线',
dataIndex: 'businessLines',
key: 'businessLines',
width: 120,
render: (text, record, index) => {
return text ? getMultis(text?.split('、') || []) : '-';
}
}, {
title: starHeader('岗位'),
dataIndex: 'post',
key: 'post',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: starHeader('入职时间'),
dataIndex: 'hireDate',
key: 'hireDate',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: starHeader('转正时间'),
dataIndex: 'regularDate',
key: 'regularDate',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: starHeader('工龄'),
dataIndex: 'workYears',
key: 'workYears',
width: 120,
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: 120,
render: (text, record) => {
return <div>
<span style={{ color: '#1890FF', cursor: 'pointer' }} onClick={() => onEdit(record)}>编辑</span>&nbsp;&nbsp;
<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();
}
});
}
const scroll = useMemo(() => ({}), []);
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></div>
<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);

263
web/client/src/sections/humanAffairs/containers/salersDistribution/salesMemberModal.js

@ -0,0 +1,263 @@
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 businessLines = ['市政', '地灾', '水利', '智慧城市', '工地', '环保', '安防', '产品投标', '交通', '矿山', '产品线']
const SalesMemberModal = (props) => {
const { dispatch, actions, user, meetingList, onConfirm, getMultis, onCancel, close, rzMembers, dataToEdit } = props;
const { humanAffairs } = actions;
const form = useRef();//表单
const [lineOptions, setLineOptions] = useState([]);
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);
let lineOptions = businessLines.map((l, index) => {
return <Select.Option value={l} key={index}>
{l}
</Select.Option>
})
setLineOptions(lineOptions);
if (dataToEdit) {
setPeoplePro(dataToEdit);
onChange(dataToEdit.provinces?.split('、') || []);//市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 == 'noValid') {
Notification.error({
content: '你填写的员工编号无对应的人员信息',
duration: 2,
})
} else if (peoplePro?.userCode) {
if (values.userCode == peoplePro.userCode) {
values.provinces = values.provinces.join('、')
values.cities = values.cities.join('、')
values.businessLines = values.businessLines.join('、')
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.userCode == id);//人员档案里面需要有
if (exist) {
let item = {
pepUserId: exist.pepUserId,
name: exist.userName,
department: exist.departmrnt,
hireDate: exist.hiredate,
regularDate: exist.regularDate,
userCode: exist.userCode,
post: exist.userPost//岗位
}
setPeoplePro(item)
} else {
setPeoplePro('noValid')
}
// } else {
// setPeoplePro('noValid')
// }
// }
// })
}
const renderSimpleInfo = () => {
let arrStr = peoplePro?.department?.map(t => t.name) || [];
return <div>
{
peoplePro == 'noValid' ?
<div style={{ background: '#F4F5FC', border: '1px solid rgba(0, 90, 189, 0.2)', marginTop: 8, padding: '10px 0px', textAlign: 'center', color: '#4A4A4A' }}>
没有符合条件的结果
</div> :
peoplePro?.name ? (
<div style={{ 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 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: "请输入人员编号" }]}
onChange={() => setPeoplePro({})}
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?.split('、') || []}
label="销售区域(省/直辖市)"
field='provinces'
showClear
rules={[{ required: true, message: '请选择销售区域(省/直辖市)' }]}
placeholder='请选择销售区域(省/直辖市)'
multiple filter
style={{ width: '100%' }}
onClear={() => onClear()}
onChange={value => onChange(value)}
maxTagCount={5}
>
{options}
</Form.Select>
<Form.Select
initValue={dataToEdit?.cities ? dataToEdit?.cities?.split('、') : []}
label="销售区域(市)"
field='cities'
showClear
placeholder='请选择销售区域(市)'
multiple filter
style={{ width: '100%' }}
maxTagCount={5}
>
{cityOptions}
</Form.Select>
<Form.Select
initValue={dataToEdit?.businessLines ? dataToEdit?.businessLines?.split('、') : []}
label="业务线"
field='businessLines'
showClear
placeholder='请选择业务线'
multiple filter
style={{ width: '100%' }}
maxTagCount={5}
>
{lineOptions}
</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);

8
web/client/src/sections/humanAffairs/nav-item.jsx

@ -46,6 +46,14 @@ export function getNavItem(user, dispatch) {
items: [{
itemKey: 'appointmentRecords', to: '/humanAffairs/recruit/recruitRecord/appointmentRecords', text: '任用记录'
}]
}, {
itemKey: 'salesStatistics',
text: '销售统计',
icon: <iconpark-icon style={{ width: 20, height: 20 }} name="iconxiaoshou"></iconpark-icon>,//wwwtodo
to: '/humanAffairs/recruit/salesStatistics/personnelDistribution',
items: [{
itemKey: 'personnelDistribution', to: '/humanAffairs/recruit/salesStatistics/personnelDistribution', text: '销售人员分布'
}]
}, {
itemKey: 'trainFiles',
text: '培训档案',

12
web/client/src/sections/humanAffairs/routes.js

@ -2,7 +2,7 @@ import {
PersonnelFiles, EmployeeInformation, //人员档案
DeptArchives, //部门档案
AttendanceStatistics, LeaveStatistics, OvertimeStatistics,
AppointmentRecords,
AppointmentRecords, PersonnelDistribution,
ResourceRepository,
WeeklyManagement, SaleLog, PMLog,
ProbationerKPI, RegularKPI,
@ -89,6 +89,16 @@ export default [{
component: AppointmentRecords,
breadcrumb: '任用记录',
}]
}, {
path: '/salesStatistics',
key: 'salesStatistics',
breadcrumb: '销售统计',
childRoutes: [{
path: '/personnelDistribution',
key: 'personnelDistribution',
component: PersonnelDistribution,
breadcrumb: '销售人员分布',
}]
}]
}, {
path: '/train',

3
web/client/src/utils/index.js

@ -6,7 +6,7 @@ import {
AxyRequest, EmisRequest,
basicAction, RouteRequest
} from './webapi'
import { UserAttribute } from './userAttribute';
export {
isAuthorized,
AuthorizationCode,
@ -17,4 +17,5 @@ export {
EmisRequest,
basicAction,
RouteRequest,
UserAttribute
}

8
web/client/src/utils/userAttribute.js

@ -0,0 +1,8 @@
'use strict';
export const UserAttribute = {
jobDataSource: ['普通员工', '中层', '高层'],
activeStatusDataSource: ['在职', '离职', '特殊状态-特殊账号'],
organizationDataSource: ['江西飞尚科技有限公司', '江西飞尚工程质量检测有限公司',
'江西飞尚科技有限公司江苏分公司', '江西汇派科技有限公司'],
};

6
web/client/src/utils/webapi.js

@ -31,6 +31,12 @@ export const ApiTable = {
getAttendanceVacate: 'attendance/vacate',//请假统计
getAttendanceVacateType: 'attendance/vacate/type',//请假类型
getAttendanceOvertime: 'attendance/overtime',//加班统计
getSalesList: 'sales/member/list',
addSalesMember: 'sales/member/add',
editSalesMember: 'sales/member/modify',
delSalesMember: 'sales/member/del',
addSalesMemberBulk: 'add/sales/members/bulk'
};
export const RouteTable = {
apiRoot: "/api/root",

Loading…
Cancel
Save