Browse Source

(*)合并后的代码

master
ww664853070 2 years ago
parent
commit
f55e2780de
  1. 3
      .vscode/launch.json
  2. 158
      api/app/lib/controllers/report/achievement.js
  3. 50
      api/app/lib/models/performance_detail.js
  4. 28
      api/app/lib/models/receivable_detail.js
  5. 20
      api/app/lib/routes/report/index.js
  6. 18
      api/app/lib/utils/xlsxDownload.js
  7. 1
      web/client/src/layout/actions/global.js
  8. 592
      web/client/src/layout/containers/layout/index.jsx
  9. 2
      web/client/src/layout/reducers/global.js
  10. 10
      web/client/src/sections/auth/actions/auth.js
  11. 47
      web/client/src/sections/business/actions/achievement-report.js
  12. 48
      web/client/src/sections/business/constants/index.js
  13. 175
      web/client/src/sections/business/containers/performanceReport/achievementDetails.jsx
  14. 139
      web/client/src/sections/business/containers/performanceReport/backMoneyDetails.jsx
  15. 14
      web/client/src/sections/business/containers/performanceReport/contractDetails.jsx
  16. 220
      web/client/src/sections/business/containers/performanceReport/importAchieveModal.jsx
  17. 256
      web/client/src/sections/business/containers/performanceReport/importBackModal.jsx
  18. 80
      web/client/src/sections/business/containers/performanceReport/invoicingDetails.jsx
  19. 42
      web/client/src/utils/webapi.js
  20. 10
      web/config.js
  21. 3
      web/package.json
  22. 4
      web/routes/attachment/index.js

3
.vscode/launch.json

@ -45,7 +45,8 @@
"-g postgres://FashionAdmin:123456@10.8.30.36:5432/data_center", "-g postgres://FashionAdmin:123456@10.8.30.36:5432/data_center",
"--redisHost localhost", "--redisHost localhost",
"--redisPort 6379", "--redisPort 6379",
"--apiEmisUrl http://10.8.30.161:1111", "--apiEmisUrl http://10.8.30.103:14000", //
// "--apiEmisUrl http://10.8.30.161:1111", //
// //
// //
"--qnak XuDgkao6cL0HidoMAPnA5OB10Mc_Ew08mpIfRJK5", "--qnak XuDgkao6cL0HidoMAPnA5OB10Mc_Ew08mpIfRJK5",

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

@ -257,6 +257,73 @@ async function exportAchievementDetail(ctx, dataList) {
} }
} }
async function getReceivedNumbers(ctx) {
try {
const models = ctx.fs.dc.models;
let list = await models.ReceivableDetail.findAll({//查编号
attributes: ['number']
});
ctx.status = 200
ctx.body = list;
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : undefined
}
}
}
async function importBackDetails(ctx) {
let errorMsg = { message: '导入回款明细失败' };
const transaction = await ctx.fs.dc.orm.transaction();
try {
const models = ctx.fs.dc.models;
const data = ctx.request.body;
let addArr = [];
let dataList = await models.ReceivableDetail.findAll({//查编号
attributes: ['number']
});
data.map(d => {
let exist = dataList.find(m => m.dataValues.number == d.number);
if (!exist) {
addArr.push(d);
}
})
//只处理新增的
if (addArr.length) {
await models.ReceivableDetail.bulkCreate(addArr);
}
await transaction.commit();
ctx.status = 204;
} catch (error) {
await transaction.rollback();
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = errorMsg;
}
}
async function importAchieveDetails(ctx) {
let errorMsg = { message: '导入业绩明细失败' };
const transaction = await ctx.fs.dc.orm.transaction();
try {
const models = ctx.fs.dc.models;
const data = ctx.request.body;
//业绩明细 没有唯一标识,前端校验后,导啥存啥
if (data.length) {
await models.PerformanceDetail.bulkCreate(data);
}
await transaction.commit();
ctx.status = 204;
} catch (error) {
await transaction.rollback();
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = errorMsg;
}
}
/** /**
* 查询合同明细表数据 * 查询合同明细表数据
* @param {*} ctx ctx ctx.query:{keywordTarget-关键字项keyword-关键字内容limit-页宽, page-页码} * @param {*} ctx ctx ctx.query:{keywordTarget-关键字项keyword-关键字内容limit-页宽, page-页码}
@ -283,8 +350,99 @@ async function getContractDetail(ctx) {
ctx.body = { name: 'FindError', message: '查询合同明细表数据失败' } ctx.body = { name: 'FindError', message: '查询合同明细表数据失败' }
} }
} }
/**
* 查询开票明细表数据
* @param {*} ctx ctx ctx.query:{keywordTarget-关键字项keyword-关键字内容limit-页宽, page-页码}
*/
async function getInvoicingDetail(ctx) {
try {
const { models } = ctx.fs.dc;
const { keywordTarget, keyword, limit, page } = ctx.query;
const where = {};
if (keywordTarget && keyword) {
where[keywordTarget] = { $iLike: `%${keyword}%` };
}
let invoiceDetail = await models.InvoiceDetail.findAndCountAll({
where: where,
offset: Number(page) * Number(limit),
limit: Number(limit),
order: [['id', 'DESC']]
});
ctx.status = 200
ctx.body = invoiceDetail;
} catch (error) {
ctx.fs.logger.error(`path:${ctx.path},error:${error}`)
ctx.status = 400;
ctx.body = { name: 'FindError', message: '查询开票明细表数据失败' }
}
}
/**
* 导出合同明细表数据
*/
async function exportContractDetail(ctx) {
try {
const { models } = ctx.fs.dc;
let exportData = await models.ContractDetail.findAll({
order: [['id', 'DESC']]
});
const { utils: { simpleExcelDown, contractDetailsColumnKeys } } = ctx.app.fs;
let header = [];
Object.keys(contractDetailsColumnKeys).map(key => {
header.push({ title: contractDetailsColumnKeys[key], key: key });
})
const fileName = `合同明细表_${moment().format('YYYYMMDDHHmmss')}` + '.xlsx'
const filePath = await simpleExcelDown({ data: exportData, header, fileName: fileName, needIndexCell: false })
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 = { name: 'ExportAllError', message: '导出合同明细表数据失败' }
}
}
/**
* 导出开票明细表数据
*/
async function exportInvoicingDetail(ctx) {
try {
const { models } = ctx.fs.dc;
let exportData = await models.InvoiceDetail.findAll({
order: [['id', 'DESC']]
});
const { utils: { simpleExcelDown, invoicingDetailsColumnKeys } } = ctx.app.fs;
let header = [];
Object.keys(invoicingDetailsColumnKeys).map(key => {
header.push({ title: invoicingDetailsColumnKeys[key], key: key });
})
const fileName = `开票明细表_${moment().format('YYYYMMDDHHmmss')}` + '.xlsx'
const filePath = await simpleExcelDown({ data: exportData, header, fileName: fileName, needIndexCell: false })
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 = { name: 'ExportAllError', message: '导出开票明细表数据失败' }
}
}
module.exports = { module.exports = {
getReceivedDetail,//回款 getReceivedDetail,//回款
getAchievementDetail,//业绩 getAchievementDetail,//业绩
getReceivedNumbers,//查询回款明细表 已有的所有编号
importBackDetails,//导入回款明细
importAchieveDetails,//导入业绩明细
getContractDetail, getContractDetail,
getInvoicingDetail,
exportContractDetail,
exportInvoicingDetail
} }

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

