Browse Source

(*)销售人员分布明细表 模块

master
wuqun 2 years ago
parent
commit
4d323b4003
  1. 5
      .vscode/launch.json
  2. 135
      api/app/lib/controllers/report/index.js
  3. 3
      api/app/lib/routes/report/index.js
  4. 415
      api/app/lib/utils/member.js
  5. 8
      api/config.js
  6. 5
      web/client/src/sections/business/actions/index.js
  7. 17
      web/client/src/sections/business/actions/salers-report.js
  8. 241
      web/client/src/sections/business/containers/performanceReport/achievementDetails.jsx
  9. 211
      web/client/src/sections/business/containers/performanceReport/backMoneyDetails.jsx
  10. 272
      web/client/src/sections/business/containers/salesReport/salesDistributionDetails.jsx
  11. 1
      web/client/src/utils/webapi.js

5
.vscode/launch.json

@ -63,8 +63,9 @@
// "--clickHouseCamworkflow camworkflow",
// "--clickHouseHr hr_dev",
//
"--clickHouseUrl http://10.8.16.221",
"--clickHousePepEmis pg_pepca",
"--clickHouseUrl http://10.8.30.161",
"--clickHousePepEmis pepca8",
"--clickHouseHr hrm",
]
},
{

135
api/app/lib/controllers/report/index.js

@ -1,5 +1,6 @@
'use strict';
const fs = require('fs');
const moment = require('moment');
// 查询储备项目统计表
async function getReserveItemReport(ctx, next) {
const { type } = ctx.params;
@ -27,6 +28,136 @@ async function getReserveItemReport(ctx, next) {
}
}
async function getSalersReport(ctx) {
try {
const { clickHouse } = ctx.app.fs
const { memberList, packageUserData } = ctx.app.fs.utils
const {
keywordTarget, keyword, limit, page, state,
hiredateStart, hiredateEnd, marital, native, workPlace,
orderBy, orderDirection, placeSearch, toExport
} = 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,
})
let mIds = members.map(m => m.pepUserId);
let innerSelectQuery = `where del=false and pep_user_id in [${mIds}]`
+ `${placeSearch ? `
and (sales.provinces LIKE '%${placeSearch}%' or sales.cities LIKE '%${placeSearch}%')
`: ''}`
const salersRes = await clickHouse.hr.query(`
SELECT * from sales_distribution as sales
${innerSelectQuery}
order by id desc
${!toExport && limit ? `LIMIT ${limit}` : ''}
${!toExport && limit && page ? 'OFFSET ' + parseInt(limit) * parseInt(page) : ''}
`).toPromise()
const countRes = await clickHouse.hr.query(`
SELECT
count(sales.pep_user_id) AS count from sales_distribution as sales
${innerSelectQuery}
`).toPromise()
let rslt = []
salersRes.map(d => {
let info = members.find(m => m.pepUserId == d.pep_user_id);
let item = {
name: info.userName,
userCode: info.userCode,
post: info.userPost,
department: info.departmrnt,
hireDate: info.hiredate,//入职时间
regularDate: info.regularDate,//转正时间
businessLines: d.business_lines,//业务线
...d
}
rslt.push(item);
})
if (toExport) {
await exportSalesDetail(ctx, rslt);//导出
} else {
ctx.status = 200;
ctx.body = {
count: countRes.length ? countRes[0].count : 0,
rows: rslt
};
}
} catch (error) {
ctx.fs.logger.error(`path:${ctx.path},error:${error}`)
ctx.status = 400;
ctx.body = { name: 'FindAllError', message: '获取销售人员分布明细表数据失败' }
}
}
async function exportSalesDetail(ctx, dataList) {
try {
let header = [{
title: "姓名",
key: 'name'
}, {
title: "部门名称",
key: 'department'
}, {
title: "销售区域(省/直辖市)",
key: 'provinces'
}, {
title: "销售区域(市)",
key: 'cities'
}, {
title: "业务线",
key: 'businessLines'
}, {
title: "岗位",
key: 'post'
}, {
title: "入职时间",
key: 'hireDate'
}, {
title: "转正时间",
key: 'regularDate'
}, {
title: "工龄",
key: 'workYears'
}];
const { utils: { simpleExcelDown } } = ctx.app.fs;
let exportData = []
for (let item of dataList) {
item.department = item.department.map(t => t.name).join('、') || '-';
item.cities = item.cities || '-';
item.businessLines = item.businessLines || '-';
item.post = item.post || '-';
item.regularDate = item.regularDate || '-';
item.workYears = item.hireDate ? String(moment(new Date()).diff(item.hireDate, 'years', true)).substring(0, 3) + '年' : '-'
item.hireDate = item.hireDate || '-';
exportData.push(item)
}
const fileName = `销售人员分布明细表_${moment().format('YYYYMMDDHHmmss')}` + '.xlsx'
const filePath = await simpleExcelDown({ data: exportData, 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
}
}
}
module.exports = {
getReserveItemReport
getReserveItemReport,
getSalersReport
}

3
api/app/lib/routes/report/index.js

@ -5,4 +5,7 @@ const report = require('../../controllers/report');
module.exports = function (app, router, opts) {
app.fs.api.logAttr['GET/reserveItem/report/:type'] = { content: '查询储备项目统计表', visible: false };
router.get('/reserveItem/report/:type', report.getReserveItemReport);
app.fs.api.logAttr['GET/sales/member/list'] = { content: '查询销售人员分布明细表', visible: false };
router.get('/sales/member/list', report.getSalersReport);
};

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

@ -0,0 +1,415 @@
'use strict';
const moment = require('moment')
const request = require('superagent');
module.exports = function (app, opts) {
async function memberList({
keywordTarget, keyword, limit, page, state,
hiredateStart, hiredateEnd, marital, native, workPlace,
orderBy, orderDirection,
nowAttendanceTime,
overtimeDayStatisticStartDate, overtimeDayStatisticendDate,
overtimeCountStatistic, overtimeCountStatisticStartDate, overtimeCountStatisticendDate,
vacateDayStatisticStartDate, vacateDayStatisticendDate,
vacateDurationStatistic, vacateCountStatistic, vacateCountStatisticStartDate, vacateCountStatisticendDate
}) {
//const { judgeHoliday } = app.fs.utils
const { clickHouse } = app.fs
const { database: pepEmis } = clickHouse.pepEmis.opts.config
const curDay = moment().format('YYYY-MM-DD')
const nowTime = moment()
let whereOption = []
let whereFromSelectOption = []
let returnEmpty = false
if (state == 'inOffice') {
// 在岗
//const holidayJudge = await judgeHoliday(curDay)
// if (holidayJudge) {
// if (
// holidayJudge.workday
// && nowTime.isAfter(moment(curDay + ' 08:30'))
// && nowTime.isBefore(moment(curDay + ' 17:30'))
// ) {
// // 在工作日的工作时间范围 无请假记录
// whereFromSelectOption.push(`vacateStartTime = '1970-01-01 00:00:00.000000'`)
// } else {
// returnEmpty = true
// }
// } else {
// returnEmpty = true
// }
}
if (state == 'dayoff') {
// 放假
// const holidayJudge = await judgeHoliday(curDay)
// if (holidayJudge) {
// if (
// holidayJudge.dayoff || holidayJudge.festivals
// ) {
// // 在休息日范围内且无加班申请
// whereFromSelectOption.push(`overtimeStartTime = '1970-01-01 00:00:00.000000'`)
// } else {
// returnEmpty = true
// }
// } else {
// returnEmpty = true
// }
}
if (returnEmpty) {
return {
count: 0,
rows: []
}
}
let overtimeDayStatisticWhere = []
// if (overtimeDayStatisticStartDate) {
// overtimeDayStatisticWhere.push(`overtime_day.day >= '${moment(overtimeDayStatisticStartDate).format('YYYY-MM-DD')}'`)
// }
// if (overtimeDayStatisticendDate) {
// overtimeDayStatisticWhere.push(`overtime_day.day <= '${moment(overtimeDayStatisticendDate).format('YYYY-MM-DD')}'`)
// }
let overtimeCountStatisticWhere = []
// if (overtimeCountStatisticStartDate) {
// overtimeCountStatisticWhere.push(`overtime.start_time >= '${moment(overtimeCountStatisticStartDate).startOf('day').format('YYYY-MM-DD HH:mm:ss')}'`)
// }
// if (overtimeCountStatisticendDate) {
// overtimeCountStatisticWhere.push(`overtime.end_time <= '${moment(overtimeCountStatisticendDate).endOf('day').format('YYYY-MM-DD HH:mm:ss')}'`)
// }
let vacateDayStatisticWhere = []
// if (vacateDayStatisticStartDate) {
// vacateDayStatisticWhere.push(`vacate_day.day >= '${moment(vacateDayStatisticStartDate).format('YYYY-MM-DD')}'`)
// }
// if (vacateDayStatisticendDate) {
// vacateDayStatisticWhere.push(`vacate_day.day <= '${moment(vacateDayStatisticendDate).format('YYYY-MM-DD')}'`)
// }
let vacateCountStatisticWhere = []
// if (vacateCountStatisticStartDate) {
// vacateCountStatisticWhere.push(`vacate.start_time >= '${moment(vacateCountStatisticStartDate).startOf('day').format('YYYY-MM-DD HH:mm:ss')}'`)
// }
// if (vacateCountStatisticendDate) {
// vacateCountStatisticWhere.push(`vacate.end_time <= '${moment(vacateCountStatisticendDate).endOf('day').format('YYYY-MM-DD HH:mm:ss')}'`)
// }
// CRAZY
const innerSelectQuery = `
FROM member
INNER JOIN ${pepEmis}.user AS user
ON member.pep_user_id = user.id
${keywordTarget == 'number' && keyword ? `
AND user.people_code LIKE '%${keyword}%'
`: ''}
${keywordTarget == 'name' && keyword ? `
AND user.name LIKE '%${keyword}%'
`: ''}
${nowAttendanceTime ? `
${state == 'vacate' ? 'INNER' : 'LEFT'} JOIN (
SELECT
pep_user_id,
any(start_time) AS vacateStartTime,
any(end_time) AS vacateEndTime
FROM vacate
WHERE
start_time <= '${nowTime.format('YYYY-MM-DD HH:mm:ss')}'
AND end_time > '${nowTime.format('YYYY-MM-DD HH:mm:ss')}'
GROUP BY pep_user_id
) AS hrVacate
ON hrVacate.pep_user_id = member.pep_user_id
`: ''}
${nowAttendanceTime ? `
LEFT JOIN (
SELECT
pep_user_id,
any(start_time) AS overtimeStartTime,
any(end_time) AS overtimeEndTime
FROM overtime
WHERE
start_time <= '${nowTime.format('YYYY-MM-DD HH:mm:ss')}'
AND end_time > '${nowTime.format('YYYY-MM-DD HH:mm:ss')}'
GROUP BY pep_user_id
) AS hrOvertime
ON hrOvertime.pep_user_id = member.pep_user_id
`: ''}
${orderBy == 'overtimeTakeRestSum' ||
orderBy == 'overtimePaySum' ||
orderBy == 'overtimeSum' ?
`LEFT JOIN (
SELECT
overtime.pep_user_id AS pepUserId,
sum(overtime_day.duration) AS duration
FROM overtime_day
INNER JOIN overtime
ON overtime.id = overtime_day.overtime_id
${orderBy == 'overtimeTakeRestSum' ? `AND overtime.compensate = '调休'` : ''}
${orderBy == 'overtimePaySum' ? `AND overtime.compensate = '发放加班补偿'` : ''}
${overtimeDayStatisticWhere.length ? `
WHERE ${overtimeDayStatisticWhere.join(' AND ')}
`: ''}
GROUP BY overtime.pep_user_id
) AS overtimeDayStatistic
ON overtimeDayStatistic.pepUserId = member.pep_user_id`: ''}
${overtimeCountStatistic ? `
LEFT JOIN (
SELECT
overtime.pep_user_id AS pepUserId,
count(pep_process_story_id) AS count
FROM overtime
${overtimeCountStatisticWhere.length ? `
WHERE ${overtimeCountStatisticWhere.join(' AND ')}
`: ''}
GROUP BY overtime.pep_user_id
) AS overtimeCountStatistic
ON overtimeCountStatistic.pepUserId = member.pep_user_id
`: ''}
${vacateDurationStatistic ||
orderBy == 'vacateSum' ?
`LEFT JOIN (
SELECT
vacate.pep_user_id AS pepUserId,
sum(vacate_day.duration) AS duration
FROM vacate_day
INNER JOIN vacate
ON vacate.id = vacate_day.vacate_id
${vacateDayStatisticWhere.length ? `
WHERE ${vacateDayStatisticWhere.join(' AND ')}
`: ''}
GROUP BY vacate.pep_user_id
) AS vacateDayStatistic
ON vacateDayStatistic.pepUserId = member.pep_user_id`: ''}
${vacateCountStatistic ? `
LEFT JOIN (
SELECT
vacate.pep_user_id AS pepUserId,
count(pep_process_story_id) AS count
FROM vacate
${vacateCountStatisticWhere.length ? `
WHERE ${vacateCountStatisticWhere.join(' AND ')}
`: ''}
GROUP BY vacate.pep_user_id
) AS vacateCountStatistic
ON vacateCountStatistic.pepUserId = member.pep_user_id
`: ''}
WHERE
member.del = '0'
${keywordTarget == 'post' && keyword ? `
AND user.post IN (
SELECT basicDataPost.id
FROM ${pepEmis}.basicdata_post AS basicDataPost
where basicDataPost.name LIKE '%${keyword}%'
)
` : ''}
${keywordTarget == 'dep' && keyword ? `
AND user.id IN (
SELECT department_user.user
FROM ${pepEmis}.department_user AS department_user
INNER JOIN ${pepEmis}.department AS department
ON department.id = department_user.department
AND department.name LIKE '%${keyword}%'
)
` : ''}
${state == 'dimission' ? `AND member.dimission_date IS NOT null` : ''}
${state == 'onJob' ? `AND member.dimission_date IS null` : ''}
${whereFromSelectOption.length && nowAttendanceTime ? `AND ${whereFromSelectOption.join('AND')}` : ''}
${hiredateStart ? `
AND member.hiredate >= '${moment(hiredateStart).format('YYYY-MM-DD')}'
`: ''}
${hiredateEnd ? `
AND member.hiredate <= '${moment(hiredateEnd).format('YYYY-MM-DD')}'
` : ''}
${marital ? `
AND member.marital = '${marital}'
`: ''}
${native ? `
AND member.native_place = '${native}'
`: ''}
${workPlace ? `
AND member.work_place = '${workPlace}'
`: ''}
`
const userRes = await clickHouse.hr.query(`
SELECT
hrMember."member.pep_user_id" AS pepUserId,
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,
user.job AS userJob,
user.active_status AS userActiveStatus,
user.organization AS userOrganization
FROM (
SELECT
${orderBy == 'overtimeTakeRestSum'
|| orderBy == 'overtimePaySum'
|| orderBy == 'overtimeSum' ? `
overtimeDayStatistic.duration AS overtimeDayStatisticDuration,
`: ''}
${overtimeCountStatistic ? `
overtimeCountStatistic.count AS overtimeCount,
`: ''}
${orderBy == 'vacateSum' || vacateDurationStatistic ? `
vacateDayStatistic.duration AS vacateDayStatisticDuration,
`: ''}
${vacateCountStatistic ? `
vacateCountStatistic.count AS vacateCount,
`: ''}
${nowAttendanceTime ? `
hrVacate.vacateStartTime AS vacateStartTime,
hrVacate.vacateEndTime AS vacateEndTime,
hrOvertime.overtimeStartTime AS overtimeStartTime,
hrOvertime.overtimeEndTime AS overtimeEndTime,
`: ''}
member.*
${innerSelectQuery}
${limit ? `LIMIT ${limit}` : ''}
${limit && page ? 'OFFSET ' + parseInt(limit) * parseInt(page) : ''}
) AS hrMember
LEFT JOIN ${pepEmis}.user AS user
ON pepUserId = user.id
LEFT JOIN ${pepEmis}.user_role AS user_role
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
ON department.id = department_user.department
${whereOption.length ? `WHERE ${whereOption.join(' AND ')}` : ''}
ORDER BY ${orderBy == 'code' ?
'user.people_code'
: orderBy == 'hiredate'
? 'hrMember."member.hiredate"'
: orderBy == 'age'
? 'hrMember."member.birthday"'
: orderBy == 'overtimeTakeRestSum'
|| orderBy == 'overtimePaySum'
|| orderBy == 'overtimeSum' ?
'hrMember.overtimeDayStatisticDuration'
: orderBy == 'overtimeCount' ?
'hrMember.overtimeCount'
: orderBy == 'vacateSum' ?
'hrMember.vacateDayStatisticDuration'
: orderBy == 'vacateCount' ?
'hrMember.vacateCount'
: 'user.people_code'}
${orderDirection || 'ASC'}
`).toPromise()
const countRes = await clickHouse.hr.query(`
SELECT
count(member.pep_user_id) AS count
${innerSelectQuery}
`).toPromise()
return {
count: countRes[0].count,
rows: userRes
}
}
async function packageUserData(userRes, option = {}) {
//const { judgeHoliday, } = app.fs.utils
let workTime = false
let dayoffTime = false
if (option.state) {
const curDay = moment().format('YYYY-MM-DD')
const nowTime = moment()
//const holidayJudge = await judgeHoliday(curDay)
// if (holidayJudge) {
// if (
// holidayJudge.workday
// && nowTime.isAfter(moment(curDay + ' 08:30'))
// && nowTime.isBefore(moment(curDay + ' 17:30'))
// ) {
// workTime = true
// } else if (holidayJudge.dayoff || holidayJudge.festivals) {
// dayoffTime = true
// }
// }
}
let returnD = []
let pepUserIds = [-1]
userRes.rows.forEach(u => {
let existUser = returnD.find(r => r.pepUserId == u.pepUserId)
if (existUser) {
if (u.depId && !existUser.departmrnt.some(d => d.id == u.depId)) {
existUser.departmrnt.push({
id: u.depId,
name: u.depName
})
}
if (u.roleId && !existUser.role.some(r => r.id == u.roleId)) {
existUser.role.push({
id: u.roleId,
name: u.roleName
})
}
} else {
let obj = {}
for (let k in u) {
let nextKey = k.replace('hrMember.', '')
.replace('member.', '')
if (nextKey.includes('_')) {
nextKey = nextKey.toLowerCase()
.replace(
/(_)[a-z]/g,
(L) => L.toUpperCase()
)
.replace(/_/g, '')
}
obj[nextKey] = u[k] == '1970-01-01 00:00:00.000000' || u[k] == '1970-01-01 08:00:00.000000' ? null : u[k]
}
pepUserIds.push(u.pepUserId)
console.log("查询到的用户信息:", obj);
returnD.push({
...obj,
departmrnt: u.depId ? [{
id: u.depId,
name: u.depName
}] : [],
role: u.roleId ? [{
id: u.roleId,
name: u.roleName
}] : [],
state: option.state ?
obj['dimissionDate'] ? 'dimission' :
obj['vacateStartTime'] ? 'vacate' :
workTime ? 'inOffice' :
dayoffTime ? 'dayoff' : 'rest'
: undefined,
del: undefined,
pepuserid: undefined
})
}
})
return { packageUser: returnD, pepUserIds }
}
return {
memberList,
packageUserData
}
}

8
api/config.js

@ -27,6 +27,7 @@ args.option('qndmn', 'qiniuDomain');
args.option('clickHouseUrl', 'clickHouse Url');
args.option('clickHousePort', 'clickHouse Port');
args.option('clickHousePepEmis', 'clickHouse 项企数据库名称');
args.option('clickHouseHr', 'clickHouse 人资数据库名称');
const flags = args.parse(process.argv);
@ -52,6 +53,7 @@ const CLICKHOUST_PORT = process.env.CLICKHOUST_PORT || flags.clickHousePort
const CLICKHOUST_USER = process.env.CLICKHOUST_USER || flags.clickHouseUser
const CLICKHOUST_PASSWORD = process.env.CLICKHOUST_PASSWORD || flags.clickHousePassword
const CLICKHOUST_PEP_EMIS = process.env.CLICKHOUST_PEP_EMIS || flags.clickHousePepEmis
const CLICKHOUST_HR = process.env.CLICKHOUST_HR || flags.clickHouseHr
if (
!RC_DB
@ -59,7 +61,7 @@ if (
|| !API_EMIS_URL
|| !QINIU_DOMAIN_QNDMN_RESOURCE || !QINIU_BUCKET_RESOURCE || !QINIU_AK || !QINIU_SK
|| !CLICKHOUST_URL || !CLICKHOUST_PORT
|| !CLICKHOUST_PEP_EMIS
|| !CLICKHOUST_PEP_EMIS || !CLICKHOUST_HR
) {
console.log('缺少启动参数,异常退出');
args.showHelp();
@ -117,6 +119,10 @@ const product = {
{
name: 'pepEmis',
db: CLICKHOUST_PEP_EMIS
},
{
name: 'hr',
db: CLICKHOUST_HR
}
]
}

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

@ -1,6 +1,7 @@
'use strict';
import * as reserveItem from './reserve-item';
import * as salersReport from './salers-report';
export default {
...reserveItem
...reserveItem,
...salersReport
}

17
web/client/src/sections/business/actions/salers-report.js

@ -0,0 +1,17 @@
'use strict';
import { ApiTable, basicAction } from '$utils'
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 } },
});
}

