diff --git a/api/app/lib/controllers/salesDistribution/index.js b/api/app/lib/controllers/salesDistribution/index.js new file mode 100644 index 0000000..9974264 --- /dev/null +++ b/api/app/lib/controllers/salesDistribution/index.js @@ -0,0 +1,333 @@ +'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 res = await models.SalesDistribution.findAndCountAll({ + where: { del: false }, + offset: Number(page) * Number(limit), + limit: Number(limit), + }) + + let rslt = [] + res.rows.map(d => { + let valid = false; + let info = members.find(m => m.pepUserId == d.dataValues.pepUserId); + if (info) { + if (placeSearch) { + let exist1 = d.dataValues.provinces.join(',').indexOf(placeSearch) != -1 + let exist2 = d.dataValues.cities.join(',').indexOf(placeSearch) != -1 + if (exist1 || exist2) { + valid = true; + } + } else { + valid = true; + } + } + if (valid) { + 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 } = ctx.request.body + + const existRes = await models.SalesDistribution.findOne({ + where: { pepUserId } + }) + + if (existRes && !existRes.del) { + throw '当前销售人员信息已存在' + } + + let storageData = { pepUserId, provinces, cities, 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 } = ctx.request.body + + const existRes = await models.SalesDistribution.findOne({ + where: { pepUserId } + }) + + if (!existRes) { + throw '当前销售人员信息不存在' + } + + let storageData = { pepUserId, provinces, cities, 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, del = false } = editArr[i]; + + let dataToUpdate = { + provinces, + cities, + 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, +} \ No newline at end of file diff --git a/api/app/lib/models/sales_distribution.js b/api/app/lib/models/sales_distribution.js new file mode 100644 index 0000000..6d7bf6c --- /dev/null +++ b/api/app/lib/models/sales_distribution.js @@ -0,0 +1,62 @@ +/* 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.ARRAY(DataTypes.STRING), + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: false, + field: "provinces", + autoIncrement: false + }, + cities: { + type: DataTypes.ARRAY(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 + } + }, { + tableName: "sales_distribution", + comment: "", + indexes: [] + }); + dc.models.SalesDistribution = SalesDistribution; + return SalesDistribution; +}; \ No newline at end of file diff --git a/api/app/lib/routes/salesDistribution/index.js b/api/app/lib/routes/salesDistribution/index.js new file mode 100644 index 0000000..3ddf054 --- /dev/null +++ b/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); +}; \ No newline at end of file diff --git a/api/app/lib/utils/member.js b/api/app/lib/utils/member.js index 479d521..f0ce54a 100644 --- a/api/app/lib/utils/member.js +++ b/api/app/lib/utils/member.js @@ -244,6 +244,7 @@ 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, @@ -287,6 +288,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