@ -13,11 +13,11 @@ module.exports = dc => {
comment: null, comment: null,
primaryKey: true, primaryKey: true,
field: "id", field: "id",
autoIncrement: false autoIncrement: true
}, },
recConDate: { recConDate: {
type: DataTypes.DATEONLY, type: DataTypes.DATEONLY,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "收到合同日期", comment: "收到合同日期",
primaryKey: false, primaryKey: false,
@ -26,7 +26,7 @@ module.exports = dc => {
}, },
month: { month: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "月份", comment: "月份",
primaryKey: false, primaryKey: false,
@ -35,7 +35,7 @@ module.exports = dc => {
}, },
department: { department: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "部门:申请部门", comment: "部门:申请部门",
primaryKey: false, primaryKey: false,
@ -44,7 +44,7 @@ module.exports = dc => {
}, },
sale: { sale: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "销售人员", comment: "销售人员",
primaryKey: false, primaryKey: false,
@ -53,7 +53,7 @@ module.exports = dc => {
}, },
customer: { customer: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "客户名称:【甲方名称】", comment: "客户名称:【甲方名称】",
primaryKey: false, primaryKey: false,
@ -62,7 +62,7 @@ module.exports = dc => {
}, },
item: { item: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "项目名称", comment: "项目名称",
primaryKey: false, primaryKey: false,
@ -71,7 +71,7 @@ module.exports = dc => {
}, },
amount: { amount: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "合同金额:【合同金额\n(元)】", comment: "合同金额:【合同金额\n(元)】",
primaryKey: false, primaryKey: false,
@ -80,7 +80,7 @@ module.exports = dc => {
}, },
realPerformance: { realPerformance: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "实际业绩;(合同金额-预\n支提成及委外费用)*特批折算比例\n(G-L)*K", comment: "实际业绩;(合同金额-预\n支提成及委外费用)*特批折算比例\n(G-L)*K",
primaryKey: false, primaryKey: false,
@ -89,7 +89,7 @@ module.exports = dc => {
}, },
assessmentPerformance: { assessmentPerformance: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "合同金额*省外业务(1.1)*复购业务\n(1.05)*可复制的业务路径(1.1)", comment: "合同金额*省外业务(1.1)*复购业务\n(1.05)*可复制的业务路径(1.1)",
primaryKey: false, primaryKey: false,
@ -98,7 +98,7 @@ module.exports = dc => {
}, },
isApproval: { isApproval: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "价格是否特批:根据折算比例-《合同明细表》推\n算(100%——“是”;其他为否)", comment: "价格是否特批:根据折算比例-《合同明细表》推\n算(100%——“是”;其他为否)",
primaryKey: false, primaryKey: false,
@ -107,7 +107,7 @@ module.exports = dc => {
}, },
approvalProp: { approvalProp: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "特批折算比例:【业绩折\n算比例】", comment: "特批折算比例:【业绩折\n算比例】",
primaryKey: false, primaryKey: false,
@ -116,7 +116,7 @@ module.exports = dc => {
}, },
cost: { cost: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "预支提成及委外费用", comment: "预支提成及委外费用",
primaryKey: false, primaryKey: false,
@ -125,7 +125,7 @@ module.exports = dc => {
}, },
serviceLine: { serviceLine: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "业务线", comment: "业务线",
primaryKey: false, primaryKey: false,
@ -134,7 +134,7 @@ module.exports = dc => {
}, },
cusType: { cusType: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "客户类型", comment: "客户类型",
primaryKey: false, primaryKey: false,
@ -143,7 +143,7 @@ module.exports = dc => {
}, },
industry: { industry: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "行业", comment: "行业",
primaryKey: false, primaryKey: false,
@ -152,7 +152,7 @@ module.exports = dc => {
}, },
source: { source: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "信息来源", comment: "信息来源",
primaryKey: false, primaryKey: false,
@ -161,7 +161,7 @@ module.exports = dc => {
}, },
itemType: { itemType: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "项目类型", comment: "项目类型",
primaryKey: false, primaryKey: false,
@ -170,7 +170,7 @@ module.exports = dc => {
}, },
cusProvince: { cusProvince: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "客户省份", comment: "客户省份",
primaryKey: false, primaryKey: false,
@ -179,7 +179,7 @@ module.exports = dc => {
}, },
cusAttribute: { cusAttribute: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "客户属性:G:政府、事业单位\nB:央企、国企、平台商\nC:资源方", comment: "客户属性:G:政府、事业单位\nB:央企、国企、平台商\nC:资源方",
primaryKey: false, primaryKey: false,
@ -188,7 +188,7 @@ module.exports = dc => {
}, },
repurchaseCount: { repurchaseCount: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "复购次数:同一客户储备成单-成单项目数量\n(PM)", comment: "复购次数:同一客户储备成单-成单项目数量\n(PM)",
primaryKey: false, primaryKey: false,
@ -197,7 +197,7 @@ module.exports = dc => {
}, },
reproducible: { reproducible: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "是否可复制的业务路径:《合同评审》表单获取", comment: "是否可复制的业务路径:《合同评审》表单获取",
primaryKey: false, primaryKey: false,
@ -206,7 +206,7 @@ module.exports = dc => {
}, },
outProvince: { outProvince: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "省外业务1.1:当【客户省份】=“江西”时,省外业务=1;当【客户省份】<>“江西”时,省外业务=1.1", comment: "省外业务1.1:当【客户省份】=“江西”时,省外业务=1;当【客户省份】<>“江西”时,省外业务=1.1",
primaryKey: false, primaryKey: false,
@ -215,7 +215,7 @@ module.exports = dc => {
}, },
repurchase: { repurchase: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "复购业务1.05:当【复购次数】\n<2时,复购业务=1;当【复购次数】\n>=2时,复购业务=1.05;", comment: "复购业务1.05:当【复购次数】\n<2时,复购业务=1;当【复购次数】\n>=2时,复购业务=1.05;",
primaryKey: false, primaryKey: false,
@ -224,7 +224,7 @@ module.exports = dc => {
}, },
isreproduce: { isreproduce: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "可复制的业务路径1.1:当【是否可复\n制的业务路径】=“是”,可复制的业务路径=1.1;当【是否可复制的业务路径】=“否”,可复制的业务路径=1;", comment: "可复制的业务路径1.1:当【是否可复\n制的业务路径】=“是”,可复制的业务路径=1.1;当【是否可复制的业务路径】=“否”,可复制的业务路径=1;",
primaryKey: false, primaryKey: false,

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

@ -13,7 +13,7 @@ module.exports = dc => {
comment: null, comment: null,
primaryKey: true, primaryKey: true,
field: "id", field: "id",
autoIncrement: false autoIncrement: true
}, },
year: { year: {
type: DataTypes.STRING, type: DataTypes.STRING,
@ -44,7 +44,7 @@ module.exports = dc => {
}, },
department: { department: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "部门:申请部门", comment: "部门:申请部门",
primaryKey: false, primaryKey: false,
@ -53,7 +53,7 @@ module.exports = dc => {
}, },
sale: { sale: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "销售人员:申请人", comment: "销售人员:申请人",
primaryKey: false, primaryKey: false,
@ -62,7 +62,7 @@ module.exports = dc => {
}, },
contractNo: { contractNo: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "合同编号", comment: "合同编号",
primaryKey: false, primaryKey: false,
@ -71,7 +71,7 @@ module.exports = dc => {
}, },
customer: { customer: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "客户名称:【甲方名称】", comment: "客户名称:【甲方名称】",
primaryKey: false, primaryKey: false,
@ -80,7 +80,7 @@ module.exports = dc => {
}, },
item: { item: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "项目名称", comment: "项目名称",
primaryKey: false, primaryKey: false,
@ -89,7 +89,7 @@ module.exports = dc => {
}, },
amount: { amount: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "合同金额:【合同金额\n(元)】", comment: "合同金额:【合同金额\n(元)】",
primaryKey: false, primaryKey: false,
@ -98,7 +98,7 @@ module.exports = dc => {
}, },
changeAmount: { changeAmount: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "变更后合同金额", comment: "变更后合同金额",
primaryKey: false, primaryKey: false,
@ -107,7 +107,7 @@ module.exports = dc => {
}, },
receivableYear: { receivableYear: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "回款年份", comment: "回款年份",
primaryKey: false, primaryKey: false,
@ -116,7 +116,7 @@ module.exports = dc => {
}, },
receivableDate: { receivableDate: {
type: DataTypes.DATEONLY, type: DataTypes.DATEONLY,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "回款日期", comment: "回款日期",
primaryKey: false, primaryKey: false,
@ -125,7 +125,7 @@ module.exports = dc => {
}, },
receivableAmount: { receivableAmount: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "回款金额", comment: "回款金额",
primaryKey: false, primaryKey: false,
@ -134,7 +134,7 @@ module.exports = dc => {
}, },
invoicedBack: { invoicedBack: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "开票-回款", comment: "开票-回款",
primaryKey: false, primaryKey: false,
@ -143,7 +143,7 @@ module.exports = dc => {
}, },
remainConAmount: { remainConAmount: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "剩余合同金额", comment: "剩余合同金额",
primaryKey: false, primaryKey: false,
@ -152,7 +152,7 @@ module.exports = dc => {
}, },
incomeConfirmdate: { incomeConfirmdate: {
type: DataTypes.DATEONLY, type: DataTypes.DATEONLY,
allowNull: false, allowNull: true,
defaultValue: null, defaultValue: null,
comment: "收入确认时间", comment: "收入确认时间",
primaryKey: false, primaryKey: false,

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

@ -16,6 +16,26 @@ module.exports = function (app, router, opts) {
app.fs.api.logAttr['GET/detail/achievement'] = { content: '查询业绩明细表', visible: false }; app.fs.api.logAttr['GET/detail/achievement'] = { content: '查询业绩明细表', visible: false };
router.get('/detail/achievement', achieve.getAchievementDetail); router.get('/detail/achievement', achieve.getAchievementDetail);
app.fs.api.logAttr['GET/detail/received/numbers'] = { content: '查询业绩明细已有的编号集合', visible: false };
router.get('/detail/received/numbers', achieve.getReceivedNumbers);
app.fs.api.logAttr['POST/add/received/back/bulk'] = { content: '导入回款明细', visible: true };
router.post('/add/received/back/bulk', achieve.importBackDetails);
app.fs.api.logAttr['POST/add/achievement/bulk'] = { content: '导入业绩明细', visible: true };
router.post('/add/achievement/bulk', achieve.importAchieveDetails);
app.fs.api.logAttr['GET/contract/detail'] = { content: '查询合同明细表', visible: false }; app.fs.api.logAttr['GET/contract/detail'] = { content: '查询合同明细表', visible: false };
router.get('/contract/detail', achieve.getContractDetail); router.get('/contract/detail', achieve.getContractDetail);
app.fs.api.logAttr['GET/invoicing/detail'] = { content: '查询开票明细表', visible: false };
router.get('/invoicing/detail', achieve.getInvoicingDetail);
app.fs.api.logAttr['GET/export/contract/detail'] = { content: '导出合同明细表', visible: false };
router.get('/export/contract/detail', achieve.exportContractDetail);
app.fs.api.logAttr['GET/export/invoicing/detail'] = { content: '导出开票明细表', visible: false };
router.get('/export/invoicing/detail', achieve.exportInvoicingDetail);
}; };

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

@ -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'), needIndexCell = true } = {}) {
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();
@ -35,9 +35,11 @@ module.exports = function (app, opts) {
headerStyle.border.bottomColor = '#000000'; headerStyle.border.bottomColor = '#000000';
const headerRow = sheet_1.addRow(); const headerRow = sheet_1.addRow();
const indexCell = headerRow.addCell(); if (needIndexCell) {
indexCell.value = '序号' const indexCell = headerRow.addCell();
indexCell.style = headerStyle indexCell.value = '序号'
indexCell.style = headerStyle
}
for (let h of header) { for (let h of header) {
const cell = headerRow.addCell(); const cell = headerRow.addCell();
cell.value = h.title; cell.value = h.title;
@ -54,9 +56,11 @@ module.exports = function (app, opts) {
style.border.bottomColor = '#000000'; style.border.bottomColor = '#000000';
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const row = sheet_1.addRow(); const row = sheet_1.addRow();
const indexCell = row.addCell(); if (needIndexCell) {
indexCell.value = i + 1 const indexCell = row.addCell();
indexCell.style = headerStyle indexCell.value = i + 1
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 || '-';

1
web/client/src/layout/actions/global.js

@ -38,6 +38,7 @@ export function initApiRoot () {
type: INIT_API_ROOT, type: INIT_API_ROOT,
payload: { payload: {
apiRoot: res.root, apiRoot: res.root,
webPepUrl:res.webPepUrl
} }
}) })
}); });

592
web/client/src/layout/containers/layout/index.jsx

@ -10,9 +10,13 @@ import { resize } from '../../actions/global';
import * as NProgress from 'nprogress'; import * as NProgress from 'nprogress';
import PerfectScrollbar from 'perfect-scrollbar'; import PerfectScrollbar from 'perfect-scrollbar';
import { useLocation } from "react-router"; import { useLocation } from "react-router";
import { RouteTable } from '$utils';
import { RouteRequest } from '@peace/utils';
import Cookie from 'js-cookie';
import { login, LOGIN_SUCCESS } from '../../../sections/auth/actions/auth';
NProgress.configure({ NProgress.configure({
template: ` template: `
<div class="bar" style="height:2px" role="bar"> <div class="bar" style="height:2px" role="bar">
<div class="peg"></div> <div class="peg"></div>
</div> </div>
@ -23,303 +27,337 @@ NProgress.configure({
}); });
let scrollbar let scrollbar
let requestUser = true
let requestlogout = false;
// const location111 = useLocation(); // const location111 = useLocation();
const LayoutContainer = props => { const LayoutContainer = props => {
const { const {
dispatch, msg, user, copyright, children, sections, clientWidth, clientHeight, dispatch, actions, msg, user, copyright, children, sections, clientWidth, clientHeight,
location, match, routes, history, socket, location, match, routes, history, socket, apiRoot, webPepUrl
} = props } = props
const [collapsed, setCollapsed] = useState(false) const [collapsed, setCollapsed] = useState(false)
NProgress.start(); NProgress.start();
const resize_ = () => { const resize_ = () => {
dispatch(resize( dispatch(resize(
document.getElementById('DrApp').clientHeight, document.getElementById('DrApp').clientHeight,
document.getElementById('DrApp').clientWidth - (collapsed ? 120 : 240) document.getElementById('DrApp').clientWidth - (collapsed ? 120 : 240)
)); ));
} }
function deepCopy(data) {// function deepCopy (data) {//
//string,number,bool,null,undefined,symbol //string,number,bool,null,undefined,symbol
//object,array,date //object,array,date
if (data && typeof data === "object") { if (data && typeof data === "object") {
// //
if (typeof data === "function") { if (typeof data === "function") {
let tempFunc = data.bind(null); let tempFunc = data.bind(null);
tempFunc.prototype = deepCopy(data.prototype); tempFunc.prototype = deepCopy(data.prototype);
return tempFunc; return tempFunc;
} }
switch (Object.prototype.toString.call(data)) { switch (Object.prototype.toString.call(data)) {
case "[object String]": case "[object String]":
return data.toString(); return data.toString();
case "[object Number]": case "[object Number]":
return Number(data.toString()); return Number(data.toString());
case "[object Boolean]": case "[object Boolean]":
return new Boolean(data.toString()); return new Boolean(data.toString());
case "[object Date]": case "[object Date]":
return new Date(data.getTime()); return new Date(data.getTime());
case "[object Array]": case "[object Array]":
var arr = []; var arr = [];
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
arr[i] = deepCopy(data[i]); arr[i] = deepCopy(data[i]);
} }
return arr; return arr;
//js //js
case "[object Object]": case "[object Object]":
var obj = {}; var obj = {};
for (let key in data) { for (let key in data) {
//hasOwnProperty obj.hasOwnProperty(prop) //hasOwnProperty obj.hasOwnProperty(prop)
obj[key] = deepCopy(data[key]); obj[key] = deepCopy(data[key]);
} }
return obj; return obj;
} }
} else { } else {
//string,number,bool,null,undefined,symbol //string,number,bool,null,undefined,symbol
return data; return data;
} }
} }
const [allItems, setAllItems] = useState([]) const [allItems, setAllItems] = useState([])
// const [headerItems, setHeaderItems] = useState([]) // const [headerItems, setHeaderItems] = useState([])
const [leftItems, setLeftItems] = useState([]) const [leftItems, setLeftItems] = useState([])
const [leftChange, setLeftChange] = useState(true) const [leftChange, setLeftChange] = useState(true)
const [leftShow, setLeftShow] = useState(false) const [leftShow, setLeftShow] = useState(false)
useEffect(() => { useEffect(() => {
let topItems = []// let topItems = []//
const lastSelectedKeys = localStorage.getItem('poms_selected_sider') const lastSelectedKeys = localStorage.getItem('poms_selected_sider')
let nextItems = []// let nextItems = []//
for (let c of sections) { for (let c of sections) {
if (typeof c.getNavItem == 'function') { if (typeof c.getNavItem == 'function') {
let item = c.getNavItem(user, dispatch); let item = c.getNavItem(user, dispatch);
if (item) { if (item) {
nextItems.push.apply(nextItems, item) nextItems.push.apply(nextItems, item)
if (item.length > 0) { if (item.length > 0) {
for (let j = 0; j < item.length; j++) { for (let j = 0; j < item.length; j++) {
let itm = deepCopy(item[j]); let itm = deepCopy(item[j]);
if (itm.hasOwnProperty('items')) { if (itm.hasOwnProperty('items')) {
for (let i = 0; i < itm.items.length; i++) { for (let i = 0; i < itm.items.length; i++) {
itm.items[i].fatherKey = itm.itemKey itm.items[i].fatherKey = itm.itemKey
delete itm.items[i].items delete itm.items[i].items
}
// topItems.push(itm)
// }
// else {
// topItems.push.apply(topItems, item)
}
} }
} // topItems.push(itm)
} // }
// else {
// topItems.push.apply(topItems, item)
}
}
}
} }
} }
setAllItems(nextItems) }
// setHeaderItems(topItems) setAllItems(nextItems)
if (lastSelectedKeys) {// // setHeaderItems(topItems)
for (let i = 0; i < nextItems.length; i++) { if (lastSelectedKeys) {//
if (JSON.parse(lastSelectedKeys)[0] == nextItems[i].itemKey) { for (let i = 0; i < nextItems.length; i++) {
if (JSON.parse(lastSelectedKeys)[0] == nextItems[i].itemKey) {
// let openArr = [] // let openArr = []
// for (let j = 0; j < nextItems[i].items.length; j++) { // for (let j = 0; j < nextItems[i].items.length; j++) {
// openArr.push(nextItems[i].items[j].itemKey) // openArr.push(nextItems[i].items[j].itemKey)
// } // }
// localStorage.setItem('poms_open_sider', JSON.stringify(openArr)) // localStorage.setItem('poms_open_sider', JSON.stringify(openArr))
setLeftItems(nextItems[i].items) setLeftItems(nextItems[i].items)
}
} }
setLeftShow(true) }
} setLeftShow(true)
else { }
setLeftShow(false) else {
} setLeftShow(false)
}
window.addEventListener('resize', resize_); window.addEventListener('resize', resize_);
return () => { return () => {
window.removeEventListener('resize', resize_); window.removeEventListener('resize', resize_);
} }
}, []) }, [])
useEffect(() => { useEffect(() => {
let pathnameArr = location.pathname.split('/') let pathnameArr = location.pathname.split('/')
let openArr = [] let openArr = []
for (let i = 0; i < allItems.length; i++) { for (let i = 0; i < allItems.length; i++) {
if (allItems[i].items) { if (allItems[i].items) {
for (let j = 0; j < allItems[i].items.length; j++) { for (let j = 0; j < allItems[i].items.length; j++) {
if (allItems[i].items[j].items) { if (allItems[i].items[j].items) {
for (let k = 0; k < allItems[i].items[j].items.length; k++) { for (let k = 0; k < allItems[i].items[j].items.length; k++) {
if (allItems[i].items[j].items[k].to == location.pathname) { if (allItems[i].items[j].items[k].to == location.pathname) {
for (let o = 0; o < allItems[i].items.length; o++) { for (let o = 0; o < allItems[i].items.length; o++) {
openArr.push(allItems[i].items[o].itemKey) openArr.push(allItems[i].items[o].itemKey)
}
localStorage.setItem('poms_selected_sider', JSON.stringify([pathnameArr[1]]))
// localStorage.setItem('poms_open_sider', JSON.stringify(openArr))
setLeftItems(allItems[i].items)
setLeftShow(true)
}
} }
} localStorage.setItem('poms_selected_sider', JSON.stringify([pathnameArr[1]]))
} // localStorage.setItem('poms_open_sider', JSON.stringify(openArr))
setLeftItems(allItems[i].items)
setLeftShow(true)
}
}
}
} }
} }
}, [location]) }
}, [location])
useEffect(() => { useEffect(() => {
NProgress.done(); NProgress.done();
if ((!user || !user.authorized)) { if ((!user || !user.authorized)) {
history.push('/signin'); // history.push('/signin');
} getUserInfoByToken()
if (msg) { }
if (msg.done) { if (msg) {
Notification.success({ if (msg.done) {
// title: msg.done, Notification.success({
content: msg.done, // title: msg.done,
duration: 2, content: msg.done,
}) duration: 2,
} })
if (msg.error) { }
Notification.error({ if (msg.error) {
// title: msg.error, Notification.error({
content: msg.error, // title: msg.error,
duration: 2, content: msg.error,
}) duration: 2,
} })
} }
const dom = document.getElementById('page-content'); }
if (dom) { const dom = document.getElementById('page-content');
if (!scrollbar) { if (dom) {
scrollbar = new PerfectScrollbar('#page-content', { suppressScrollX: true }); if (!scrollbar) {
scrollbar.update(); scrollbar = new PerfectScrollbar('#page-content', { suppressScrollX: true });
} else { scrollbar.update();
scrollbar.update(); } else {
dom.scrollTop = 0; scrollbar.update();
} dom.scrollTop = 0;
} }
}) }
})
const getUserInfoByToken = () => {
if (requestUser) {
requestUser = false;
RouteRequest.get(RouteTable.apiRoot).then(res => {
let token = Cookie.get('pepToken', { domain: res.domain });
dispatch(login({ token })).then(res => {
console.log(res);
if (res.type == 'LOGIN_SUCCESS') {
const data = res.payload?.user || {}
history.push('/businessManagement/pmReport/reserveItemsReporting')
localStorage.setItem('poms_open_sider', JSON.stringify(["pmReport"]))
localStorage.setItem('poms_selected_sider', JSON.stringify(["businessManagement"]))
localStorage.setItem('word', JSON.stringify('')) //
dispatch(actions.layout.initWebSocket({ ioUrl: apiRoot, token: data.token, hrUserId: data.hrUserInfo && hrUserInfo.id }))
} else {
window.location.href = `${webPepUrl}/signin`
}
}, error => {
window.location.href = `${webPepUrl}/signin`
})
}, error => {
message.error('鉴权失败', 5);
window.location.href = `${webPepUrl}/signin`
})
}
}
// websocket 使
useEffect(() => {
// console.log(socket)
if (socket) {
socket.on('CAMERA_ONLINE', function (msg) {
console.info(msg);
if (msg.online == 'ON') {
Notification.success({
title: 'Hi',
content: (<div><div>{msg.name}</div><div style={{ marginTop: 5 }}>已上线</div></div>),
duration: 2,
})
}
if (msg.online == 'OFF') {
Notification.error({
title: 'Hi',
content: (<div><div>{msg.name}</div><div style={{ marginTop: 5 }}>发生离线</div></div>),
duration: 2,
})
}
});
return () => {
socket.off("CAMERA_ONLINE");
}
}
}, [socket])
return ( // websocket 使
<Layout id="layout" style={{ height: '100%' }}> useEffect(() => {
{ // console.log(socket)
<> if (socket) {
<Layout.Header> socket.on('CAMERA_ONLINE', function (msg) {
<Header console.info(msg);
// headerItems={headerItems} // if (msg.online == 'ON') {
user={user} Notification.success({
pathname={location.pathname} title: 'Hi',
toggleCollapsed={() => { content: (<div><div>{msg.name}</div><div style={{ marginTop: 5 }}>已上线</div></div>),
setCollapsed(!collapsed); duration: 2,
}} })
collapsed={collapsed} }
history={history} if (msg.online == 'OFF') {
tochange={(val) => { Notification.error({
// setLeftChange(!leftChange) // title: 'Hi',
if (val.fatherKey) { content: (<div><div>{msg.name}</div><div style={{ marginTop: 5 }}>发生离线</div></div>),
localStorage.setItem('poms_selected_sider', JSON.stringify([val.itemKey])); duration: 2,
const historyOpenKeys = localStorage.getItem('poms_open_sider'); })
const openKeys = historyOpenKeys && JSON.parse(historyOpenKeys).concat(val.openKey || val.itemKey) || [val.openKey || val.itemKey]
localStorage.setItem('poms_open_sider', JSON.stringify(openKeys));
// for (let i = 0; i < allItems.length; i++) {
// if (val.fatherKey == allItems[i].itemKey) {
// let openArr = []
// for (let j = 0; j < allItems[i].items.length; j++) {
// openArr.push(allItems[i].items[j].itemKey)
// }
// localStorage.setItem('poms_selected_sider', JSON.stringify([val.fatherKey]))
// localStorage.setItem('poms_open_sider', JSON.stringify(openArr))
// setLeftItems(allItems[i].items)
// }
// }
// setLeftShow(true)
}
else {
localStorage.setItem('poms_open_sider', JSON.stringify([]))
localStorage.removeItem('poms_selected_sider')
// setLeftShow(false)
}
history.push(val.to);
}}
/>
</Layout.Header>
<Layout style={{ height: 'calc(100% - 50px)' }}>
{leftShow ? (<Layout.Sider>
<Sider
sections={sections}
leftItems={leftItems}
dispatch={dispatch}
user={user}
leftChange={leftChange}
pathname={location.pathname}
collapsed={collapsed}
/>
</Layout.Sider>) : ('')}
<Layout.Content>
<div style={{
background: "#F2F3F5",
}}>
<div id="page-content" style={{
height: clientHeight - 12,
minWidth: 520,
position: 'relative',
}}>
<div style={{
minHeight: clientHeight - 32 - 12,
position: 'relative',
padding: '12px 8px',
}}>
{children}
</div>
<Layout.Footer>
<Footer />
</Layout.Footer>
</div>
</div>
</Layout.Content>
</Layout>
</>
} }
</Layout > });
) return () => {
socket.off("CAMERA_ONLINE");
}
}
}, [socket])
return (
<Layout id="layout" style={{ height: '100%' }}>
{
<>
<Layout.Header>
<Header
// headerItems={headerItems} //
user={user}
pathname={location.pathname}
toggleCollapsed={() => {
setCollapsed(!collapsed);
}}
collapsed={collapsed}
history={history}
tochange={(val) => {
// setLeftChange(!leftChange) //
if (val.fatherKey) {
localStorage.setItem('poms_selected_sider', JSON.stringify([val.itemKey]));
const historyOpenKeys = localStorage.getItem('poms_open_sider');
const openKeys = historyOpenKeys && JSON.parse(historyOpenKeys).concat(val.openKey || val.itemKey) || [val.openKey || val.itemKey]
localStorage.setItem('poms_open_sider', JSON.stringify(openKeys));
// for (let i = 0; i < allItems.length; i++) {
// if (val.fatherKey == allItems[i].itemKey) {
// let openArr = []
// for (let j = 0; j < allItems[i].items.length; j++) {
// openArr.push(allItems[i].items[j].itemKey)
// }
// localStorage.setItem('poms_selected_sider', JSON.stringify([val.fatherKey]))
// localStorage.setItem('poms_open_sider', JSON.stringify(openArr))
// setLeftItems(allItems[i].items)
// }
// }
// setLeftShow(true)
}
else {
localStorage.setItem('poms_open_sider', JSON.stringify([]))
localStorage.removeItem('poms_selected_sider')
// setLeftShow(false)
}
history.push(val.to);
}}
/>
</Layout.Header>
<Layout style={{ height: 'calc(100% - 50px)' }}>
{leftShow ? (<Layout.Sider>
<Sider
sections={sections}
leftItems={leftItems}
dispatch={dispatch}
user={user}
leftChange={leftChange}
pathname={location.pathname}
collapsed={collapsed}
/>
</Layout.Sider>) : ('')}
<Layout.Content>
<div style={{
background: "#F2F3F5",
}}>
<div id="page-content" style={{
height: clientHeight - 12,
minWidth: 520,
position: 'relative',
}}>
<div style={{
minHeight: clientHeight - 32 - 12,
position: 'relative',
padding: '12px 8px',
}}>
{children}
</div>
<Layout.Footer>
<Footer />
</Layout.Footer>
</div>
</div>
</Layout.Content>
</Layout>
</>
}
</Layout >
)
} }
function mapStateToProps(state) { function mapStateToProps (state) {
const { global, auth, ajaxResponse, webSocket } = state; const { global, auth, ajaxResponse, webSocket } = state;
return { return {
title: global.title, title: global.title,
copyright: global.copyright, copyright: global.copyright,
sections: global.sections, sections: global.sections,
actions: global.actions, actions: global.actions,
clientWidth: global.clientWidth, clientWidth: global.clientWidth,
clientHeight: global.clientHeight, clientHeight: global.clientHeight,
msg: ajaxResponse.msg, msg: ajaxResponse.msg,
user: auth.user, user: auth.user,
socket: webSocket.socket socket: webSocket.socket,
}; apiRoot: global.apiRoot,
webPepUrl: global.webPepUrl,
};
} }
export default connect(mapStateToProps)(LayoutContainer); export default connect(mapStateToProps)(LayoutContainer);

