ww664853070 2 years ago
parent
commit
d1d0a40753
  1. 5
      .vscode/launch.json
  2. 13
      api/app/lib/controllers/auth/index.js
  3. 290
      api/app/lib/controllers/report/achievement.js
  4. 135
      api/app/lib/controllers/report/index.js
  5. 430
      api/app/lib/models/contract_detail.js
  6. 196
      api/app/lib/models/invoice_detail.js
  7. 241
      api/app/lib/models/performance_detail.js
  8. 187
      api/app/lib/models/receivable_detail.js
  9. 13
      api/app/lib/routes/report/index.js
  10. 72
      api/app/lib/utils/constant.js
  11. 415
      api/app/lib/utils/member.js
  12. 6
      api/app/lib/utils/xlsxDownload.js
  13. 8
      api/config.js
  14. 1
      api/package.json
  15. 12
      api/sequelize-automate.config.js
  16. 2
      web/client/index.ejs
  17. 2
      web/client/index.html
  18. 13
      web/client/src/components/setup.jsx
  19. 42
      web/client/src/sections/business/actions/achievement-report.js
  20. 7
      web/client/src/sections/business/actions/index.js
  21. 17
      web/client/src/sections/business/actions/salers-report.js
  22. 69
      web/client/src/sections/business/constants/index.js
  23. 307
      web/client/src/sections/business/containers/performanceReport/achievementDetails.jsx
  24. 269
      web/client/src/sections/business/containers/performanceReport/backMoneyDetails.jsx
  25. 232
      web/client/src/sections/business/containers/performanceReport/contractDetails.jsx
  26. 175
      web/client/src/sections/business/containers/performanceReport/importContractDetailsModal.js
  27. 175
      web/client/src/sections/business/containers/performanceReport/importInvoicingDetailsModal.js
  28. 195
      web/client/src/sections/business/containers/performanceReport/invoicingDetails.jsx
  29. 264
      web/client/src/sections/business/containers/salesReport/salesDistributionDetails.jsx
  30. 6
      web/client/src/sections/business/nav-item.jsx
  31. 7
      web/client/src/utils/webapi.js

5
.vscode/launch.json

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

13
api/app/lib/controllers/auth/index.js

