ww664853070 2 years ago
parent
commit
5e9a67c35e
  1. 55
      api/app/lib/controllers/report/achievement.js
  2. 2
      api/app/lib/controllers/report/index.js
  3. 8
      api/app/lib/routes/report/index.js
  4. 6
      web/client/src/layout/components/sider/index.jsx
  5. 6
      web/client/src/sections/business/actions/achievement-report.js
  6. 6
      web/client/src/sections/business/constants/index.js
  7. 2
      web/client/src/sections/business/containers/performanceReport/achievementDetails.jsx
  8. 2
      web/client/src/sections/business/containers/performanceReport/backMoneyDetails.jsx
  9. 5
      web/client/src/sections/business/containers/performanceReport/contractDetails.jsx
  10. 6
      web/client/src/sections/business/containers/performanceReport/importAchieveModal.jsx
  11. 36
      web/client/src/sections/business/containers/performanceReport/importBackModal.jsx
  12. 94
      web/client/src/sections/business/containers/performanceReport/importContractDetailsModal.js

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

@ -12,7 +12,7 @@ async function getReceivedDetail(ctx) {
}
let findOption = {
where: where,
order: [['id', 'DESC']]
order: [['id', 'ASC']]
}
if (!toExport) {//非导出时考虑分页
if (limit) {
@ -102,7 +102,7 @@ async function exportReceivedDetail(ctx, dataList) {
exportData.push(item)
}
const fileName = `回款明细表_${moment().format('YYYYMMDDHHmmss')}` + '.xlsx'
const filePath = await simpleExcelDown({ data: exportData, header, fileName: fileName })
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');
@ -132,7 +132,7 @@ async function getAchievementDetail(ctx) {
}
let findOption = {
where: where,
order: [['id', 'DESC']]
order: [['id', 'ASC']]
}
if (!toExport) {//非导出时考虑分页
if (limit) {
@ -225,20 +225,20 @@ async function exportAchievementDetail(ctx, dataList) {
title: '是否可复制的业务路径',
key: 'reproducible',
}, {
title: '省外业务1.1',
title: '省外业务',
key: 'outProvince',
}, {
title: '复购业务1.05',
title: '复购业务',
key: 'repurchase',
}, {
title: '可复制的业务路径1.1',
title: '可复制的业务路径',
key: 'isreproduce',
}]
const { utils: { simpleExcelDown } } = ctx.app.fs;
let exportData = []
for (let { dataValues: item } of dataList) {
item.isApproval = item.isApproval ? '是' : '否';
item.reproducible = item.reproducible ? '是' : '否';
item.isApproval = JSON.stringify(item.isApproval) === 'null' ? '-' : item.isApproval ? '是' : '否';
item.reproducible = JSON.stringify(item.reproducible) === 'null' ? '-' : item.reproducible ? '是' : '否';
exportData.push(item)
}
const fileName = `业绩明细表_${moment().format('YYYYMMDDHHmmss')}` + '.xlsx'
@ -256,11 +256,17 @@ async function exportAchievementDetail(ctx, dataList) {
}
}
}
/**
* 查询明细表已存在编号数据回款合同开票通用
* @param {*} ctx ctx ctx.query:{tableModel表模型名}
*/
async function getReceivedNumbers(ctx) {
let errorMsg = { name: 'FindError', message: '查询失败' };
try {
const { tableModel } = ctx.query;
const models = ctx.fs.dc.models;
let list = await models.ReceivableDetail.findAll({//查编号
let modelName = tableModel || 'ReceivableDetail'
let list = await models[modelName].findAll({//查编号
attributes: ['number']
});
ctx.status = 200
@ -268,20 +274,23 @@ async function getReceivedNumbers(ctx) {
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : undefined
}
ctx.body = errorMsg;
}
}
/**
* 导入明细表数据回款合同开票通用
* @param {*} ctx ctx ctx.query:{tableModel表模型名}
*/
async function importBackDetails(ctx) {
let errorMsg = { message: '导入回款明细失败' };
let errorMsg = { message: '导入失败' };
const transaction = await ctx.fs.dc.orm.transaction();
try {
const models = ctx.fs.dc.models;
const data = ctx.request.body;
const { tableModel } = ctx.query;
let modelName = tableModel || 'ReceivableDetail'
let addArr = [];
let dataList = await models.ReceivableDetail.findAll({//查编号
let dataList = await models[modelName].findAll({//查编号
attributes: ['number']
});
data.map(d => {
@ -292,7 +301,7 @@ async function importBackDetails(ctx) {
})
//只处理新增的
if (addArr.length) {
await models.ReceivableDetail.bulkCreate(addArr);
await models[modelName].bulkCreate(addArr, { transaction });
}
await transaction.commit();
ctx.status = 204;
@ -340,7 +349,7 @@ async function getContractDetail(ctx) {
where: where,
offset: Number(page) * Number(limit),
limit: Number(limit),
order: [['id', 'DESC']]
order: [['id', 'ASC']]
});
ctx.status = 200
ctx.body = contractDetail;
@ -367,7 +376,7 @@ async function getInvoicingDetail(ctx) {
where: where,
offset: Number(page) * Number(limit),
limit: Number(limit),
order: [['id', 'DESC']]
order: [['id', 'ASC']]
});
ctx.status = 200
ctx.body = invoiceDetail;
@ -385,7 +394,7 @@ async function exportContractDetail(ctx) {
try {
const { models } = ctx.fs.dc;
let exportData = await models.ContractDetail.findAll({
order: [['id', 'DESC']]
order: [['id', 'ASC']]
});
const { utils: { simpleExcelDown, contractDetailsColumnKeys } } = ctx.app.fs;
let header = [];
@ -414,7 +423,7 @@ async function exportInvoicingDetail(ctx) {
try {
const { models } = ctx.fs.dc;
let exportData = await models.InvoiceDetail.findAll({
order: [['id', 'DESC']]
order: [['id', 'ASC']]
});
const { utils: { simpleExcelDown, invoicingDetailsColumnKeys } } = ctx.app.fs;
let header = [];
@ -438,8 +447,8 @@ module.exports = {
getReceivedDetail,//回款
getAchievementDetail,//业绩
getReceivedNumbers,//查询回款明细表 已有的所有编号
importBackDetails,//导入回款明细
getReceivedNumbers,//查询回款、合同、开票明细表 已有的所有编号
importBackDetails,//导入回款、合同、开票明细
importAchieveDetails,//导入业绩明细
getContractDetail,
getInvoicingDetail,

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

@ -58,7 +58,7 @@ async function getSalersReport(ctx) {
const salersRes = await clickHouse.hr.query(`
SELECT * from sales_distribution as sales
${innerSelectQuery}
order by id desc
order by id asc
${!toExport && limit ? `LIMIT ${limit}` : ''}
${!toExport && limit && page ? 'OFFSET ' + parseInt(limit) * parseInt(page) : ''}
`).toPromise()

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

@ -16,17 +16,15 @@ module.exports = function (app, router, opts) {
app.fs.api.logAttr['GET/detail/achievement'] = { content: '查询业绩明细表', visible: false };
router.get('/detail/achievement', achieve.getAchievementDetail);
app.fs.api.logAttr['GET/detail/received/numbers'] = { content: '查询业绩明细已有的编号集合', visible: false };
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 };
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 };
router.get('/contract/detail', achieve.getContractDetail);

6
web/client/src/layout/components/sider/index.jsx

@ -18,9 +18,9 @@ const Sider = (props) => {
let nextItems = leftItems
setItems(nextItems)
scrollbar = new PerfectScrollbar('#page-slider', { suppressScrollX: true });
if (pathname == '/') {
dispatch(push(homePath))
}
// if (pathname == '/') {
// dispatch(push(homePath))
// }
}, [leftItems])
let routeSelectedKey = useLocation().pathname.split('/');
// let routeSelectedKey = [useLocation().pathname.split('/')[1]]//

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

@ -29,10 +29,11 @@ export function getAchievementDetail(query) {
}
//回款明细 编号集合
export function getReceivedNumbers() {
export function getReceivedNumbers(query = {}) {
return (dispatch) => basicAction({
type: "get",
dispatch: dispatch,
query: query,
actionType: "GET_RECEIVED_DETAIL_NUMBERS",
url: `${ApiTable.getReceivedNumbers}`,
msg: { option: "查询回款明细编号集合" },
@ -40,10 +41,11 @@ export function getReceivedNumbers() {
});
}
//导入回款明细
export function importBackDetails(values) {
export function importBackDetails(values, query = {}) {
return dispatch => basicAction({
type: 'post',
dispatch: dispatch,
query: query,
actionType: 'RECEIVED_DETAIL_BULK_ADD',
url: ApiTable.importBackDetails,
data: values,

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

@ -116,7 +116,7 @@ export const achievementColumnKeys = {
cusAttribute: '客户属性',
repurchaseCount: '复购次数',
reproducible: '是否可复制的业务路径',
outProvince: '省外业务1.1',
repurchase: '复购业务1.05',
isreproduce: '可复制的业务路径1.1',
outProvince: '省外业务',
repurchase: '复购业务',
isreproduce: '可复制的业务路径',
}

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

@ -46,7 +46,7 @@ const AchievementDetails = (props) => {
switch (key) {
default:
columns.push({
title: columnKeys[key], dataIndex: key, key: key, width: 130,
title: columnKeys[key], dataIndex: key, key: key, width: 120,
render: (text, record) => ['isApproval', 'reproducible'].indexOf(key) != -1 ? JSON.stringify(text) === 'null' ? '-' : text ? '是' : '否'
: text === 0 ? text : text ? text : '-'
});

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

@ -45,7 +45,7 @@ const BackMoneyDetails = (props) => {
switch (key) {
default:
columns.push({
title: columnKeys[key], dataIndex: key, key: key, width: 130,
title: columnKeys[key], dataIndex: key, key: key, width: 115,
render: (text, record) => text === 0 ? text : text ? text : '-'
});
break;

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

@ -34,8 +34,8 @@ const ContractDetails = (props) => {
default:
columns.push({
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 * 32
});
break;
}
@ -218,6 +218,7 @@ const ContractDetails = (props) => {
importModalV ? <ImportContractDetailsModal
onCancel={() => {
setImportModalV(false);
getContractDetailData();
}} /> : ''
}
{

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

@ -163,6 +163,7 @@ const ImportAchieveModal = props => {
return
}
let postData = [];
let zzsPattern = /^[+]{0,1}(\d+)$/;//
for (let i = 0; i < res.length; i++) {
let d = res[i];
let obj = {};
@ -183,6 +184,11 @@ const ImportAchieveModal = props => {
error(`${i + 2}行【价格是否特批】错误,请填写是或否`)
return
}
//
if (obj.repurchaseCount && !zzsPattern.test(obj.repurchaseCount)) {
error(`${i + 2}行【复购次数】填写错误,需要为非负整数`)
return
}
if (obj.reproducible && ['是', '否'].indexOf(obj.reproducible) == -1) {
error(`${i + 2}行【是否可复制的业务路径】错误,请填写是或否`)

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

@ -171,7 +171,9 @@ const ImportBackModal = props => {
return
}
let postData = [];
const numPattern = /^\d+(\.\d+)?$/;//
//const numPattern = /^\d+(\.\d+)?$/;//
let zzsPattern = /^[+]{0,1}(\d+)$/;//
let yearPattern = /^(19|20)\d{2}$/;//1900-2099
for (let i = 0; i < res.length; i++) {
let d = res[i];
let obj = {};
@ -187,6 +189,16 @@ const ImportBackModal = props => {
error(`${i + 2}行【年度】、【序号】、【编号】存在空值,请填写`)
return
}
//
if (obj.year && !yearPattern.test(obj.year)) {
error(`${i + 2}行【年度】填写错误`)
return
}
//
if (obj.serialNo && !zzsPattern.test(obj.serialNo)) {
error(`${i + 2}行【序号】填写错误,需要为非负整数`)
return
}
let exist = allNumbers.find(m => m.number == obj.number);//
if (exist) {
error(`${i + 2}行的【编号】在系统中已存在`)
@ -196,32 +208,20 @@ const ImportBackModal = props => {
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}行【剩余合同金额】填写错误,需要为数字`)
//
if (obj.receivableYear && !yearPattern.test(obj.receivableYear)) {
error(`${i + 2}行【回款年份】填写错误`)
return
}
//
let tValid = judgeTimeValid(obj.receivableDate);
if (!tValid) {
error(`${i + 2}行回款日期错误,请填写yyyy/mm/dd格式`)
error(`${i + 2}行【回款日期】错误,请填写yyyy/mm/dd格式`)
return
}
let cValid = judgeTimeValid(obj.incomeConfirmdate);
if (!cValid) {
error(`${i + 2}行收入确认时间错误,请填写yyyy/mm/dd格式`)
error(`${i + 2}行【收入确认时间】错误,请填写yyyy/mm/dd格式`)
return
}
postData.push(obj);

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

@ -4,27 +4,40 @@ 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 moment from 'moment';
import { contractDetailsColumnKeys } from '../../constants/index';
const ColumnDateKey = ['applyDate', 'recConDate', 'acceptanceDate', 'backConfirmDate'];
const ColumnDateName = ['申请日期', '收到合同日期', '验收日期', '收入确认时间'];
//下载模板和上传文件读取
const ImportContractDetailsModal = props => {
const { dispatch, actions, onCancel } = props;
const { humanAffairs } = actions;
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({ tableModel: 'ContractDetail' })).then(r => {
if (r.success) {
setUploadAble(true);
setAllNumbers(r.payload.data);
}
})
}, []);
const confirm = () => {
if (postData.length) {
setLoading(true)
// dispatch(humanAffairs.addSalesMemberBulk(postData)).then(res => {
// if (res.success) {
// onCancel()
// }
// setLoading(false)
// })
//导入明细接口通用
dispatch(businessManagement.importBackDetails(postData, { tableModel: 'ContractDetail' })).then(res => {
if (res.success) {
onCancel()
}
setLoading(false)
})
} else {
Notification.warning({ content: '没有数据可以提交,请上传数据文件', duration: 2 })
}
@ -94,9 +107,29 @@ const ImportContractDetailsModal = props => {
})
}
const judgeNullTime = (v) => {
//注意的点:xlsx将excel中的时间内容解析后,会小一天
//如2020/8/1,xlsx会解析成 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 judgeNull = (value) => {
return value ? String(value).trim().replace(/\s*/g, "") : 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 (
@ -137,12 +170,51 @@ const ImportContractDetailsModal = props => {
return
}
let postData = [];
let yearPattern = /^(19|20)\d{2}$/;//1900-2099年
let zzsPattern = /^[+]{0,1}(\d+)$/;//正整数
for (let i = 0; i < res.length; i++) {
let d = res[i];
let obj = {};
Object.keys(contractDetailsColumnKeys).map(key => {
obj[key] = d[contractDetailsColumnKeys[key]] || '';
//四个日期验证“申请日期”、“收到合同日期”、“验收日期”、“收入确认时间”
if (ColumnDateKey.indexOf(key) != -1) {//两个时间
obj[key] = judgeNullTime(d[contractDetailsColumnKeys[key]]);
} else {
obj[key] = d[contractDetailsColumnKeys[key]] || null;
}
})
//年度 序号 编号必填
if (!obj.year || !obj.serialNo || !obj.number) {
error(`${i + 2}行【年度】、【序号】、【编号】存在空值,请填写`)
return
}
//年度
if (obj.year && !yearPattern.test(obj.year)) {
error(`${i + 2}行【年度】填写错误`)
return
}
//序号 正整数
if (obj.serialNo && !zzsPattern.test(obj.serialNo)) {
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
}
for (let k = 0; k < ColumnDateKey.length; k++) {
let cValid = judgeTimeValid(obj[ColumnDateKey[k]]);
if (!cValid) {
error(`${i + 2}行【${ColumnDateName[k]}】错误,请填写yyyy/mm/dd格式`)
return
}
}
postData.push(obj);
}
setPostData(postData)
@ -151,7 +223,7 @@ const ImportContractDetailsModal = props => {
onSuccess({ message: msg })
})
}}>
<Button icon={<IconUpload />} theme="light">
<Button icon={<IconUpload />} theme="light" disabled={!uploadAble}>
请选择文件
</Button>
</Form.Upload>

Loading…
Cancel
Save