2
web/client/src/layout/reducers/global.js

@ -11,6 +11,7 @@ function global (state = {
clientHeight: 768, clientHeight: 768,
clientWidth: 1024, clientWidth: 1024,
apiRoot: '', apiRoot: '',
webPepUrl: ''
}, action) { }, action) {
const payload = action.payload; const payload = action.payload;
switch (action.type) { switch (action.type) {
@ -30,6 +31,7 @@ function global (state = {
case INIT_API_ROOT: case INIT_API_ROOT:
return Immutable.fromJS(state).merge({ return Immutable.fromJS(state).merge({
apiRoot: payload.apiRoot, apiRoot: payload.apiRoot,
webPepUrl: payload.webPepUrl
}).toJS(); }).toJS();
default: default:
return state; return state;

10
web/client/src/sections/auth/actions/auth.js

@ -4,7 +4,7 @@ import { ApiTable, AxyRequest, EmisRequest } from '$utils'
import { Request } from '@peace/utils'; import { Request } from '@peace/utils';
export const INIT_AUTH = 'INIT_AUTH'; export const INIT_AUTH = 'INIT_AUTH';
export function initAuth(userData) { export function initAuth (userData) {
const sessionUser = JSON.parse(sessionStorage.getItem('dcUser')) const sessionUser = JSON.parse(sessionStorage.getItem('dcUser'))
const user = userData || sessionUser || {}; const user = userData || sessionUser || {};
if (user.authorized && !sessionUser) { if (user.authorized && !sessionUser) {
@ -21,11 +21,11 @@ export function initAuth(userData) {
export const REQUEST_LOGIN = 'REQUEST_LOGIN'; export const REQUEST_LOGIN = 'REQUEST_LOGIN';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_ERROR = 'LOGIN_ERROR'; export const LOGIN_ERROR = 'LOGIN_ERROR';
export function login(username, password) { export function login ({ username, password, token }) {
return dispatch => { return dispatch => {
dispatch({ type: REQUEST_LOGIN }); dispatch({ type: REQUEST_LOGIN });
if (!username || !password) { if ((!username || !password) && !token) {
dispatch({ dispatch({
type: LOGIN_ERROR, type: LOGIN_ERROR,
payload: { error: '请输入账号名和密码' } payload: { error: '请输入账号名和密码' }
@ -43,7 +43,7 @@ export function login(username, password) {
// }, // },
// }); // });
return Request.post(ApiTable.login, { username, password, code: 'HR' }) return Request.post(ApiTable.login, { username, password, token, code: 'HR' })
.then(user => { .then(user => {
sessionStorage.setItem('dcUser', JSON.stringify(user)); sessionStorage.setItem('dcUser', JSON.stringify(user));
return dispatch({ return dispatch({
@ -63,7 +63,7 @@ export function login(username, password) {
} }
export const LOGOUT = 'LOGOUT'; export const LOGOUT = 'LOGOUT';
export function logout() { export function logout () {
const user = JSON.parse(sessionStorage.getItem('dcUser')) const user = JSON.parse(sessionStorage.getItem('dcUser'))
user && user.token ? user && user.token ?
Request.put(ApiTable.logout, { Request.put(ApiTable.logout, {

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

@ -28,6 +28,40 @@ export function getAchievementDetail(query) {
}); });
} }
//回款明细 编号集合
export function getReceivedNumbers() {
return (dispatch) => basicAction({
type: "get",
dispatch: dispatch,
actionType: "GET_RECEIVED_DETAIL_NUMBERS",
url: `${ApiTable.getReceivedNumbers}`,
msg: { option: "查询回款明细编号集合" },
reducer: { name: "ReceivedDetailNumbers", params: { noClear: true } },
});
}
//导入回款明细
export function importBackDetails(values) {
return dispatch => basicAction({
type: 'post',
dispatch: dispatch,
actionType: 'RECEIVED_DETAIL_BULK_ADD',
url: ApiTable.importBackDetails,
data: values,
msg: { option: '导入回款明细' },
});
}
//导入业绩明细
export function importAchieveDetails(values) {
return dispatch => basicAction({
type: 'post',
dispatch: dispatch,
actionType: 'ACHIEVEMENT_DETAIL_BULK_ADD',
url: ApiTable.importAchieveDetails,
data: values,
msg: { option: '导入业绩明细' },
});
}
//查询合同明细表 //查询合同明细表
export function getContractDetail(query) { export function getContractDetail(query) {
return (dispatch) => basicAction({ return (dispatch) => basicAction({
@ -39,4 +73,17 @@ export function getContractDetail(query) {
msg: { option: "查询合同明细表" }, msg: { option: "查询合同明细表" },
reducer: { name: "ContractDetail", params: { noClear: true } }, reducer: { name: "ContractDetail", params: { noClear: true } },
}); });
}
//查询开票明细表
export function getInvoicingDetail(query) {
return (dispatch) => basicAction({
type: "get",
dispatch: dispatch,
actionType: "GET_INVOICING_DETAIL",
query: query,
url: `${ApiTable.getInvoicingDetail}`,
msg: { option: "查询开票明细表" },
reducer: { name: "InvoicingDetail", params: { noClear: true } },
});
} }

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

@ -71,4 +71,52 @@ export const invoicingDetailsColumnKeys = {
taxRate13: '税率13%', taxRate13: '税率13%',
taxRate9: '税率9%', taxRate9: '税率9%',
taxRate6: '税率6%' taxRate6: '税率6%'
}
export const backMoneyColumnKeys = {
year: '年度',
serialNo: '序号',
number: '编号',
department: '部门',
sale: '销售人员',
contractNo: '合同编号',
customer: '客户名称',
item: '项目名称',
amount: '合同金额',
changeAmount: '变更后合同金额',
receivableYear: '回款年份',
receivableDate: '回款日期',
receivableAmount: '回款金额',
invoicedBack: '开票-回款',
remainConAmount: '剩余合同金额',
incomeConfirmdate: '收入确认时间',
thirdPayment: '第三方付款单位',
remark: '备注',
}
export const achievementColumnKeys = {
recConDate: '收到合同日期',
month: '月份',
department: '部门',
sale: '销售人员',
customer: '客户名称',
item: '项目名称',
amount: '合同金额',
realPerformance: '实际业绩',
assessmentPerformance: '考核业绩',
isApproval: '价格是否特批',
approvalProp: '特批折算比例',
cost: '预支提成及委外费用',
serviceLine: '业务线',
cusType: '客户类型',
industry: '行业',
source: '信息来源',
itemType: '项目类型',
cusProvince: '客户省份',
cusAttribute: '客户属性',
repurchaseCount: '复购次数',
reproducible: '是否可复制的业务路径',
outProvince: '省外业务1.1',
repurchase: '复购业务1.05',
isreproduce: '可复制的业务路径1.1',
} }

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

@ -4,6 +4,8 @@ import moment from 'moment'
import { Select, Input, Button, Toast, Radio, Tooltip, Table, Pagination, Skeleton } from '@douyinfe/semi-ui'; import { Select, Input, Button, Toast, Radio, Tooltip, Table, Pagination, Skeleton } from '@douyinfe/semi-ui';
import { IconSearch } from '@douyinfe/semi-icons'; import { IconSearch } from '@douyinfe/semi-icons';
import { SkeletonScreen } from "$components"; import { SkeletonScreen } from "$components";
import { achievementColumnKeys } from '../../constants/index';
import ImportAchieveModal from './importAchieveModal'
import '../../style.less'; import '../../style.less';
const AchievementDetails = (props) => { const AchievementDetails = (props) => {
@ -14,7 +16,8 @@ const AchievementDetails = (props) => {
const [limits, setLimits] = useState()// const [limits, setLimits] = useState()//
const [query, setQuery] = useState({ limit: 10, page: 0 }); // const [query, setQuery] = useState({ limit: 10, page: 0 }); //
const [tableData, setTableData] = useState([]); const [tableData, setTableData] = useState([]);
const [exportUrl, setExportUrl] = useState('') const [importModalV, setImportModalV] = useState(false);
const [exportUrl, setExportUrl] = useState('');
const page = useRef(query.page); const page = useRef(query.page);
function seachValueChange(value) { function seachValueChange(value) {
setKeyword(value) setKeyword(value)
@ -37,151 +40,22 @@ const AchievementDetails = (props) => {
}) })
} }
const columns = [{ const renderColumns = (columnKeys) => {
title: '收到合同日期', let columns = [];
dataIndex: 'recConDate', Object.keys(columnKeys).map(key => {
key: 'recConDate', switch (key) {
width: 120, default:
render: (text, record) => <span>{text || '-'}</span> columns.push({
}, { title: columnKeys[key], dataIndex: key, key: key, width: 130,
title: '月份', render: (text, record) => ['isApproval', 'reproducible'].indexOf(key) != -1 ? JSON.stringify(text) === 'null' ? '-' : text ? '是' : '否'
dataIndex: 'month', : text === 0 ? text : text ? text : '-'
key: 'month', });
width: 120, break;
render: (text, record) => <span>{text || '-'}</span> }
}, { })
title: '部门', return columns;
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) {// function handleRow(record, index) {//
if (index % 2 === 0) { if (index % 2 === 0) {
return { return {
@ -273,7 +147,7 @@ const AchievementDetails = (props) => {
placeholder={SkeletonScreen()} placeholder={SkeletonScreen()}
> >
<Table <Table
columns={columns} columns={renderColumns(achievementColumnKeys)}
dataSource={tableData} dataSource={tableData}
bordered={false} bordered={false}
empty="暂无数据" empty="暂无数据"
@ -307,6 +181,13 @@ const AchievementDetails = (props) => {
</div> </div>
</div> </div>
</div> </div>
{
importModalV ? <ImportAchieveModal
onCancel={() => {
setImportModalV(false);
getAchievementDetails();
}} /> : ''
}
{ {
exportUrl ? <iframe src={`/_api/${exportUrl}`} style={{ display: 'none' }} /> : '' exportUrl ? <iframe src={`/_api/${exportUrl}`} style={{ display: 'none' }} /> : ''
} }

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

@ -4,8 +4,9 @@ import moment from 'moment'
import { Select, Input, Button, Toast, Radio, Tooltip, Table, Pagination, Skeleton } from '@douyinfe/semi-ui'; import { Select, Input, Button, Toast, Radio, Tooltip, Table, Pagination, Skeleton } from '@douyinfe/semi-ui';
import { IconSearch } from '@douyinfe/semi-icons'; import { IconSearch } from '@douyinfe/semi-icons';
import { SkeletonScreen } from "$components"; import { SkeletonScreen } from "$components";
import ImportBackModal from './importBackModal';
import '../../style.less'; import '../../style.less';
import { backMoneyColumnKeys } from '../../constants/index';
const BackMoneyDetails = (props) => { const BackMoneyDetails = (props) => {
const { dispatch, actions, user } = props const { dispatch, actions, user } = props
const { businessManagement } = actions const { businessManagement } = actions
@ -14,7 +15,8 @@ const BackMoneyDetails = (props) => {
const [limits, setLimits] = useState()// const [limits, setLimits] = useState()//
const [query, setQuery] = useState({ limit: 10, page: 0 }); // const [query, setQuery] = useState({ limit: 10, page: 0 }); //
const [tableData, setTableData] = useState([]); const [tableData, setTableData] = useState([]);
const [exportUrl, setExportUrl] = useState('') const [importModalV, setImportModalV] = useState(false);
const [exportUrl, setExportUrl] = useState('');
const page = useRef(query.page); const page = useRef(query.page);
function seachValueChange(value) { function seachValueChange(value) {
setKeyword(value) setKeyword(value)
@ -37,115 +39,21 @@ const BackMoneyDetails = (props) => {
}) })
} }
const columns = [{ const renderColumns = (columnKeys) => {
title: '年度', let columns = [];
dataIndex: 'year', Object.keys(columnKeys).map(key => {
key: 'year', switch (key) {
width: 120, default:
render: (text, record) => <span>{text || '-'}</span> columns.push({
}, { title: columnKeys[key], dataIndex: key, key: key, width: 130,
title: '序号', render: (text, record) => text === 0 ? text : text ? text : '-'
dataIndex: 'serialNo', });
key: 'serialNo', break;
width: 120, }
render: (text, record) => <span>{text || '-'}</span> })
}, { return columns;
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) {// function handleRow(record, index) {//
if (index % 2 === 0) { if (index % 2 === 0) {
return { return {
@ -237,7 +145,7 @@ const BackMoneyDetails = (props) => {
placeholder={SkeletonScreen()} placeholder={SkeletonScreen()}
> >
<Table <Table
columns={columns} columns={renderColumns(backMoneyColumnKeys)}
dataSource={tableData} dataSource={tableData}
bordered={false} bordered={false}
empty="暂无数据" empty="暂无数据"
@ -271,6 +179,13 @@ const BackMoneyDetails = (props) => {
</div> </div>
</div> </div>
</div> </div>
{
importModalV ? <ImportBackModal
onCancel={() => {
setImportModalV(false);
getBackMoneyDetails();
}} /> : ''
}
{ {
exportUrl ? <iframe src={`/_api/${exportUrl}`} style={{ display: 'none' }} /> : '' exportUrl ? <iframe src={`/_api/${exportUrl}`} style={{ display: 'none' }} /> : ''
} }

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

@ -9,7 +9,7 @@ import '../../style.less';
import moment from 'moment' import moment from 'moment'
const ContractDetails = (props) => { const ContractDetails = (props) => {
const { dispatch, actions } = props const { dispatch, actions, user } = props
const { businessManagement } = actions; const { businessManagement } = actions;
const [keywordTarget, setKeywordTarget] = useState('contractNo'); const [keywordTarget, setKeywordTarget] = useState('contractNo');
const [keyword, setKeyword] = useState('');// const [keyword, setKeyword] = useState('');//
@ -23,6 +23,7 @@ const ContractDetails = (props) => {
const [exportUrl, setExportUrl] = useState(''); const [exportUrl, setExportUrl] = useState('');
const page = useRef(query.page); const page = useRef(query.page);
const scroll = useMemo(() => ({}), []);
const CONTRACTDETAILS = "contractDetails"; const CONTRACTDETAILS = "contractDetails";
const renderColumns = (columnKeys) => { const renderColumns = (columnKeys) => {
@ -33,7 +34,8 @@ const ContractDetails = (props) => {
default: default:
columns.push({ columns.push({
title: columnKeys[key], dataIndex: key, key: key, title: columnKeys[key], dataIndex: key, key: key,
render: (text, record) => text === 0 ? text : text ? text : '—', width: 32 + columnKeys[key].length * 16 render: (text, record) => text === 0 ? text : text ? text : '—',
width: 32 + columnKeys[key].length * 16
}); });
break; break;
} }
@ -108,7 +110,6 @@ const ContractDetails = (props) => {
} }
}) })
} }
const scroll = useMemo(() => ({}), []);
return ( return (
<> <>
<div style={{ padding: '0px 12px' }}> <div style={{ padding: '0px 12px' }}>
@ -160,9 +161,10 @@ const ContractDetails = (props) => {
onClick={() => { setImportModalV(true); }}> onClick={() => { setImportModalV(true); }}>
导入 导入
</div> </div>
<div style={{ padding: '6px 20px', background: '#00BA85', color: '#FFFFFF', fontSize: 14, marginLeft: 18 }} onClick={() => { <div style={{ padding: '6px 20px', background: '#00BA85', color: '#FFFFFF', fontSize: 14, marginLeft: 18, cursor: "pointer" }}
exportAllData() onClick={() => {
}}> exportAllData()
}}>
导出全部 导出全部
</div> </div>
</div> </div>

220
web/client/src/sections/business/containers/performanceReport/importAchieveModal.jsx

@ -0,0 +1,220 @@
'use strict';
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import moment from 'moment';
import { Modal, Form, Button, Notification } from '@douyinfe/semi-ui';
import { IconUpload } from '@douyinfe/semi-icons';
import XLSX from 'xlsx';
import { achievementColumnKeys } from '../../constants/index';
//
const ImportAchieveModal = props => {
const { dispatch, actions, onCancel } = props;
const { businessManagement } = actions
const [msg, setMsg] = useState('');
const [loading, setLoading] = useState('');
const [postData, setPostData] = useState([]);
//
useEffect(() => {
}, []);
const confirm = () => {
if (postData.length) {
setLoading(true)
dispatch(businessManagement.importAchieveDetails(postData)).then(res => {
if (res.success) {
onCancel()
}
setLoading(false)
})
} else {
Notification.warning({ content: '没有数据可以提交,请上传数据文件', duration: 2 })
}
}
const dldCsvMb = () => {
//
let head = [];
Object.keys(achievementColumnKeys).map(key => {
head.push(achievementColumnKeys[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;
}
const judgeNullTime = (v) => {
//xlsxexcel
//2020/8/1xlsx Fri Jul 31 2020 23:59:17 GMT+0800 43
let a = new Date(v);
a.setTime(a.getTime() + 43 * 1000);
return v ? a : null;
}
const judgeTimeValid = (time) => {
let valid = true;
if (!time) {
return valid;//
}
const ymd = /^((19|20)[0-9]{2})[\/\-]((0[1-9])|(1[0-2]))[\/\-]((0[1-9])|((1|2)[0-9])|(3[0-1]))$/;//
if (time instanceof Date) {
let timeStr = moment(time).format('YYYY/MM/DD');
if (!ymd.test(timeStr)) {
valid = false;
}
} else {
valid = false;
}
return valid;
}
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(achievementColumnKeys).map(key => {
if (key === 'recConDate') {//
obj[key] = judgeNullTime(d[achievementColumnKeys[key]]);
} else {
obj[key] = d[achievementColumnKeys[key]] || null;
}
})
let tValid = judgeTimeValid(obj.recConDate);
if (!tValid) {
error(`${i + 2}行【收到合同日期】错误,请填写yyyy/mm/dd格式`)
return
}
if (obj.isApproval && ['是', '否'].indexOf(obj.isApproval) == -1) {
error(`${i + 2}行【价格是否特批】错误,请填写是或否`)
return
}
if (obj.reproducible && ['是', '否'].indexOf(obj.reproducible) == -1) {
error(`${i + 2}行【是否可复制的业务路径】错误,请填写是或否`)
return
}
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)(ImportAchieveModal);

256
web/client/src/sections/business/containers/performanceReport/importBackModal.jsx

@ -0,0 +1,256 @@
'use strict';
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import moment from 'moment';
import { Modal, Form, Button, Notification } from '@douyinfe/semi-ui';
import { IconUpload } from '@douyinfe/semi-icons';
import XLSX from 'xlsx';
import { backMoneyColumnKeys } from '../../constants/index';
//
const ImportBackModal = props => {
const { dispatch, actions, onCancel } = props;
const { businessManagement } = actions
const [msg, setMsg] = useState('');
const [loading, setLoading] = useState('');
const [postData, setPostData] = useState([]);
const [uploadAble, setUploadAble] = useState(false);
const [allNumbers, setAllNumbers] = useState([]);
//
useEffect(() => {
dispatch(businessManagement.getReceivedNumbers()).then(r => {
if (r.success) {
setUploadAble(true);
setAllNumbers(r.payload.data);
}
})
}, []);
const confirm = () => {
if (postData.length) {
setLoading(true)
dispatch(businessManagement.importBackDetails(postData)).then(res => {
if (res.success) {
onCancel()
}
setLoading(false)
})
} else {
Notification.warning({ content: '没有数据可以提交,请上传数据文件', duration: 2 })
}
}
const dldCsvMb = () => {
//
let head = [];
Object.keys(backMoneyColumnKeys).map(key => {
head.push(backMoneyColumnKeys[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;
}
const judgeNullTime = (v) => {
//xlsxexcel
//2020/8/1xlsx Fri Jul 31 2020 23:59:17 GMT+0800 43
let a = new Date(v);
a.setTime(a.getTime() + 43 * 1000);
return v ? a : null;
}
const judgeTimeValid = (time) => {
let valid = true;
if (!time) {
return valid;//
}
const ymd = /^((19|20)[0-9]{2})[\/\-]((0[1-9])|(1[0-2]))[\/\-]((0[1-9])|((1|2)[0-9])|(3[0-1]))$/;//
if (time instanceof Date) {
let timeStr = moment(time).format('YYYY/MM/DD');
if (!ymd.test(timeStr)) {
valid = false;
}
} else {
valid = false;
}
return valid;
}
return (
<Modal
title="导入" visible={true}
onOk={confirm} width={620}
confirmLoading={loading}
onCancel={() => {
setMsg('')
setLoading(false)
setPostData([])
onCancel()
}}
>
<div style={{ borderBottom: '1px solid #DCDEE0', margin: '0px -24px' }}></div>
<Form>
<Form.Upload
label={'回款明细表'} labelPosition='left'
action={'/'} accept={fileLimit}
maxSize={200} limit={1}
onRemove={(currentFile, fileList, fileItem) => {
setMsg('');
setPostData([]);
}}
customRequest={(data) => {
const { file, onSuccess, onError } = data
getFileBlob(file.url).then(res => {
const error = (msg) => {
setMsg(msg)
onError({ message: msg })
}
if (res.length > 1000) {
error('一次性上传数据行数应小于1000行,请分批上传')
return
}
if (!res.length) {
error('请填写至少一行数据')
return
}
let postData = [];
const numPattern = /^\d+(\.\d+)?$/;//
for (let i = 0; i < res.length; i++) {
let d = res[i];
let obj = {};
Object.keys(backMoneyColumnKeys).map(key => {
if (['receivableDate', 'incomeConfirmdate'].indexOf(key) != -1) {//
obj[key] = judgeNullTime(d[backMoneyColumnKeys[key]]);
} else {
obj[key] = d[backMoneyColumnKeys[key]] || null;
}
})
//
if (!obj.year || !obj.serialNo || !obj.number) {
error(`${i + 2}行【年度】、【序号】、【编号】存在空值,请填写`)
return
}
let exist = allNumbers.find(m => m.number == obj.number);//
if (exist) {
error(`${i + 2}行的【编号】在系统中已存在`)
return
}
if (postData.some(p => p.number == obj.number)) {//
error(`${i + 2}行【编号】重复,请更改后重新上传`)
return
}
//
if (obj.amount && !numPattern.test(obj.amount)) {
error(`${i + 2}行【合同金额】填写错误,需要为数字`)
return
}
if (obj.changeAmount && !numPattern.test(obj.changeAmount)) {
error(`${i + 2}行【变更后合同金额】填写错误,需要为数字`)
return
}
if (obj.receivableAmount && !numPattern.test(obj.receivableAmount)) {
error(`${i + 2}行【回款金额】填写错误,需要为数字`)
return
}
if (obj.remainConAmount && !numPattern.test(obj.remainConAmount)) {
error(`${i + 2}行【剩余合同金额】填写错误,需要为数字`)
return
}
//
let tValid = judgeTimeValid(obj.receivableDate);
if (!tValid) {
error(`${i + 2}行回款日期错误,请填写yyyy/mm/dd格式`)
return
}
let cValid = judgeTimeValid(obj.incomeConfirmdate);
if (!cValid) {
error(`${i + 2}行收入确认时间错误,请填写yyyy/mm/dd格式`)
return
}
postData.push(obj);
}
setPostData(postData)
let msg = '文件解析完成,点击确定按钮上传保存!'
setMsg(msg)
onSuccess({ message: msg })
})
}}>
<Button icon={<IconUpload />} theme="light" disabled={!uploadAble}>
请选择文件
</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)(ImportBackModal);

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

@ -1,6 +1,6 @@
import React, { useEffect, useState, useRef, useMemo } from 'react'; import React, { useEffect, useState, useRef, useMemo } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Select, Input, Button, Table, Pagination, Skeleton } from '@douyinfe/semi-ui'; import { Select, Input, Button, Table, Pagination, Skeleton, Toast, } from '@douyinfe/semi-ui';
import { SkeletonScreen, Setup } from "$components"; import { SkeletonScreen, Setup } from "$components";
import { IconSearch } from '@douyinfe/semi-icons'; import { IconSearch } from '@douyinfe/semi-icons';
import ImportInvoicingDetailsModal from './importInvoicingDetailsModal'; import ImportInvoicingDetailsModal from './importInvoicingDetailsModal';
@ -9,8 +9,8 @@ import '../../style.less';
import moment from 'moment' import moment from 'moment'
const InvoicingDetails = (props) => { const InvoicingDetails = (props) => {
const { dispatch, actions } = props const { dispatch, actions, user } = props
const { } = actions; const { businessManagement } = actions;
const [keywordTarget, setKeywordTarget] = useState('contractNo'); const [keywordTarget, setKeywordTarget] = useState('contractNo');
const [keyword, setKeyword] = useState('');// const [keyword, setKeyword] = useState('');//
const [limits, setLimits] = useState()// const [limits, setLimits] = useState()//
@ -20,6 +20,11 @@ const InvoicingDetails = (props) => {
const [setup, setSetup] = useState(false);// const [setup, setSetup] = useState(false);//
const [setupp, setSetupp] = useState([]);// const [setupp, setSetupp] = useState([]);//
const [exportUrl, setExportUrl] = useState('');
const page = useRef(query.page);
const scroll = useMemo(() => ({}), []);
const INVOICINGDETAILS = "invoicingDetails"; const INVOICINGDETAILS = "invoicingDetails";
const renderColumns = (columnKeys) => { const renderColumns = (columnKeys) => {
let columns = []; let columns = [];
@ -29,7 +34,8 @@ const InvoicingDetails = (props) => {
default: default:
columns.push({ columns.push({
title: columnKeys[key], dataIndex: key, key: key, title: columnKeys[key], dataIndex: key, key: key,
render: (text, record) => text === 0 ? text : text ? text : '—', width: 32 + columnKeys[key].length * 16 render: (text, record) => text === 0 ? text : text ? text : '—',
width: 32 + columnKeys[key].length * 16
}); });
break; break;
} }
@ -52,6 +58,7 @@ const InvoicingDetails = (props) => {
) )
: ""; : "";
attribute(); attribute();
getInvoicingDetailData();
}, []); }, []);
// //
function attribute() { function attribute() {
@ -66,9 +73,16 @@ const InvoicingDetails = (props) => {
} }
setSetupp(newColumns); setSetupp(newColumns);
} }
useEffect(() => {
}, [query]) function getInvoicingDetailData(param) {
let queryParam = param || query;
dispatch(businessManagement.getInvoicingDetail({ keywordTarget, keyword, ...queryParam })).then(r => {
if (r.success) {
setTableData(r.payload?.data?.rows);
setLimits(r.payload?.data?.count);
}
})
}
function handleRow(record, index) {// function handleRow(record, index) {//
if (index % 2 === 0) { if (index % 2 === 0) {
return { return {
@ -80,7 +94,22 @@ const InvoicingDetails = (props) => {
return {}; return {};
} }
} }
const scroll = useMemo(() => ({}), []);
const exportAllData = () => {
dispatch(businessManagement.getInvoicingDetail({ limit: 1, page: 0 })).then((res) => {
if (res.success) {
if (res.payload.data.count) {
let url = `export/invoicing/detail?token=${user.token}&timestamp=${moment().valueOf()}`
setExportUrl(url);
} else {
Toast.info({
content: '暂无可导出的数据',
duration: 3,
})
}
}
})
}
return ( return (
<> <>
<div style={{ padding: '0px 12px' }}> <div style={{ padding: '0px 12px' }}>
@ -120,6 +149,7 @@ const InvoicingDetails = (props) => {
<Button theme='solid' type='primary' style={{ width: 80, borderRadius: 2, height: 32, background: '#DBECFF', color: '#005ABD' }} <Button theme='solid' type='primary' style={{ width: 80, borderRadius: 2, height: 32, background: '#DBECFF', color: '#005ABD' }}
onClick={() => { onClick={() => {
setQuery({ limit: 10, page: 0 }) setQuery({ limit: 10, page: 0 })
getInvoicingDetailData({ limit: 10, page: 0 })
}}>查询</Button> }}>查询</Button>
</div> </div>
<div style={{ display: 'flex', marginRight: 20 }}> <div style={{ display: 'flex', marginRight: 20 }}>
@ -130,7 +160,10 @@ const InvoicingDetails = (props) => {
onClick={() => { setImportModalV(true); }}> onClick={() => { setImportModalV(true); }}>
导入 导入
</div> </div>
<div style={{ padding: '6px 20px', background: '#00BA85', color: '#FFFFFF', fontSize: 14, marginLeft: 18 }}> <div style={{ padding: '6px 20px', background: '#00BA85', color: '#FFFFFF', fontSize: 14, marginLeft: 18, cursor: "pointer" }}
onClick={() => {
exportAllData()
}}>
导出全部 导出全部
</div> </div>
</div> </div>
@ -185,20 +218,25 @@ const InvoicingDetails = (props) => {
setImportModalV(false); setImportModalV(false);
}} /> : '' }} /> : ''
} }
{
exportUrl ? <iframe src={`/_api/${exportUrl}`} style={{ display: 'none' }} /> : ''
}
</div> </div>
{setup ? ( {
<Setup setup ? (
tableType={INVOICINGDETAILS} <Setup
tableList={tableList} tableType={INVOICINGDETAILS}
length={19} tableList={tableList}
close={() => { length={19}
setSetup(false); close={() => {
attribute(); setSetup(false);
}} attribute();
/> }}
) : ( />
"" ) : (
)} ""
)
}
</> </>
) )
} }

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

@ -5,31 +5,37 @@ export const AxyRequest = new ProxyRequest("_axy");
export const EmisRequest = new ProxyRequest("_emis") export const EmisRequest = new ProxyRequest("_emis")
export const webUtils = new customWebUtils({ export const webUtils = new customWebUtils({
userKey: 'dcUser' userKey: 'dcUser'
}); });
const { basicAction, RouteRequest } = webUtils const { basicAction, RouteRequest } = webUtils
export { export {
basicAction, RouteRequest basicAction, RouteRequest
} }
export const ApiTable = { export const ApiTable = {
login: "login", login: "login",
logout: "logout", logout: "logout",
//项目报表 //项目报表
getReserveItemReport: "reserveItem/report/{type}", getReserveItemReport: "reserveItem/report/{type}",
//销售人员分布明细表 //销售人员分布明细表
getSalesList: 'sales/member/list', getSalesList: 'sales/member/list',
//业绩报表 //业绩报表
getReceivedDetail: 'detail/received/back', getReceivedDetail: 'detail/received/back',
getAchievementDetail: 'detail/achievement', getAchievementDetail: 'detail/achievement',
getContractDetail: 'contract/detail',
//客户联系人对接跟进 getReceivedNumbers: 'detail/received/numbers',
getCustomerContactsFollowup:'customerContactsFollup' importBackDetails: 'add/received/back/bulk',
importAchieveDetails: 'add/achievement/bulk',
getContractDetail: 'contract/detail',
getInvoicingDetail: 'invoicing/detail',
//客户联系人对接跟进
getCustomerContactsFollowup:'customerContactsFollup'
}; };
export const RouteTable = { export const RouteTable = {
apiRoot: "/api/root", apiRoot: "/api/root",
fileUpload: "/_upload/new", fileUpload: "/_upload/new",
cleanUpUploadTrash: "/_upload/cleanup", cleanUpUploadTrash: "/_upload/cleanup",
getServiceUrl: '/_service/url' getServiceUrl: '/_service/url'
}; };

10
web/config.js

@ -12,6 +12,10 @@ dev && console.log('\x1B[33m%s\x1b[0m', '请遵循并及时更新 readme.md,
args.option(['p', 'port'], '启动端口'); args.option(['p', 'port'], '启动端口');
args.option(['u', 'api-url'], 'webapi的URL'); args.option(['u', 'api-url'], 'webapi的URL');
args.option('apiHrUrl', 'webapi的URL 外网可访问'); args.option('apiHrUrl', 'webapi的URL 外网可访问');
args.option(['d', 'domain'], 'web domain');
args.option('webPepUrl', '企业管理 web');
// 七牛 // 七牛
args.option('qnak', 'qiniuAccessKey'); args.option('qnak', 'qiniuAccessKey');
@ -24,6 +28,8 @@ const flags = args.parse(process.argv);
const API_URL = process.env.API_URL || flags.apiUrl; const API_URL = process.env.API_URL || flags.apiUrl;
const API_DC_URL = process.env.API_DC_URL || flags.apiHrUrl; const API_DC_URL = process.env.API_DC_URL || flags.apiHrUrl;
const FS_REPORT_DOMAIN = process.FS_REPORT_DOMAIN || flags.domain;
const WEB_PEP_URL = process.env.WEB_PEP_URL || flags.webPepUrl;
// 七牛 // 七牛
const ANXINCLOUD_QINIU_AK = process.env.ANXINCLOUD_QINIU_ACCESSKEY || flags.qnak; const ANXINCLOUD_QINIU_AK = process.env.ANXINCLOUD_QINIU_ACCESSKEY || flags.qnak;
@ -76,7 +82,9 @@ const product = {
}, },
service: { service: {
url: ANXINCLOUD_PM_SERVICES url: ANXINCLOUD_PM_SERVICES
} },
domain: FS_REPORT_DOMAIN,
webPepUrl: WEB_PEP_URL
} }
}, { }, {
entry: require('./client').entry,// 静态信息 entry: require('./client').entry,// 静态信息

3
web/package.json

@ -7,7 +7,7 @@
"test": "mocha", "test": "mocha",
"start-vite": "cross-env NODE_ENV=developmentVite npm run start-params", "start-vite": "cross-env NODE_ENV=developmentVite npm run start-params",
"start": "cross-env NODE_ENV=development npm run start-params", "start": "cross-env NODE_ENV=development npm run start-params",
"start-params": "node server -p 5700 -u http://localhost:4700 --apiHrUrl http://localhost:4700 --qnak 5XrM4wEB9YU6RQwT64sPzzE6cYFKZgssdP5Kj3uu --qnsk w6j2ixR_i-aelc6I7S3HotKIX-ukMzcKmDfH6-M5 --qnbkt pep-process-report --qndmn https://pepsource.anxinyun.cn --pmrs http://10.8.30.109:14000", "start-params": "node server -p 5700 -u http://localhost:4700 --apiHrUrl http://localhost:4700 -d localhost --webPepUrl http://localhost:5300 --qnak 5XrM4wEB9YU6RQwT64sPzzE6cYFKZgssdP5Kj3uu --qnsk w6j2ixR_i-aelc6I7S3HotKIX-ukMzcKmDfH6-M5 --qnbkt pep-process-report --qndmn https://pepsource.anxinyun.cn --pmrs http://10.8.30.109:14000",
"deploy": "export NODE_ENV=production&& npm run build && node server", "deploy": "export NODE_ENV=production&& npm run build && node server",
"build-dev": "cross-env NODE_ENV=development&&webpack --config webpack.config.js", "build-dev": "cross-env NODE_ENV=development&&webpack --config webpack.config.js",
"build": "cross-env NODE_ENV=production&&webpack --config webpack.config.prod.js" "build": "cross-env NODE_ENV=production&&webpack --config webpack.config.prod.js"
@ -60,6 +60,7 @@
"ezuikit-js": "^0.6.1", "ezuikit-js": "^0.6.1",
"fs-attachment": "^1.0.0", "fs-attachment": "^1.0.0",
"fs-web-server-scaffold": "^1.0.6", "fs-web-server-scaffold": "^1.0.6",
"js-cookie": "^3.0.1",
"js-export-excel": "^1.1.4", "js-export-excel": "^1.1.4",
"koa-better-http-proxy": "^0.2.5", "koa-better-http-proxy": "^0.2.5",
"koa-proxy": "^1.0.0-alpha.3", "koa-proxy": "^1.0.0-alpha.3",

4
web/routes/attachment/index.js

@ -20,11 +20,13 @@ module.exports = {
entry: function (app, router, opts) { entry: function (app, router, opts) {
const getApiRoot = async function (ctx) { const getApiRoot = async function (ctx) {
const { apiUrl } = opts; const { apiUrl, domain, webPepUrl } = opts;
ctx.status = 200; ctx.status = 200;
ctx.body = { ctx.body = {
root: apiUrl, root: apiUrl,
domain: domain,
webPepUrl
}; };
}; };

Loading…
Cancel
Save