@ -9,9 +9,18 @@ async function login (ctx, next) {
try { try {
const params = ctx.request.body; const params = ctx.request.body;
const emisLoginRes = await ctx.app.fs.emisRequest.post('login', { let emisLoginRes = null
data: params if (params.username && params.password) {
emisLoginRes = await ctx.app.fs.emisRequest.post('login', {
data: { ...params, }
})
} else if (params.token) {
emisLoginRes = await ctx.app.fs.emisRequest.get('user-info', {
query: {
token: params.token,
}
}) })
}
if (!emisLoginRes) { if (!emisLoginRes) {
throw "无此用户,请使用正确的登录信息" throw "无此用户,请使用正确的登录信息"

290
api/app/lib/controllers/report/achievement.js

@ -0,0 +1,290 @@
'use strict';
const fs = require('fs');
const moment = require('moment');
//业绩报表相关
async function getReceivedDetail(ctx) {
try {
const { models } = ctx.fs.dc;
const { keywordTarget, keyword, limit, page, toExport } = ctx.query
let where = {}
if (keywordTarget == 'contract' && keyword) {
where.contractNo = { $like: `%${keyword}%` }
}
let findOption = {
where: where,
order: [['id', 'DESC']]
}
if (!toExport) {//非导出时考虑分页
if (limit) {
findOption.limit = Number(limit)
}
if (page && limit) {
findOption.offset = Number(page) * Number(limit)
}
}
let res = await models.ReceivableDetail.findAndCountAll(findOption);
if (toExport) {//数据导出相关
await exportReceivedDetail(ctx, res.rows);
} else {
ctx.status = 200
ctx.body = {
count: res.count,
rows: res.rows
};
}
} catch (error) {
ctx.fs.logger.error(`path:${ctx.path},error:${error}`)
ctx.status = 400;
ctx.body = { name: 'FindAllError', message: '获取失败' }
}
}
async function exportReceivedDetail(ctx, dataList) {
try {
let header = [{
title: '年度',
key: 'year',
}, {
title: '序号',
key: 'serialNo',
}, {
title: '编号',
key: 'number',
}, {
title: '部门',
key: 'department',
}, {
title: '销售人员',
key: 'sale',
}, {
title: '合同编号',
key: 'contractNo',
}, {
title: '客户名称',
key: 'customer',
}, {
title: '项目名称',
key: 'item',
}, {
title: '合同金额',
key: 'amount',
}, {
title: '变更后合同金额',
key: 'changeAmount',
}, {
title: '回款年份',
key: 'receivableYear',
}, {
title: '回款日期',
key: 'receivableDate',
}, {
title: '回款金额',
key: 'receivableAmount',
}, {
title: '开票-回款',
key: 'invoicedBack',
}, {
title: '剩余合同金额',
key: 'remainConAmount',
}, {
title: '收入确认时间',
key: 'incomeConfirmdate',
}, {
title: '第三方付款单位',
key: 'thirdPayment',
}, {
title: '备注',
key: 'remark',
}]
const { utils: { simpleExcelDown } } = ctx.app.fs;
let exportData = []
for (let { dataValues: item } of dataList) {
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
}
}
}
async function getAchievementDetail(ctx) {
try {
const { models } = ctx.fs.dc;
const { keywordTarget, keyword, limit, page, toExport } = ctx.query
let where = {}
if (keywordTarget == 'saler' && keyword) {
where.sale = { $like: `%${keyword}%` }
}
if (keywordTarget == 'line' && keyword) {
where.serviceLine = { $like: `%${keyword}%` }
}
if (keywordTarget == 'dep' && keyword) {
where.department = { $like: `%${keyword}%` }
}
let findOption = {
where: where,
order: [['id', 'DESC']]
}
if (!toExport) {//非导出时考虑分页
if (limit) {
findOption.limit = Number(limit)
}
if (page && limit) {
findOption.offset = Number(page) * Number(limit)
}
}
let res = await models.PerformanceDetail.findAndCountAll(findOption);
if (toExport) {//数据导出相关
await exportAchievementDetail(ctx, res.rows);
} else {
ctx.status = 200
ctx.body = {
count: res.count,
rows: res.rows
};
}
} catch (error) {
ctx.fs.logger.error(`path:${ctx.path},error:${error}`)
ctx.status = 400;
ctx.body = { name: 'FindAllError', message: '获取失败' }
}
}
async function exportAchievementDetail(ctx, dataList) {
try {
let header = [{
title: '收到合同日期',
key: 'recConDate',
}, {
title: '月份',
key: 'month',
}, {
title: '部门',
key: 'department',
}, {
title: '销售人员',
key: 'sale',
}, {
title: '客户名称',
key: 'customer',
}, {
title: '项目名称',
key: 'item',
}, {
title: '合同金额',
key: 'amount',
}, {
title: '实际业绩',
key: 'realPerformance',
}, {
title: '考核业绩',
key: 'assessmentPerformance',
}, {
title: '价格是否特批',
key: 'isApproval',
}, {
title: '特批折算比例',
key: 'approvalProp',
}, {
title: '预支提成及委外费用',
key: 'cost',
}, {
title: '业务线',
key: 'serviceLine',
}, {
title: '客户类型',
key: 'cusType',
}, {
title: '行业',
key: 'industry',
}, {
title: '信息来源',
key: 'source',
}, {
title: '项目类型',
key: 'itemType',
}, {
title: '客户省份',
key: 'cusProvince',
}, {
title: '客户属性',
key: 'cusAttribute',
}, {
title: '复购次数',
key: 'repurchaseCount',
}, {
title: '是否可复制的业务路径',
key: 'reproducible',
}, {
title: '省外业务1.1',
key: 'outProvince',
}, {
title: '复购业务1.05',
key: 'repurchase',
}, {
title: '可复制的业务路径1.1',
key: 'isreproduce',
}]
const { utils: { simpleExcelDown } } = ctx.app.fs;
let exportData = []
for (let { dataValues: item } of dataList) {
item.isApproval = item.isApproval ? '是' : '否';
item.reproducible = item.reproducible ? '是' : '否';
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
}
}
}
/**
* 查询合同明细表数据
* @param {*} ctx ctx ctx.query:{keywordTarget-关键字项keyword-关键字内容limit-页宽, page-页码}
*/
async function getContractDetail(ctx) {
try {
const { models } = ctx.fs.dc;
const { keywordTarget, keyword, limit, page } = ctx.query;
const where = {};
if (keywordTarget && keyword) {
where[keywordTarget] = { $iLike: `%${keyword}%` };
}
let contractDetail = await models.ContractDetail.findAndCountAll({
where: where,
offset: Number(page) * Number(limit),
limit: Number(limit),
order: [['id', 'DESC']]
});
ctx.status = 200
ctx.body = contractDetail;
} catch (error) {
ctx.fs.logger.error(`path:${ctx.path},error:${error}`)
ctx.status = 400;
ctx.body = { name: 'FindError', message: '查询合同明细表数据失败' }
}
}
module.exports = {
getReceivedDetail,//回款
getAchievementDetail,//业绩
getContractDetail,
}

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

@ -1,5 +1,6 @@
'use strict'; 'use strict';
const fs = require('fs');
const moment = require('moment');
// 查询储备项目统计表 // 查询储备项目统计表
async function getReserveItemReport(ctx, next) { async function getReserveItemReport(ctx, next) {
const { type } = ctx.params; 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 = { module.exports = {
getReserveItemReport getReserveItemReport,
getSalersReport
} }

430
api/app/lib/models/contract_detail.js

@ -0,0 +1,430 @@
/* eslint-disable*/
'use strict';
module.exports = dc => {
const DataTypes = dc.ORM;
const sequelize = dc.orm;
const ContractDetail = sequelize.define("contractDetail", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: true,
field: "id",
autoIncrement: true
},
year: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "年度",
primaryKey: false,
field: "year ",
autoIncrement: false
},
serialNo: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null,
comment: "序号",
primaryKey: false,
field: "serial_no",
autoIncrement: false
},
number: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "编号",
primaryKey: false,
field: "number",
autoIncrement: false
},
introduction: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "简介",
primaryKey: false,
field: "introduction",
autoIncrement: false
},
contractNo: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "合同编号",
primaryKey: false,
field: "contract_no",
autoIncrement: false
},
applyDate: {
type: DataTypes.DATEONLY,
allowNull: true,
defaultValue: null,
comment: "申请日期",
primaryKey: false,
field: "apply_date",
autoIncrement: false
},
recConDate: {
type: DataTypes.DATEONLY,
allowNull: true,
defaultValue: null,
comment: "收到合同日期",
primaryKey: false,
field: "rec_con_date",
autoIncrement: false
},
contractPaper: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "合同纸质版情况",
primaryKey: false,
field: "contract_paper",
autoIncrement: false
},
contractElec: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "合同电子版情况",
primaryKey: false,
field: "contract_elec",
autoIncrement: false
},
department: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "部门",
primaryKey: false,
field: "department",
autoIncrement: false
},
business: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "业务线",
primaryKey: false,
field: "business",
autoIncrement: false
},
sale: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "销售人员",
primaryKey: false,
field: "sale",
autoIncrement: false
},
customer: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "客户名称",
primaryKey: false,
field: "customer",
autoIncrement: false
},
item: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "项目名称",
primaryKey: false,
field: "item",
autoIncrement: false
},
itemType: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "项目类型",
primaryKey: false,
field: "item_type",
autoIncrement: false
},
amount: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "合同金额",
primaryKey: false,
field: "amount",
autoIncrement: false
},
changeAmount: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "变更后合同金额",
primaryKey: false,
field: "change_amount",
autoIncrement: false
},
cInvoicedAmount: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "累计开票金额",
primaryKey: false,
field: "c_invoiced_amount",
autoIncrement: false
},
cBackAmount: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "累计回款金额",
primaryKey: false,
field: "c_back_amount",
autoIncrement: false
},
invoicedBack: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "开票-回款",
primaryKey: false,
field: "invoiced_back",
autoIncrement: false
},
unInvoicedAmount: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "未开票金额",
primaryKey: false,
field: "un_invoiced_amount",
autoIncrement: false
},
remainConAmount: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "剩余合同金额",
primaryKey: false,
field: "remain_con_amount",
autoIncrement: false
},
backPercent: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "回款率",
primaryKey: false,
field: "back_percent",
autoIncrement: false
},
retentionMoney: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "质保金",
primaryKey: false,
field: "retention_money",
autoIncrement: false
},
retentionPercent: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "质保金比例",
primaryKey: false,
field: "retention_percent",
autoIncrement: false
},
retentionTerm: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "质保期",
primaryKey: false,
field: "retention_term",
autoIncrement: false
},
invoiceTax1: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "发票税率金额",
primaryKey: false,
field: "invoice_tax1",
autoIncrement: false
},
invoiceTax2: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: null,
primaryKey: false,
field: "invoice_tax2",
autoIncrement: false
},
invoiceTax3: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: null,
primaryKey: false,
field: "invoice_tax3",
autoIncrement: false
},
payType: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "合同付款方式",
primaryKey: false,
field: "pay_type",
autoIncrement: false
},
cost: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "预支提成及委外费用",
primaryKey: false,
field: "cost",
autoIncrement: false
},
performanceRatio: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "业绩折算比例",
primaryKey: false,
field: "performance_ratio",
autoIncrement: false
},
realPerformance: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "实际业绩",
primaryKey: false,
field: "real_performance",
autoIncrement: false
},
assessmentPerformance: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "考核业绩",
primaryKey: false,
field: "assessment_performance",
autoIncrement: false
},
acceptanceDate: {
type: DataTypes.DATEONLY,
allowNull: true,
defaultValue: null,
comment: "验收日期",
primaryKey: false,
field: "acceptance_date",
autoIncrement: false
},
backConfirmDate: {
type: DataTypes.DATEONLY,
allowNull: true,
defaultValue: null,
comment: "收入确认时间",
primaryKey: false,
field: "back_confirm_date",
autoIncrement: false
},
recYear: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "接单年份",
primaryKey: false,
field: "rec_year",
autoIncrement: false
},
recMonth: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "接单月份",
primaryKey: false,
field: "rec_month",
autoIncrement: false
},
cusAttribute: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "客户属性",
primaryKey: false,
field: "cus_attribute",
autoIncrement: false
},
cusType: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "客户类型",
primaryKey: false,
field: "cus_type",
autoIncrement: false
},
industry: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "行业",
primaryKey: false,
field: "industry",
autoIncrement: false
},
source: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "信息来源",
primaryKey: false,
field: "source",
autoIncrement: false
},
cusProvince: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "客户省份",
primaryKey: false,
field: "cus_province",
autoIncrement: false
},
cusArea: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "项目所在地",
primaryKey: false,
field: "cus_area",
autoIncrement: false
},
text: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: null,
primaryKey: false,
field: "text",
autoIncrement: false
}
}, {
tableName: "contract_detail",
comment: "",
indexes: []
});
dc.models.ContractDetail = ContractDetail;
return ContractDetail;
};

196
api/app/lib/models/invoice_detail.js