241
web/client/src/sections/business/containers/performanceReport/achievementDetails.jsx

@ -1,18 +1,154 @@
import React, { useEffect, useState } from 'react';
import React, { useRef, useEffect, useState, useMemo } from 'react';
import { connect } from 'react-redux';
import { Table } from '@douyinfe/semi-ui';
import { Select, Input, Button, Popconfirm, Radio, Tooltip, Table, Pagination, Skeleton } from '@douyinfe/semi-ui';
import { IconSearch } from '@douyinfe/semi-icons';
import { SkeletonScreen } from "$components";
import '../../style.less';
const AchievementDetails = (props) => {
const columns = [
{
title: '序号',
dataIndex: 'index',
render: (text, record, index) => index + 1
},
];
const data = [];
const { dispatch, actions } = props
const [keywordTarget, setKeywordTarget] = useState('contract');
const [keyword, setKeyword] = useState('');//
const [limits, setLimits] = useState()//
const [query, setQuery] = useState({ limit: 10, page: 0 }); //
const [tableData, setTableData] = useState([]);
const page = useRef(query.page);
function seachValueChange(value) {
setKeyword(value)
}
const columns = [{
title: '收到合同日期',
dataIndex: 'year',
key: 'year',
width: 120,
}, {
title: '月份',
dataIndex: 'id',
key: 'id',
width: 120,
}, {
title: '部门',
dataIndex: 'department',
key: 'department',
width: 140,
}, {
title: '销售人员',
dataIndex: 'salers',
key: 'salers',
width: 140,
}, {
title: '客户名称',
dataIndex: 'name',
key: 'name',
width: 140,
}, {
title: '项目名称',
dataIndex: 'projectName',
key: 'projectName',
width: 140,
}, {
title: '合同金额',
dataIndex: 'money',
key: 'money',
width: 140,
}, {
title: '实际业绩',
dataIndex: 'moneyAfterChange',
key: 'moneyAfterChange',
width: 140,
}, {
title: '考核业绩',
dataIndex: 'backYear',
key: 'backYear',
width: 120,
}, {
title: '价格是否特批',
dataIndex: 'backDate',
key: 'backDate',
width: 120,
}, {
title: '特批折算比例',
dataIndex: 'backMoney',
key: 'backMoney',
width: 120,
}, {
title: '预支提成及委外费用',
dataIndex: 'invoiceBack',
key: 'invoiceBack',
width: 140,
}, {
title: '业务线',
dataIndex: 'leaveMoney',
key: 'leaveMoney',
width: 120,
}, {
title: '客户类型',
dataIndex: 'payConfirmTime',
key: 'payConfirmTime',
width: 140,
}, {
title: '行业',
dataIndex: 'thirdPayer',
key: 'thirdPayer',
width: 140,
}, {
title: '信息来源',
dataIndex: 'desc',
key: 'desc',
width: 120,
}, {
title: '项目类型',
dataIndex: 'leaveMoney',
key: 'leaveMoney',
width: 120,
}, {
title: '客户省份',
dataIndex: 'payConfirmTime',
key: 'payConfirmTime',
width: 140,
}, {
title: '客户属性',
dataIndex: 'thirdPayer',
key: 'thirdPayer',
width: 140,
}, {
title: '复购次数',
dataIndex: 'desc',
key: 'desc',
width: 120,
}, {
title: '是否可复制的业务路径',
dataIndex: 'leaveMoney',
key: 'leaveMoney',
width: 120,
}, {
title: '省外业务1.1',
dataIndex: 'payConfirmTime',
key: 'payConfirmTime',
width: 140,
}, {
title: '复购业务1.05',
dataIndex: 'thirdPayer',
key: 'thirdPayer',
width: 140,
}, {
title: '可复制的业务路径1.1',
dataIndex: 'desc',
key: 'desc',
width: 120,
}]
function handleRow(record, index) {//
if (index % 2 === 0) {
return {
style: {
background: '#FAFCFF',
}
};
} else {
return {};
}
}
const scroll = useMemo(() => ({}), []);
return (
<>
<div style={{ padding: '0px 12px' }}>
@ -24,9 +160,88 @@ const AchievementDetails = (props) => {
<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 ', marginTop: 9 }}>
<Table columns={columns} dataSource={data} pagination={false} />
<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", }}>PERFORMANCE DETAILS</div>
</div>
</div>
<div style={{ margin: '18px 0px' }}>
<div style={{ display: 'flex', margin: '16px 0px', justifyContent: 'space-between' }}>
<div style={{ display: 'flex' }}>
<div>
<Select value={keywordTarget} onChange={setKeywordTarget} style={{ width: 150 }} >
<Select.Option value='contract'>合同编号</Select.Option>
<Select.Option value='invoice'>发票号码</Select.Option>
<Select.Option value='certificate'>凭证号</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={{ marginTop: 20 }}>
<Skeleton
loading={false}
active={true}
placeholder={SkeletonScreen()}
>
<Table
columns={columns}
dataSource={tableData}
bordered={false}
empty="暂无数据"
pagination={false}
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>
</div>
</>
)

211
web/client/src/sections/business/containers/performanceReport/backMoneyDetails.jsx

@ -1,18 +1,124 @@
import React, { useEffect, useState } from 'react';
import React, { useRef, useEffect, useState, useMemo } from 'react';
import { connect } from 'react-redux';
import { Table } from '@douyinfe/semi-ui';
import { Select, Input, Button, Popconfirm, Radio, Tooltip, Table, Pagination, Skeleton } from '@douyinfe/semi-ui';
import { IconSearch } from '@douyinfe/semi-icons';
import { SkeletonScreen } from "$components";
import '../../style.less';
const BackMoneyDetails = (props) => {
const columns = [
{
title: '序号',
dataIndex: 'index',
render: (text, record, index) => index + 1
},
];
const data = [];
const { dispatch, actions } = props
const [keywordTarget, setKeywordTarget] = useState('contract');
const [keyword, setKeyword] = useState('');//
const [limits, setLimits] = useState()//
const [query, setQuery] = useState({ limit: 10, page: 0 }); //
const [tableData, setTableData] = useState([]);
const page = useRef(query.page);
function seachValueChange(value) {
setKeyword(value)
}
const columns = [{
title: '年份',
dataIndex: 'year',
key: 'year',
width: 120,
}, {
title: '序号',
dataIndex: 'id',
key: 'id',
width: 120,
}, {
title: '编号',
dataIndex: 'number',
key: 'number',
width: 120,
}, {
title: '部门',
dataIndex: 'department',
key: 'department',
width: 140,
}, {
title: '销售人员',
dataIndex: 'salers',
key: 'salers',
width: 140,
}, {
title: '合同编号',
dataIndex: 'contractNumber',
key: 'contractNumber',
width: 140,
}, {
title: '客户名称',
dataIndex: 'name',
key: 'name',
width: 140,
}, {
title: '项目名称',
dataIndex: 'projectName',
key: 'projectName',
width: 140,
}, {
title: '合同金额',
dataIndex: 'money',
key: 'money',
width: 140,
}, {
title: '变更后合同金额',
dataIndex: 'moneyAfterChange',
key: 'moneyAfterChange',
width: 140,
}, {
title: '回款年份',
dataIndex: 'backYear',
key: 'backYear',
width: 120,
}, {
title: '回款日期',
dataIndex: 'backDate',
key: 'backDate',
width: 120,
}, {
title: '回款金额',
dataIndex: 'backMoney',
key: 'backMoney',
width: 120,
}, {
title: '开票-回款',
dataIndex: 'invoiceBack',
key: 'invoiceBack',
width: 140,
}, {
title: '剩余合同金额',
dataIndex: 'leaveMoney',
key: 'leaveMoney',
width: 120,
}, {
title: '收入确认时间',
dataIndex: 'payConfirmTime',
key: 'payConfirmTime',
width: 140,
}, {
title: '第三方付款单位',
dataIndex: 'thirdPayer',
key: 'thirdPayer',
width: 140,
}, {
title: '备注',
dataIndex: 'desc',
key: 'desc',
width: 120,
}]
function handleRow(record, index) {//
if (index % 2 === 0) {
return {
style: {
background: '#FAFCFF',
}
};
} else {
return {};
}
}
const scroll = useMemo(() => ({}), []);
return (
<>
<div style={{ padding: '0px 12px' }}>
@ -24,9 +130,88 @@ const BackMoneyDetails = (props) => {
<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 ', marginTop: 9 }}>
<Table columns={columns} dataSource={data} pagination={false} />
<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", }}>COLLECTION DETAILS</div>
</div>
</div>
<div style={{ margin: '18px 0px' }}>
<div style={{ display: 'flex', margin: '16px 0px', justifyContent: 'space-between' }}>
<div style={{ display: 'flex' }}>
<div>
<Select value={keywordTarget} onChange={setKeywordTarget} style={{ width: 150 }} >
<Select.Option value='contract'>合同编号</Select.Option>
<Select.Option value='invoice'>发票号码</Select.Option>
<Select.Option value='certificate'>凭证号</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={{ marginTop: 20 }}>
<Skeleton
loading={false}
active={true}
placeholder={SkeletonScreen()}
>
<Table
columns={columns}
dataSource={tableData}
bordered={false}
empty="暂无数据"
pagination={false}
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>
</div>
</>
)

272
web/client/src/sections/business/containers/salesReport/salesDistributionDetails.jsx

@ -1,18 +1,178 @@
import React, { useEffect, useState } from 'react';
import React, { useRef, useEffect, useState, useMemo } from 'react';
import { connect } from 'react-redux';
import { Table } from '@douyinfe/semi-ui';
import moment from 'moment'
import { Select, Input, Button, Toast, Radio, Tooltip, Table, Pagination, Skeleton } from '@douyinfe/semi-ui';
import { IconSearch } from '@douyinfe/semi-icons';
import { SkeletonScreen } from "$components";
import '../../style.less';
const SalesDistributionDetails = (props) => {
const { dispatch, actions, user } = props
const { businessManagement } = actions
const [keywordTarget, setKeywordTarget] = useState('dep');
const [keyword, setKeyword] = useState('');//
const [limits, setLimits] = useState()//
const [query, setQuery] = useState({ limit: 10, page: 0 }); //
const [tableData, setTableData] = useState([]);
const [exportUrl, setExportUrl] = useState('')
const page = useRef(query.page);
function seachValueChange(value) {
setKeyword(value)
}
const columns = [
{
title: '序号',
dataIndex: 'index',
render: (text, record, index) => index + 1
},
];
const data = [];
useEffect(() => {
getMemberSearchList()
}, []);
useEffect(() => {
getMemberSearchList()//
}, [query])
function getMemberSearchList() {
let kt = keywordTarget == 'place' ? '' : keywordTarget;
let k = keywordTarget == 'place' ? '' : keyword;
let placeSearch = keywordTarget == 'place' ? keyword : '';
dispatch(businessManagement.getSalesList({ keywordTarget: kt, keyword: k, placeSearch, ...query })).then(r => {
if (r.success) {
setTableData(r.payload?.data?.rows);
setLimits(r.payload?.data?.count)
}
})
}
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 starHeader = (header) => {
return <div>
<img src="/assets/images/hrImg/V.png" style={{ width: 14, height: 14 }} /> {header}
</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: 140,
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> : '-')
}
}]
function handleRow(record, index) {//
if (index % 2 === 0) {
return {
style: {
background: '#FAFCFF',
}
};
} else {
return {};
}
}
const exportAllData = () => {
dispatch(businessManagement.getSalesList({ limit: 1 })).then((res) => {//
if (res.success) {
if (res.payload.data.count) {
let url = `sales/member/list?token=${user.token}&toExport=1&timestamp=${moment().valueOf()}`
setExportUrl(url);
} else {
Toast.info({
content: '暂无可导出的数据',
duration: 3,
})
}
}
})
}
const scroll = useMemo(() => ({}), []);
return (
<>
<div style={{ padding: '0px 12px' }}>
@ -24,9 +184,97 @@ const SalesDistributionDetails = (props) => {
<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 ', marginTop: 9 }}>
<Table columns={columns} dataSource={data} pagination={false} />
</div>
<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", }}>SALES PERSONNEL DISTRIBUTION DETAILS</div>
</div>
</div>
<div style={{ margin: '18px 0px' }}>
<div style={{ display: 'flex', margin: '16px 0px', justifyContent: 'space-between' }}>
<div style={{ display: 'flex' }}>
<div>
<Select value={keywordTarget} onChange={setKeywordTarget} style={{ width: 150 }} >
<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' }}>
<div style={{ padding: '6px 20px', background: '#00BA85', color: '#FFFFFF', fontSize: 14, marginLeft: 18, cursor: "pointer" }}
onClick={() => {
exportAllData()
}}>
导出全部
</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={false}
active={true}
placeholder={SkeletonScreen()}
>
<Table
columns={columns}
dataSource={tableData}
bordered={false}
empty="暂无数据"
pagination={false}
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>
{
exportUrl ? <iframe src={`/_api/${exportUrl}`} style={{ display: 'none' }} /> : ''
}
</div>
</>
)

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

@ -18,6 +18,7 @@ export const ApiTable = {
//项目报表
getReserveItemReport: "reserveItem/report/{type}",
getSalesList: 'sales/member/list',
};
export const RouteTable = {

Loading…
Cancel
Save