@ -0,0 +1,196 @@
/* eslint-disable*/
'use strict';
module.exports = dc => {
const DataTypes = dc.ORM;
const sequelize = dc.orm;
const InvoiceDetail = sequelize.define("invoiceDetail", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: true,
field: "id",
autoIncrement: false
},
year: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "年度:【申请日期】-年份",
primaryKey: false,
field: "year",
autoIncrement: false
},
serialNo: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: "序号:自动生成\n(自动升序),每一年自动从1开始",
primaryKey: false,
field: "serial_no",
autoIncrement: false
},
number: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "编号:年份+序号 如2022年1",
primaryKey: false,
field: "number",
autoIncrement: false
},
department: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "部门:申请部门",
primaryKey: false,
field: "department",
autoIncrement: false
},
sale: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "销售人员:申请人",
primaryKey: false,
field: "sale",
autoIncrement: false
},
contractNo: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "合同编号",
primaryKey: false,
field: "contract_no",
autoIncrement: false
},
customer: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "客户名称:【甲方名称】",
primaryKey: false,
field: "customer",
autoIncrement: false
},
item: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "项目名称",
primaryKey: false,
field: "item",
autoIncrement: false
},
amount: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "合同金额:【合同金额\n(元)】",
primaryKey: false,
field: "amount",
autoIncrement: false
},
changeAmount: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "变更后合同金额",
primaryKey: false,
field: "change_amount",
autoIncrement: false
},
invoiceNo: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "发票号码:《发票申请单》【发票号】一个发票号码一个行数据",
primaryKey: false,
field: "invoice_no",
autoIncrement: false
},
invoiceType: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "开票类型:《发票申请单》\n【发票类型】",
primaryKey: false,
field: "invoice_type",
autoIncrement: false
},
invoiceDate: {
type: DataTypes.DATEONLY,
allowNull: false,
defaultValue: null,
comment: "开票日期:《发票申请单》\n【开票日期】",
primaryKey: false,
field: "invoice_date",
autoIncrement: false
},
invoiceAmount: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "开票金额:《发票申请单》【发票金额】取财务填写数据",
primaryKey: false,
field: "invoice_amount",
autoIncrement: false
},
outputTax: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "销项税额:《发票申请单》-销项税额",
primaryKey: false,
field: "output_tax",
autoIncrement: false
},
total: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "合计:自动算出\n(开票金额+销项税额)",
primaryKey: false,
field: "total",
autoIncrement: false
},
taxRate13: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "税率13%:《发票申请单》",
primaryKey: false,
field: "tax_rate13",
autoIncrement: false
},
taxRate9: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "税率9%:《发票申请单》",
primaryKey: false,
field: "tax_rate9",
autoIncrement: false
},
taxRate6: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "税率6%:《发票申请单》",
primaryKey: false,
field: "tax_rate6",
autoIncrement: false
}
}, {
tableName: "invoice_detail",
comment: "",
indexes: []
});
dc.models.InvoiceDetail = InvoiceDetail;
return InvoiceDetail;
};

241
api/app/lib/models/performance_detail.js

@ -0,0 +1,241 @@
/* eslint-disable*/
'use strict';
module.exports = dc => {
const DataTypes = dc.ORM;
const sequelize = dc.orm;
const PerformanceDetail = sequelize.define("performanceDetail", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: true,
field: "id",
autoIncrement: false
},
recConDate: {
type: DataTypes.DATEONLY,
allowNull: false,
defaultValue: null,
comment: "收到合同日期",
primaryKey: false,
field: "rec_con_date",
autoIncrement: false
},
month: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "月份",
primaryKey: false,
field: "month",
autoIncrement: false
},
department: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "部门:申请部门",
primaryKey: false,
field: "department",
autoIncrement: false
},
sale: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "销售人员",
primaryKey: false,
field: "sale",
autoIncrement: false
},
customer: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "客户名称:【甲方名称】",
primaryKey: false,
field: "customer",
autoIncrement: false
},
item: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "项目名称",
primaryKey: false,
field: "item",
autoIncrement: false
},
amount: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "合同金额:【合同金额\n(元)】",
primaryKey: false,
field: "amount",
autoIncrement: false
},
realPerformance: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "实际业绩;(合同金额-预\n支提成及委外费用)*特批折算比例\n(G-L)*K",
primaryKey: false,
field: "real_performance",
autoIncrement: false
},
assessmentPerformance: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "合同金额*省外业务(1.1)*复购业务\n(1.05)*可复制的业务路径(1.1)",
primaryKey: false,
field: "assessment_performance",
autoIncrement: false
},
isApproval: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: null,
comment: "价格是否特批:根据折算比例-《合同明细表》推\n算(100%——“是”;其他为否)",
primaryKey: false,
field: "isApproval",
autoIncrement: false
},
approvalProp: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "特批折算比例:【业绩折\n算比例】",
primaryKey: false,
field: "approval_prop",
autoIncrement: false
},
cost: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "预支提成及委外费用",
primaryKey: false,
field: "cost",
autoIncrement: false
},
serviceLine: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "业务线",
primaryKey: false,
field: "service_line",
autoIncrement: false
},
cusType: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "客户类型",
primaryKey: false,
field: "cus_type",
autoIncrement: false
},
industry: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "行业",
primaryKey: false,
field: "industry",
autoIncrement: false
},
source: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "信息来源",
primaryKey: false,
field: "source",
autoIncrement: false
},
itemType: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "项目类型",
primaryKey: false,
field: "item_type",
autoIncrement: false
},
cusProvince: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "客户省份",
primaryKey: false,
field: "cus_province",
autoIncrement: false
},
cusAttribute: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "客户属性:G:政府、事业单位\nB:央企、国企、平台商\nC:资源方",
primaryKey: false,
field: "cus_attribute",
autoIncrement: false
},
repurchaseCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: "复购次数:同一客户储备成单-成单项目数量\n(PM)",
primaryKey: false,
field: "repurchase_count",
autoIncrement: false
},
reproducible: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: null,
comment: "是否可复制的业务路径:《合同评审》表单获取",
primaryKey: false,
field: "reproducible",
autoIncrement: false
},
outProvince: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "省外业务1.1:当【客户省份】=“江西”时,省外业务=1;当【客户省份】<>“江西”时,省外业务=1.1",
primaryKey: false,
field: "out_province",
autoIncrement: false
},
repurchase: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "复购业务1.05:当【复购次数】\n<2时,复购业务=1;当【复购次数】\n>=2时,复购业务=1.05;",
primaryKey: false,
field: "repurchase",
autoIncrement: false
},
isreproduce: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "可复制的业务路径1.1:当【是否可复\n制的业务路径】=“是”,可复制的业务路径=1.1;当【是否可复制的业务路径】=“否”,可复制的业务路径=1;",
primaryKey: false,
field: "isreproduce",
autoIncrement: false
}
}, {
tableName: "performance_detail",
comment: "",
indexes: []
});
dc.models.PerformanceDetail = PerformanceDetail;
return PerformanceDetail;
};

187
api/app/lib/models/receivable_detail.js

@ -0,0 +1,187 @@
/* eslint-disable*/
'use strict';
module.exports = dc => {
const DataTypes = dc.ORM;
const sequelize = dc.orm;
const ReceivableDetail = sequelize.define("receivableDetail", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: true,
field: "id",
autoIncrement: false
},
year: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "年度:【申请日期】-年份",
primaryKey: false,
field: "year",
autoIncrement: false
},
serialNo: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: "序号:自动生成\n(自动升序),每一年自动从1开始",
primaryKey: false,
field: "serial_no",
autoIncrement: false
},
number: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "编号:年份+序号 如2022年1",
primaryKey: false,
field: "number",
autoIncrement: false
},
department: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "部门:申请部门",
primaryKey: false,
field: "department",
autoIncrement: false
},
sale: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "销售人员:申请人",
primaryKey: false,
field: "sale",
autoIncrement: false
},
contractNo: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "合同编号",
primaryKey: false,
field: "contract_no",
autoIncrement: false
},
customer: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "客户名称:【甲方名称】",
primaryKey: false,
field: "customer",
autoIncrement: false
},
item: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "项目名称",
primaryKey: false,
field: "item",
autoIncrement: false
},
amount: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "合同金额:【合同金额\n(元)】",
primaryKey: false,
field: "amount",
autoIncrement: false
},
changeAmount: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "变更后合同金额",
primaryKey: false,
field: "change_amount",
autoIncrement: false
},
receivableYear: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "回款年份",
primaryKey: false,
field: "receivable_year",
autoIncrement: false
},
receivableDate: {
type: DataTypes.DATEONLY,
allowNull: false,
defaultValue: null,
comment: "回款日期",
primaryKey: false,
field: "receivable_date",
autoIncrement: false
},
receivableAmount: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "回款金额",
primaryKey: false,
field: "receivable_amount",
autoIncrement: false
},
invoicedBack: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "开票-回款",
primaryKey: false,
field: "invoiced_back ",
autoIncrement: false
},
remainConAmount: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "剩余合同金额",
primaryKey: false,
field: "remain_con_amount",
autoIncrement: false
},
incomeConfirmdate: {
type: DataTypes.DATEONLY,
allowNull: false,
defaultValue: null,
comment: "收入确认时间",
primaryKey: false,
field: "income_confirmdate",
autoIncrement: false
},
thirdPayment: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "第三方付款单位",
primaryKey: false,
field: "third_payment",
autoIncrement: false
},
remark: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "备注:回款流程》-备注",
primaryKey: false,
field: "remark",
autoIncrement: false
}
}, {
tableName: "receivable_detail",
comment: "",
indexes: []
});
dc.models.ReceivableDetail = ReceivableDetail;
return ReceivableDetail;
};

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

@ -1,8 +1,21 @@
'use strict'; 'use strict';
const report = require('../../controllers/report'); const report = require('../../controllers/report');
const achieve = require('../../controllers/report/achievement');
module.exports = function (app, router, opts) { module.exports = function (app, router, opts) {
app.fs.api.logAttr['GET/reserveItem/report/:type'] = { content: '查询储备项目统计表', visible: false }; app.fs.api.logAttr['GET/reserveItem/report/:type'] = { content: '查询储备项目统计表', visible: false };
router.get('/reserveItem/report/:type', report.getReserveItemReport); 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);
app.fs.api.logAttr['GET/detail/received/back'] = { content: '查询回款明细表', visible: false };
router.get('/detail/received/back', achieve.getReceivedDetail);
app.fs.api.logAttr['GET/detail/achievement'] = { content: '查询业绩明细表', visible: false };
router.get('/detail/achievement', achieve.getAchievementDetail);
app.fs.api.logAttr['GET/contract/detail'] = { content: '查询合同明细表', visible: false };
router.get('/contract/detail', achieve.getContractDetail);
}; };

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

@ -13,8 +13,80 @@ module.exports = function (app, opts) {
'调休': '调休' '调休': '调休'
} }
const contractDetailsColumnKeys = {
year: '年度',
serialNo: '序号',
number: '编号',
introduction: '简介',
contractNo: '合同编号',
applyDate: '申请日期',
recConDate: '收到合同日期',
contractPaper: '合同纸质版情况',
contractElec: '合同电子版情况',
department: '部门',
business: '业务线',
sale: '销售人员',
customer: '客户名称',
item: '项目名称',
itemType: '项目类型',
amount: '合同金额',
changeAmount: '变更后合同金额',
cInvoicedAmount: '累计开票金额',
cBackAmount: '累计回款金额',
invoicedBack: '开票-回款',
unInvoicedAmount: '未开票金额',
remainConAmount: '剩余合同金额',
backPercent: '回款率',
retentionMoney: '质保金',
retentionPercent: '质保金比例',
retentionTerm: '质保期',
invoiceTax1: '发票税率金额13%',
invoiceTax2: '发票税率金额9%',
invoiceTax3: '发票税率金额6%',
payType: '合同付款方式',
cost: '预支提成及委外费用',
performanceRatio: '业绩折算比例',
realPerformance: '实际业绩',
assessmentPerformance: '考核业绩',
acceptanceDate: '验收日期',
backConfirmDate: '收入确认时间',
recYear: '接单年份',
recMonth: '接单月份',
cusAttribute: '客户属性',
cusType: '客户类型',
industry: '行业',
source: '信息来源',
cusProvince: '客户省份',
cusArea: '项目所在地',
text: '备注',
}
const invoicingDetailsColumnKeys = {
year: '年度',
serialNo: '序号',
number: '编号',
department: '部门',
sale: '销售人员',
contractNo: '合同编号',
customer: '客户名称',
item: '项目名称',
amount: '合同金额',
changeAmount: '变更后合同金额',
invoiceNo: '发票号码',
invoiceType: '开票类型',
invoiceDate: '开票日期',
invoiceAmount: '开票金额',
outputTax: '销项税额',
total: '合计',
taxRate13: '税率13%',
taxRate9: '税率9%',
taxRate6: '税率6%'
}
return { return {
dayType, dayType,
overtimeType, overtimeType,
contractDetailsColumnKeys,
invoicingDetailsColumnKeys
} }
} }

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
}
}

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

@ -8,7 +8,7 @@ const moment = require('moment')
module.exports = function (app, opts) { module.exports = function (app, opts) {
//递归创建目录 同步方法 //递归创建目录 同步方法
async function makeDir (dir) { async function makeDir(dir) {
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
makeDir(path.dirname(dir)) makeDir(path.dirname(dir))
fs.mkdirSync(dir, function (err) { fs.mkdirSync(dir, function (err) {
@ -19,7 +19,7 @@ module.exports = function (app, opts) {
} }
} }
async function simpleExcelDown ({ data = [], header = [], fileName = moment().format('YYYY-MM-DD HH:mm:ss') } = {}) { async function simpleExcelDown({ data = [], header = [], fileName = moment().format('YYYY-MM-DD HH:mm:ss') } = {}) {
const fileDirPath = path.join(__dirname, `../../downloadFiles`) const fileDirPath = path.join(__dirname, `../../downloadFiles`)
makeDir(fileDirPath) makeDir(fileDirPath)
const file = new xlsx.File(); const file = new xlsx.File();
@ -59,7 +59,7 @@ module.exports = function (app, opts) {
indexCell.style = headerStyle indexCell.style = headerStyle
for (let h of header) { for (let h of header) {
const cell = row.addCell(); const cell = row.addCell();
cell.value = data[i][h.key] || h.defaultValue || ''; cell.value = data[i][h.key] || h.defaultValue || '-';
cell.style = style cell.style = style
} }
} }

8
api/config.js

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

1
api/package.json

@ -33,6 +33,7 @@
"pg": "^7.9.0", "pg": "^7.9.0",
"redis": "^3.1.2", "redis": "^3.1.2",
"request": "^2.88.2", "request": "^2.88.2",
"sequelize-automate-freesun": "^1.2.2",
"superagent": "^3.5.2", "superagent": "^3.5.2",
"uuid": "^3.3.2" "uuid": "^3.3.2"
}, },

12
api/sequelize-automate.config.js

@ -1,11 +1,11 @@
module.exports = { module.exports = {
// 数据库配置 与 sequelize 相同 // 数据库配置 与 sequelize 相同
dbOptions: { dbOptions: {
database: 'hr-dev', database: 'data_center',
username: 'postgres', username: 'FashionAdmin',
password: '123', password: '123456',
dialect: 'postgres', dialect: 'postgres',
host: '10.8.30.166', host: '10.8.30.36',
port: 5432, port: 5432,
define: { define: {
underscored: false, underscored: false,
@ -26,10 +26,10 @@ module.exports = {
dir: './app/lib/models', // 指定输出 models 文件的目录 dir: './app/lib/models', // 指定输出 models 文件的目录
typesDir: 'models', // 指定输出 TypeScript 类型定义的文件目录,只有 TypeScript / Midway 等会有类型定义 typesDir: 'models', // 指定输出 TypeScript 类型定义的文件目录,只有 TypeScript / Midway 等会有类型定义
emptyDir: false, // !!! 谨慎操作 生成 models 之前是否清空 `dir` 以及 `typesDir` emptyDir: false, // !!! 谨慎操作 生成 models 之前是否清空 `dir` 以及 `typesDir`
tables: ['overtime_day','vacate_day'], // 指定生成哪些表的 models,如 ['user', 'user_post'];如果为 null,则忽略改属性 tables: ['contract_detail', 'invoice_detail', 'receivable_detail'], // 指定生成哪些表的 models,如 ['user', 'user_post'];如果为 null,则忽略改属性
skipTables: [], // 指定跳过哪些表的 models,如 ['user'];如果为 null,则忽略改属性 skipTables: [], // 指定跳过哪些表的 models,如 ['user'];如果为 null,则忽略改属性
tsNoCheck: false, // 是否添加 `@ts-nocheck` 注释到 models 文件中 tsNoCheck: false, // 是否添加 `@ts-nocheck` 注释到 models 文件中
ignorePrefix: [], // 生成的模型名称忽略的前缀,因为 项目中有以下表名是以 t_ 开头的,在实际模型中不需要, 可以添加多个 [ 't_data_', 't_',] ,长度较长的 前缀放前面 ignorePrefix: [], // 生成的模型名称忽略的前缀,因为 项目中有以下表名是以 t_ 开头的,在实际模型中不需要, 可以添加多个 [ 't_data_', 't_',] ,长度较长的 前缀放前面
attrLength: false, // 在生成模型的字段中 是否生成 如 var(128)这种格式,公司一般使用 String ,则配置为 false attrLength: false, // 在生成模型的字段中 是否生成 如 var(128)这种格式,公司一般使用 String ,则配置为 false
}, }
} }

2
web/client/index.ejs

@ -9,7 +9,7 @@
<!-- <link rel="shortcut icon" href="/assets/images/favicon.ico"> --> <!-- <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_16.53951580d7de40330d8cd62a7b291c97.es5.js"></script>
</head> </head>

2
web/client/index.html

@ -7,7 +7,7 @@
<!-- <link rel="shortcut icon" href="/assets/images/favicon.ico"> --> <!-- <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_16.53951580d7de40330d8cd62a7b291c97.es5.js"></script>
</head> </head>
<body> <body>

13
web/client/src/components/setup.jsx

@ -5,12 +5,13 @@ import {
Checkbox, Checkbox,
} from "@douyinfe/semi-ui"; } from "@douyinfe/semi-ui";
function Setup (props) { function Setup(props) {
const { const {
close, close,
tableType, tableType,
tableList, tableList,
length length,
objWidth
} = props; } = props;
const [check, setCheck] = useState([]); const [check, setCheck] = useState([]);
@ -22,7 +23,7 @@ function Setup (props) {
setCheck(checkItem ? JSON.parse(checkItem) : []) setCheck(checkItem ? JSON.parse(checkItem) : [])
ischeck(); ischeck();
}, []); }, []);
function ischeck (value) { function ischeck(value) {
if (check.length >= length) { if (check.length >= length) {
if (check.includes(value)) { if (check.includes(value)) {
return false; return false;
@ -57,7 +58,7 @@ function Setup (props) {
</div> </div>
} }
visible={true} visible={true}
style={{ width: 600 }} style={{ width: objWidth?.modalWidth || 600 }}
onOk={() => { onOk={() => {
localStorage.setItem(tableType, JSON.stringify(check)); localStorage.setItem(tableType, JSON.stringify(check));
close(); close();
@ -82,7 +83,7 @@ function Setup (props) {
<div <div
key={index} key={index}
style={{ style={{
width: 550, width: objWidth?.tableListWidth || 550,
border: "1px solid #EAEAEA", border: "1px solid #EAEAEA",
padding: "0px 5px", padding: "0px 5px",
borderRadius: 4, borderRadius: 4,
@ -98,7 +99,7 @@ function Setup (props) {
> >
{item.title} {item.title}
</div> </div>
<div style={{ padding: "15px 12px", width: 530 }}> <div style={{ padding: "15px 12px", width: objWidth?.listWidth || 530 }}>
{item.list?.map((itm) => { {item.list?.map((itm) => {
return ( return (
<Checkbox <Checkbox

42
web/client/src/sections/business/actions/achievement-report.js

@ -0,0 +1,42 @@
'use strict';
import { ApiTable, basicAction } from '$utils'
//回款明细表
export function getReceivedDetail(query) {
return (dispatch) => basicAction({
type: "get",
dispatch: dispatch,
actionType: "GET_RECEIVED_DETAIL",
query: query,
url: `${ApiTable.getReceivedDetail}`,
msg: { option: "查询回款明细表" },
reducer: { name: "ReceivedDetail", params: { noClear: true } },
});
}
//业绩明细表
export function getAchievementDetail(query) {
return (dispatch) => basicAction({
type: "get",
dispatch: dispatch,
actionType: "GET_ACHIEVEMENT_DETAIL",
query: query,
url: `${ApiTable.getAchievementDetail}`,
msg: { option: "查询业绩明细表" },
reducer: { name: "AchievementDetail", params: { noClear: true } },
});
}
//查询合同明细表
export function getContractDetail(query) {
return (dispatch) => basicAction({
type: "get",
dispatch: dispatch,
actionType: "GET_CONTRACT_DETAIL",
query: query,
url: `${ApiTable.getContractDetail}`,
msg: { option: "查询合同明细表" },
reducer: { name: "ContractDetail", params: { noClear: true } },
});
}

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

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

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

69
web/client/src/sections/business/constants/index.js

@ -3,3 +3,72 @@ export const RESERVEITEM_TYPE = {
depSummary: 2, depSummary: 2,
lostStatistic: 3 lostStatistic: 3
} }
export const contractDetailsColumnKeys = {
year: '年度',
serialNo: '序号',
number: '编号',
introduction: '简介',
contractNo: '合同编号',
applyDate: '申请日期',
recConDate: '收到合同日期',
contractPaper: '合同纸质版情况',
contractElec: '合同电子版情况',
department: '部门',
business: '业务线',
sale: '销售人员',
customer: '客户名称',
item: '项目名称',
itemType: '项目类型',
amount: '合同金额',
changeAmount: '变更后合同金额',
cInvoicedAmount: '累计开票金额',
cBackAmount: '累计回款金额',
invoicedBack: '开票-回款',
unInvoicedAmount: '未开票金额',
remainConAmount: '剩余合同金额',
backPercent: '回款率',
retentionMoney: '质保金',
retentionPercent: '质保金比例',
retentionTerm: '质保期',
invoiceTax1: '发票税率金额13%',
invoiceTax2: '发票税率金额9%',
invoiceTax3: '发票税率金额6%',
payType: '合同付款方式',
cost: '预支提成及委外费用',
performanceRatio: '业绩折算比例',
realPerformance: '实际业绩',
assessmentPerformance: '考核业绩',
acceptanceDate: '验收日期',
backConfirmDate: '收入确认时间',
recYear: '接单年份',
recMonth: '接单月份',
cusAttribute: '客户属性',
cusType: '客户类型',
industry: '行业',
source: '信息来源',
cusProvince: '客户省份',
cusArea: '项目所在地',
text: '备注',
}
export const invoicingDetailsColumnKeys = {
year: '年度',
serialNo: '序号',
number: '编号',
department: '部门',
sale: '销售人员',
contractNo: '合同编号',
customer: '客户名称',
item: '项目名称',
amount: '合同金额',
changeAmount: '变更后合同金额',
invoiceNo: '发票号码',
invoiceType: '开票类型',
invoiceDate: '开票日期',
invoiceAmount: '开票金额',
outputTax: '销项税额',
total: '合计',
taxRate13: '税率13%',
taxRate9: '税率9%',
taxRate6: '税率6%'
}

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

@ -1,18 +1,216 @@
import React, { useEffect, useState } from 'react'; import React, { useRef, useEffect, useState, useMemo } from 'react';
import { connect } from 'react-redux'; 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'; import '../../style.less';
const AchievementDetails = (props) => { const AchievementDetails = (props) => {
const { dispatch, actions, user } = props
const { businessManagement } = actions
const [keywordTarget, setKeywordTarget] = useState('saler');
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 = [ useEffect(() => {
{ getAchievementDetails()
title: '序号', }, []);
dataIndex: 'index',
render: (text, record, index) => index + 1 useEffect(() => {
}, getAchievementDetails()
]; }, [query])
const data = [];
function getAchievementDetails() {
dispatch(businessManagement.getAchievementDetail({ keywordTarget, keyword, ...query })).then(r => {
if (r.success) {
setTableData(r.payload?.data?.rows);
setLimits(r.payload?.data?.count)
}
})
}
const columns = [{
title: '收到合同日期',
dataIndex: 'recConDate',
key: 'recConDate',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '月份',
dataIndex: 'month',
key: 'month',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '部门',
dataIndex: 'department',
key: 'department',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '销售人员',
dataIndex: 'sale',
key: 'sale',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '客户名称',
dataIndex: 'customer',
key: 'customer',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '项目名称',
dataIndex: 'item',
key: 'item',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '合同金额',
dataIndex: 'amount',
key: 'amount',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '实际业绩',
dataIndex: 'realPerformance',
key: 'realPerformance',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '考核业绩',
dataIndex: 'assessmentPerformance',
key: 'assessmentPerformance',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '价格是否特批',
dataIndex: 'isApproval',
key: 'isApproval',
width: 120,
render: (text, record) => <span>{text ? '是' : '否'}</span>
}, {
title: '特批折算比例',
dataIndex: 'approvalProp',
key: 'approvalProp',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '预支提成及委外费用',
dataIndex: 'cost',
key: 'cost',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '业务线',
dataIndex: 'serviceLine',
key: 'serviceLine',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '客户类型',
dataIndex: 'cusType',
key: 'cusType',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '行业',
dataIndex: 'industry',
key: 'industry',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '信息来源',
dataIndex: 'source',
key: 'source',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '项目类型',
dataIndex: 'itemType',
key: 'itemType',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '客户省份',
dataIndex: 'cusProvince',
key: 'cusProvince',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '客户属性',
dataIndex: 'cusAttribute',
key: 'cusAttribute',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '复购次数',
dataIndex: 'repurchaseCount',
key: 'repurchaseCount',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '是否可复制的业务路径',
dataIndex: 'reproducible',
key: 'reproducible',
width: 120,
render: (text, record) => <span>{text ? '是' : '否'}</span>
}, {
title: '省外业务1.1',
dataIndex: 'outProvince',
key: 'outProvince',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '复购业务1.05',
dataIndex: 'repurchase',
key: 'repurchase',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '可复制的业务路径1.1',
dataIndex: 'isreproduce',
key: 'isreproduce',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}]
function handleRow(record, index) {//
if (index % 2 === 0) {
return {
style: {
background: '#FAFCFF',
}
};
} else {
return {};
}
}
const exportAllData = () => {
dispatch(businessManagement.getAchievementDetail({ limit: 1 })).then((res) => {//
if (res.success) {
if (res.payload.data.count) {
let url = `detail/achievement?token=${user.token}&toExport=1&timestamp=${moment().valueOf()}`
setExportUrl(url);
} else {
Toast.info({
content: '暂无可导出的数据',
duration: 3,
})
}
}
})
}
const scroll = useMemo(() => ({}), []);
return ( return (
<> <>
<div style={{ padding: '0px 12px' }}> <div style={{ padding: '0px 12px' }}>
@ -24,9 +222,94 @@ const AchievementDetails = (props) => {
<div style={{ color: '#033C9A', fontSize: 14 }}>业绩明细表</div> <div style={{ color: '#033C9A', fontSize: 14 }}>业绩明细表</div>
</div> </div>
<div style={{ background: '#FFFFFF', boxShadow: '0px 0px 12px 2px rgba(220,222,224,0.2)', borderRadius: 2, padding: '20px ', marginTop: 9 }}> <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>
<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='saler'>销售人员</Select.Option>
<Select.Option value='line'>业务线</Select.Option>
<Select.Option value='dep'>部门</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: '#0F7EFB', color: '#FFFFFF', fontSize: 14, cursor: "pointer" }}
onClick={() => { setImportModalV(true); }}>
导入
</div>
<div style={{ padding: '6px 20px', background: '#00BA85', color: '#FFFFFF', fontSize: 14, cursor: "pointer", marginLeft: 18 }}
onClick={() => {
exportAllData()
}}>
导出全部
</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>
{
exportUrl ? <iframe src={`/_api/${exportUrl}`} style={{ display: 'none' }} /> : ''
}
</div> </div>
</> </>
) )

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

@ -1,18 +1,180 @@
import React, { useEffect, useState } from 'react'; import React, { useRef, useEffect, useState, useMemo } from 'react';
import { connect } from 'react-redux'; 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'; import '../../style.less';
const BackMoneyDetails = (props) => { const BackMoneyDetails = (props) => {
const { dispatch, actions, user } = props
const { businessManagement } = actions
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 [exportUrl, setExportUrl] = useState('')
const page = useRef(query.page);
function seachValueChange(value) {
setKeyword(value)
}
const columns = [ useEffect(() => {
{ getBackMoneyDetails()
}, []);
useEffect(() => {
getBackMoneyDetails()
}, [query])
function getBackMoneyDetails() {
dispatch(businessManagement.getReceivedDetail({ keywordTarget, keyword, ...query })).then(r => {
if (r.success) {
setTableData(r.payload?.data?.rows);
setLimits(r.payload?.data?.count)
}
})
}
const columns = [{
title: '年度',
dataIndex: 'year',
key: 'year',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '序号', title: '序号',
dataIndex: 'index', dataIndex: 'serialNo',
render: (text, record, index) => index + 1 key: 'serialNo',
}, width: 120,
]; render: (text, record) => <span>{text || '-'}</span>
const data = []; }, {
title: '编号',
dataIndex: 'number',
key: 'number',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '部门',
dataIndex: 'department',
key: 'department',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '销售人员',
dataIndex: 'sale',
key: 'sale',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '合同编号',
dataIndex: 'contractNo',
key: 'contractNo',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '客户名称',
dataIndex: 'customer',
key: 'customer',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '项目名称',
dataIndex: 'item',
key: 'item',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '合同金额',
dataIndex: 'amount',
key: 'amount',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '变更后合同金额',
dataIndex: 'changeAmount',
key: 'changeAmount',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '回款年份',
dataIndex: 'receivableYear',
key: 'receivableYear',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '回款日期',
dataIndex: 'receivableDate',
key: 'receivableDate',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '回款金额',
dataIndex: 'receivableAmount',
key: 'receivableAmount',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '开票-回款',
dataIndex: 'invoicedBack',
key: 'invoicedBack',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '剩余合同金额',
dataIndex: 'remainConAmount',
key: 'remainConAmount',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '收入确认时间',
dataIndex: 'incomeConfirmdate',
key: 'incomeConfirmdate',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '第三方付款单位',
dataIndex: 'thirdPayment',
key: 'thirdPayment',
width: 140,
render: (text, record) => <span>{text || '-'}</span>
}, {
title: '备注',
dataIndex: 'remark',
key: 'remark',
width: 120,
render: (text, record) => <span>{text || '-'}</span>
}]
function handleRow(record, index) {//
if (index % 2 === 0) {
return {
style: {
background: '#FAFCFF',
}
};
} else {
return {};
}
}
const exportAllData = () => {
dispatch(businessManagement.getReceivedDetail({ limit: 1 })).then((res) => {//
if (res.success) {
if (res.payload.data.count) {
let url = `detail/received/back?token=${user.token}&toExport=1&timestamp=${moment().valueOf()}`
setExportUrl(url);
} else {
Toast.info({
content: '暂无可导出的数据',
duration: 3,
})
}
}
})
}
const scroll = useMemo(() => ({}), []);
return ( return (
<> <>
<div style={{ padding: '0px 12px' }}> <div style={{ padding: '0px 12px' }}>
@ -24,9 +186,94 @@ const BackMoneyDetails = (props) => {
<div style={{ color: '#033C9A', fontSize: 14 }}>回款明细表</div> <div style={{ color: '#033C9A', fontSize: 14 }}>回款明细表</div>
</div> </div>
<div style={{ background: '#FFFFFF', boxShadow: '0px 0px 12px 2px rgba(220,222,224,0.2)', borderRadius: 2, padding: '20px ', marginTop: 9 }}> <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>
<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' }}>
<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, cursor: "pointer", marginLeft: 18 }}
onClick={() => {
exportAllData()
}}>
导出全部
</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>
{
exportUrl ? <iframe src={`/_api/${exportUrl}`} style={{ display: 'none' }} /> : ''
}
</div> </div>
</> </>
) )

232
web/client/src/sections/business/containers/performanceReport/contractDetails.jsx

@ -1,18 +1,114 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useRef, useMemo } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Table } from '@douyinfe/semi-ui'; import { Select, Input, Button, Table, Pagination, Skeleton, Toast, } from '@douyinfe/semi-ui';
import { SkeletonScreen, Setup } from "$components";
import { IconSearch } from '@douyinfe/semi-icons';
import ImportContractDetailsModal from './importContractDetailsModal';
import { contractDetailsColumnKeys } from '../../constants/index';
import '../../style.less'; import '../../style.less';
import moment from 'moment'
const ContractDetails = (props) => { const ContractDetails = (props) => {
const { dispatch, actions } = props
const { businessManagement } = actions;
const [keywordTarget, setKeywordTarget] = useState('contractNo');
const [keyword, setKeyword] = useState('');//
const [limits, setLimits] = useState()//
const [query, setQuery] = useState({ limit: 10, page: 0 }); //
const [tableData, setTableData] = useState([]);
const [importModalV, setImportModalV] = useState(false);
const columns = [ const [setup, setSetup] = useState(false);//
{ const [setupp, setSetupp] = useState([]);//
title: '序号',
dataIndex: 'index', const [exportUrl, setExportUrl] = useState('');
render: (text, record, index) => index + 1 const page = useRef(query.page);
},
]; const CONTRACTDETAILS = "contractDetails";
const data = []; const renderColumns = (columnKeys) => {
let columns = [];
Object.keys(columnKeys).map(key => {
tableList[0].list.push({ name: columnKeys[key], value: key });
switch (key) {
default:
columns.push({
title: columnKeys[key], dataIndex: key, key: key,
render: (text, record) => text === 0 ? text : text ? text : '—', width: 32 + columnKeys[key].length * 16
});
break;
}
})
return columns;
}
const tableList = [{
title: '基础信息',
list: []
}];
const columns = renderColumns(contractDetailsColumnKeys);
function seachValueChange(value) {
setKeyword(value)
}
useEffect(() => {
localStorage.getItem(CONTRACTDETAILS) == null
? localStorage.setItem(
CONTRACTDETAILS,
JSON.stringify(['year', 'serialNo', 'number', 'introduction', 'contractNo', 'applyDate', 'recConDate', 'department', 'business', 'sale', 'customer', 'item', 'itemType', 'amount'])
)
: "";
attribute();
getContractDetailData();
}, []);
//
function attribute() {
const arr = localStorage.getItem(CONTRACTDETAILS)
? JSON.parse(localStorage.getItem(CONTRACTDETAILS))
: [];
let newColumns = [];
for (let i = 0; i < columns.length; i++) {
if (arr.indexOf(columns[i].key) > -1) {
newColumns.push(columns[i]);
}
}
setSetupp(newColumns);
}
function getContractDetailData(param) {
let queryParam = param || query;
dispatch(businessManagement.getContractDetail({ keywordTarget, keyword, ...queryParam })).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 exportAllData = () => {
dispatch(businessManagement.getContractDetail({ limit: 1, page: 0 })).then((res) => {
if (res.success) {
if (res.payload.data.count) {
let url = `export/contract/detail?token=${user.token}&timestamp=${moment().valueOf()}`
setExportUrl(url);
} else {
Toast.info({
content: '暂无可导出的数据',
duration: 3,
})
}
}
})
}
const scroll = useMemo(() => ({}), []);
return ( return (
<> <>
<div style={{ padding: '0px 12px' }}> <div style={{ padding: '0px 12px' }}>
@ -23,11 +119,123 @@ const ContractDetails = (props) => {
<div style={{ color: '#033C9A', fontSize: 14, margin: '0px 8px' }}>/</div> <div style={{ color: '#033C9A', fontSize: 14, margin: '0px 8px' }}>/</div>
<div style={{ color: '#033C9A', fontSize: 14 }}>合同明细表</div> <div style={{ color: '#033C9A', fontSize: 14 }}>合同明细表</div>
</div> </div>
<div style={{ background: '#FFFFFF', boxShadow: '0px 0px 12px 2px rgba(220,222,224,0.2)', borderRadius: 2, padding: '20px ', marginTop: 9 }}> <div style={{ background: '#FFFFFF', boxShadow: '0px 0px 12px 2px rgba(220,222,224,0.2)', borderRadius: 2, padding: '20px 0px 20px 19px ', marginTop: 12 }}>
<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", }}>CONTRACT 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='contractNo'>合同编号</Select.Option>
<Select.Option value='department'>部门</Select.Option>
<Select.Option value='business'>业务线</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 })
getContractDetailData({ limit: 10, page: 0 })
}}>查询</Button>
</div>
<div style={{ display: 'flex', marginRight: 20 }}>
<img src="/assets/images/hrImg/newsetup.png" alt="" style={{ width: 20, height: 20, cursor: "pointer", marginRight: 15, marginTop: 6 }}
onClick={() => setSetup(true)}
/>
<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 }} onClick={() => {
exportAllData()
}}>
导出全部
</div>
</div>
</div> </div>
<div style={{ marginTop: 20 }}>
<Skeleton
loading={false}
active={true}
placeholder={SkeletonScreen()}
>
<Table
columns={setupp.filter((s) => s)}
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 });
getContractDetailData({ limit: pageSize, page: currentPage - 1 });
page.current = currentPage - 1;
}}
/>
</div>
</div>
</div>
</div>
</div>
{
importModalV ? <ImportContractDetailsModal
onCancel={() => {
setImportModalV(false);
}} /> : ''
}
{
exportUrl ? <iframe src={`/_api/${exportUrl}`} style={{ display: 'none' }} /> : ''
}
</div> </div>
{setup ? (
<Setup
tableType={CONTRACTDETAILS}
tableList={tableList}
length={45}
objWidth={{ modalWidth: 720, tableListWidth: 670, listWidth: 650 }}
close={() => {
setSetup(false);
attribute();
}}
/>
) : (
""
)}
</> </>
) )
} }

175
web/client/src/sections/business/containers/performanceReport/importContractDetailsModal.js

@ -0,0 +1,175 @@
'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 XLSX from 'xlsx';
import { contractDetailsColumnKeys } from '../../constants/index';
//下载模板和上传文件读取
const ImportContractDetailsModal = props => {
const { dispatch, actions, onCancel } = props;
const { humanAffairs } = actions;
const [msg, setMsg] = useState('');
const [loading, setLoading] = useState('');
const [postData, setPostData] = useState([]);
//初始化
useEffect(() => {
}, []);
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 = [];
Object.keys(contractDetailsColumnKeys).map(key => {
head.push(contractDetailsColumnKeys[key]);
})
head = head.join(',') + "\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 = () => {
dldCsvMb();
}
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 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 = [];
for (let i = 0; i < res.length; i++) {
let d = res[i];
let obj = {};
Object.keys(contractDetailsColumnKeys).map(key => {
obj[key] = d[contractDetailsColumnKeys[key]] || '';
})
postData.push(obj);
}
setPostData(postData)
let msg = '文件解析完成,点击确定按钮上传保存!'
setMsg(msg)
onSuccess({ message: msg })
})
}}>
<Button icon={<IconUpload />} theme="light">
请选择文件
</Button>
</Form.Upload>
<span>{msg}</span>
<div style={{ color: '#ccc' }}>最大不超过200M导入文件需与
<span onClick={() => download()} style={{ cursor: 'pointer', color: '#0066FF' }}>导入模板</span>
一致</div>
</Form>
</Modal >
)
}
function mapStateToProps(state) {
const { auth, global } = state;
return {
user: auth.user,
actions: global.actions,
}
}
export default connect(mapStateToProps)(ImportContractDetailsModal);

175
web/client/src/sections/business/containers/performanceReport/importInvoicingDetailsModal.js

@ -0,0 +1,175 @@
'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 XLSX from 'xlsx';
import { invoicingDetailsColumnKeys } from '../../constants/index';
//下载模板和上传文件读取
const ImportInvoicingDetailsModal = props => {
const { dispatch, actions, onCancel } = props;
const { humanAffairs } = actions;
const [msg, setMsg] = useState('');
const [loading, setLoading] = useState('');
const [postData, setPostData] = useState([]);
//初始化
useEffect(() => {
}, []);
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 = [];
Object.keys(invoicingDetailsColumnKeys).map(key => {
head.push(invoicingDetailsColumnKeys[key]);
})
head = head.join(',') + "\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 = () => {
dldCsvMb();
}
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 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 = [];
for (let i = 0; i < res.length; i++) {
let d = res[i];
let obj = {};
Object.keys(invoicingDetailsColumnKeys).map(key => {
obj[key] = d[invoicingDetailsColumnKeys[key]] || '';
})
postData.push(obj);
}
setPostData(postData)
let msg = '文件解析完成,点击确定按钮上传保存!'
setMsg(msg)
onSuccess({ message: msg })
})
}}>
<Button icon={<IconUpload />} theme="light">
请选择文件
</Button>
</Form.Upload>
<span>{msg}</span>
<div style={{ color: '#ccc' }}>最大不超过200M导入文件需与
<span onClick={() => download()} style={{ cursor: 'pointer', color: '#0066FF' }}>导入模板</span>
一致</div>
</Form>
</Modal >
)
}
function mapStateToProps(state) {
const { auth, global } = state;
return {
user: auth.user,
actions: global.actions,
}
}
export default connect(mapStateToProps)(ImportInvoicingDetailsModal);

195
web/client/src/sections/business/containers/performanceReport/invoicingDetails.jsx

@ -1,18 +1,86 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useRef, useMemo } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Table } from '@douyinfe/semi-ui'; import { Select, Input, Button, Table, Pagination, Skeleton } from '@douyinfe/semi-ui';
import { SkeletonScreen, Setup } from "$components";
import { IconSearch } from '@douyinfe/semi-icons';
import ImportInvoicingDetailsModal from './importInvoicingDetailsModal';
import { invoicingDetailsColumnKeys } from '../../constants/index';
import '../../style.less'; import '../../style.less';
import moment from 'moment'
const InvoicingDetails = (props) => { const InvoicingDetails = (props) => {
const { dispatch, actions } = props
const { } = actions;
const [keywordTarget, setKeywordTarget] = useState('contractNo');
const [keyword, setKeyword] = useState('');//
const [limits, setLimits] = useState()//
const [query, setQuery] = useState({ limit: 10, page: 0 }); //
const [tableData, setTableData] = useState([]);
const [importModalV, setImportModalV] = useState(false);
const columns = [ const [setup, setSetup] = useState(false);//
{ const [setupp, setSetupp] = useState([]);//
title: '序号', const INVOICINGDETAILS = "invoicingDetails";
dataIndex: 'index', const renderColumns = (columnKeys) => {
render: (text, record, index) => index + 1 let columns = [];
}, Object.keys(columnKeys).map(key => {
]; tableList[0].list.push({ name: columnKeys[key], value: key });
const data = []; switch (key) {
default:
columns.push({
title: columnKeys[key], dataIndex: key, key: key,
render: (text, record) => text === 0 ? text : text ? text : '—', width: 32 + columnKeys[key].length * 16
});
break;
}
})
return columns;
}
const tableList = [{
title: '基础信息',
list: []
}];
const columns = renderColumns(invoicingDetailsColumnKeys);
function seachValueChange(value) {
setKeyword(value)
}
useEffect(() => {
localStorage.getItem(INVOICINGDETAILS) == null
? localStorage.setItem(
INVOICINGDETAILS,
JSON.stringify(['year', 'serialNo', 'number', 'department', 'sale', 'contractNo', 'customer', 'item', 'amount', 'changeAmount', 'invoiceNo', 'invoiceDate'])
)
: "";
attribute();
}, []);
//
function attribute() {
const arr = localStorage.getItem(INVOICINGDETAILS)
? JSON.parse(localStorage.getItem(INVOICINGDETAILS))
: [];
let newColumns = [];
for (let i = 0; i < columns.length; i++) {
if (arr.indexOf(columns[i].key) > -1) {
newColumns.push(columns[i]);
}
}
setSetupp(newColumns);
}
useEffect(() => {
}, [query])
function handleRow(record, index) {//
if (index % 2 === 0) {
return {
style: {
background: '#FAFCFF',
}
};
} else {
return {};
}
}
const scroll = useMemo(() => ({}), []);
return ( return (
<> <>
<div style={{ padding: '0px 12px' }}> <div style={{ padding: '0px 12px' }}>
@ -23,11 +91,114 @@ const InvoicingDetails = (props) => {
<div style={{ color: '#033C9A', fontSize: 14, margin: '0px 8px' }}>/</div> <div style={{ color: '#033C9A', fontSize: 14, margin: '0px 8px' }}>/</div>
<div style={{ color: '#033C9A', fontSize: 14 }}>开票明细表</div> <div style={{ color: '#033C9A', fontSize: 14 }}>开票明细表</div>
</div> </div>
<div style={{ background: '#FFFFFF', boxShadow: '0px 0px 12px 2px rgba(220,222,224,0.2)', borderRadius: 2, padding: '20px ', marginTop: 9 }}> <div style={{ background: '#FFFFFF', boxShadow: '0px 0px 12px 2px rgba(220,222,224,0.2)', borderRadius: 2, padding: '20px 0px 20px 19px ', marginTop: 12 }}>
<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", }}>INVOICING 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='contractNo'>合同编号</Select.Option>
<Select.Option value='invoiceNo'>发票号码</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 }}>
<img src="/assets/images/hrImg/newsetup.png" alt="" style={{ width: 20, height: 20, cursor: "pointer", marginRight: 15, marginTop: 6 }}
onClick={() => setSetup(true)}
/>
<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>
<div style={{ marginTop: 20 }}>
<Skeleton
loading={false}
active={true}
placeholder={SkeletonScreen()}
>
<Table
columns={setupp.filter((s) => s)}
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>
{
importModalV ? <ImportInvoicingDetailsModal
onCancel={() => {
setImportModalV(false);
}} /> : ''
}
</div> </div>
{setup ? (
<Setup
tableType={INVOICINGDETAILS}
tableList={tableList}
length={19}
close={() => {
setSetup(false);
attribute();
}}
/>
) : (
""
)}
</> </>
) )
} }

264
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 { 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'; import '../../style.less';
const SalesDistributionDetails = (props) => { 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 = [ 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: '序号', title: '序号',
dataIndex: 'index', dataIndex: 'id',
key: 'id',
width: 60,
render: (text, record, index) => index + 1 render: (text, record, index) => index + 1
}, }, {
]; title: starHeader('姓名'),
const data = []; 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 ( return (
<> <>
<div style={{ padding: '0px 12px' }}> <div style={{ padding: '0px 12px' }}>
@ -24,9 +184,97 @@ const SalesDistributionDetails = (props) => {
<div style={{ color: '#033C9A', fontSize: 14 }}>销售人员分布明细表</div> <div style={{ color: '#033C9A', fontSize: 14 }}>销售人员分布明细表</div>
</div> </div>
<div style={{ background: '#FFFFFF', boxShadow: '0px 0px 12px 2px rgba(220,222,224,0.2)', borderRadius: 2, padding: '20px ', marginTop: 9 }}> <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", }}>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>
<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> </div>
</> </>
) )

6
web/client/src/sections/business/nav-item.jsx

@ -11,7 +11,7 @@ export function getNavItem(user, dispatch) {
items: [{ items: [{
itemKey: 'pmReport', itemKey: 'pmReport',
text: '项目报表', text: '项目报表',
icon: <iconpark-icon style={{ width: 20, height: 20 }} name="iconcbzhongceng"></iconpark-icon>, icon: <iconpark-icon style={{ width: 20, height: 20 }} name="iconxiangmubaobiao"></iconpark-icon>,
to: '/businessManagement/pmReport/reserveItemsReporting', to: '/businessManagement/pmReport/reserveItemsReporting',
items: [{ items: [{
itemKey: 'reserveItemsReporting', itemKey: 'reserveItemsReporting',
@ -29,7 +29,7 @@ export function getNavItem(user, dispatch) {
}, { }, {
itemKey: 'performanceReport', itemKey: 'performanceReport',
text: '业绩报表', text: '业绩报表',
icon: <iconpark-icon style={{ width: 20, height: 20 }} name="iconcbzhongceng"></iconpark-icon>, icon: <iconpark-icon style={{ width: 20, height: 20 }} name="iconyejibaobiao"></iconpark-icon>,
to: '/businessManagement/performanceReport/contractDetails', to: '/businessManagement/performanceReport/contractDetails',
items: [{ items: [{
itemKey: 'contractDetails', itemKey: 'contractDetails',
@ -47,7 +47,7 @@ export function getNavItem(user, dispatch) {
}, { }, {
itemKey: 'salesReport', itemKey: 'salesReport',
text: '销售报表', text: '销售报表',
icon: <iconpark-icon style={{ width: 20, height: 20 }} name="iconcbzhongceng"></iconpark-icon>, icon: <iconpark-icon style={{ width: 20, height: 20 }} name="iconxiaoshoubaobiao"></iconpark-icon>,
to: '/businessManagement/salesReport/salesDistributionDetails', to: '/businessManagement/salesReport/salesDistributionDetails',
items: [{ items: [{
itemKey: 'salesDistributionDetails', itemKey: 'salesDistributionDetails',

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

@ -18,7 +18,12 @@ export const ApiTable = {
//项目报表 //项目报表
getReserveItemReport: "reserveItem/report/{type}", getReserveItemReport: "reserveItem/report/{type}",
//销售人员分布明细表
getSalesList: 'sales/member/list',
//业绩报表
getReceivedDetail: 'detail/received/back',
getAchievementDetail: 'detail/achievement',
getContractDetail: 'contract/detail',
}; };
export const RouteTable = { export const RouteTable = {
apiRoot: "/api/root", apiRoot: "/api/root",

Loading…
Cancel
Save