diff --git a/api/app/lib/controllers/auth/index.js b/api/app/lib/controllers/auth/index.js index cfcc1f1..76d2d73 100644 --- a/api/app/lib/controllers/auth/index.js +++ b/api/app/lib/controllers/auth/index.js @@ -9,9 +9,18 @@ async function login (ctx, next) { try { const params = ctx.request.body; - const emisLoginRes = await ctx.app.fs.emisRequest.post('login', { - data: params - }) + let emisLoginRes = null + if (params.username && params.password) { + emisLoginRes = await ctx.app.fs.emisRequest.post('login', { + data: { ...params, } + }) + } else if (params.token) { + emisLoginRes = await ctx.app.fs.emisRequest.get('user-info', { + query: { + token: params.token, + } + }) + } if (!emisLoginRes) { throw "无此用户,请使用正确的登录信息" diff --git a/api/app/lib/controllers/customerContactsFollup/index.js b/api/app/lib/controllers/customerContactsFollup/index.js new file mode 100644 index 0000000..612eed1 --- /dev/null +++ b/api/app/lib/controllers/customerContactsFollup/index.js @@ -0,0 +1,23 @@ +'use strict'; + +// 查询储备项目统计表 +async function getCustomerContactsFollowup(ctx, next) { + const { type } = ctx.params; + let rslt = null; + try { + rslt = await ctx.fs.dc.models.ReserveItemReport.findAll({ + order: [['id', 'DESC']], + // where: { type: type } + }) + ctx.status = 200 + ctx.body = rslt + } catch (error) { + ctx.fs.logger.error(`path:${ctx.path},error:${error}`) + ctx.status = 400; + ctx.body = { name: 'FindAllError', message: '获取失败' } + } +} + +module.exports = { + getCustomerContactsFollowup +} \ No newline at end of file diff --git a/api/app/lib/controllers/report/achievement.js b/api/app/lib/controllers/report/achievement.js index 6a3048d..9a027ef 100644 --- a/api/app/lib/controllers/report/achievement.js +++ b/api/app/lib/controllers/report/achievement.js @@ -5,22 +5,33 @@ const moment = require('moment'); async function getReceivedDetail(ctx) { try { const { models } = ctx.fs.dc; - const { keywordTarget, keyword, limit, page } = ctx.query + const { keywordTarget, keyword, limit, page, toExport } = ctx.query let where = {} if (keywordTarget == 'contract' && keyword) { where.contractNo = { $like: `%${keyword}%` } } - let res = await models.ReceivableDetail.findAndCountAll({ + let findOption = { where: where, - offset: Number(page) * Number(limit), - limit: Number(limit), order: [['id', 'DESC']] - }) - ctx.status = 200 - ctx.body = { - count: res.count, - rows: res.rows - }; + } + if (!toExport) {//非导出时考虑分页 + if (limit) { + findOption.limit = Number(limit) + } + if (page && limit) { + findOption.offset = Number(page) * Number(limit) + } + } + let res = await models.ReceivableDetail.findAndCountAll(findOption); + if (toExport) {//数据导出相关 + await exportReceivedDetail(ctx, res.rows); + } else { + ctx.status = 200 + ctx.body = { + count: res.count, + rows: res.rows + }; + } } catch (error) { ctx.fs.logger.error(`path:${ctx.path},error:${error}`) ctx.status = 400; @@ -28,10 +39,87 @@ async function getReceivedDetail(ctx) { } } +async function exportReceivedDetail(ctx, dataList) { + try { + let header = [{ + title: '年度', + key: 'year', + }, { + title: '序号', + key: 'serialNo', + }, { + title: '编号', + key: 'number', + }, { + title: '部门', + key: 'department', + }, { + title: '销售人员', + key: 'sale', + }, { + title: '合同编号', + key: 'contractNo', + }, { + title: '客户名称', + key: 'customer', + }, { + title: '项目名称', + key: 'item', + }, { + title: '合同金额', + key: 'amount', + }, { + title: '变更后合同金额', + key: 'changeAmount', + }, { + title: '回款年份', + key: 'receivableYear', + }, { + title: '回款日期', + key: 'receivableDate', + }, { + title: '回款金额', + key: 'receivableAmount', + }, { + title: '开票-回款', + key: 'invoicedBack', + }, { + title: '剩余合同金额', + key: 'remainConAmount', + }, { + title: '收入确认时间', + key: 'incomeConfirmdate', + }, { + title: '第三方付款单位', + key: 'thirdPayment', + }, { + title: '备注', + key: 'remark', + }] + const { utils: { simpleExcelDown } } = ctx.app.fs; + let exportData = [] + for (let { dataValues: item } of dataList) { + exportData.push(item) + } + const fileName = `回款明细表_${moment().format('YYYYMMDDHHmmss')}` + '.xlsx' + const filePath = await simpleExcelDown({ data: exportData, header, fileName: fileName }) + const fileData = fs.readFileSync(filePath); + ctx.status = 200; + ctx.set('Content-Type', 'application/x-xls'); + ctx.set('Content-disposition', 'attachment; filename=' + encodeURI(fileName)); + ctx.body = fileData; + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + message: typeof error == 'string' ? error : undefined + } + } +} async function getAchievementDetail(ctx) { try { const { models } = ctx.fs.dc; - const { keywordTarget, keyword, limit, page } = ctx.query + const { keywordTarget, keyword, limit, page, toExport } = ctx.query let where = {} if (keywordTarget == 'saler' && keyword) { where.sale = { $like: `%${keyword}%` } @@ -42,25 +130,161 @@ async function getAchievementDetail(ctx) { if (keywordTarget == 'dep' && keyword) { where.department = { $like: `%${keyword}%` } } - let res = await models.PerformanceDetail.findAndCountAll({ + let findOption = { + where: where, + order: [['id', 'DESC']] + } + if (!toExport) {//非导出时考虑分页 + if (limit) { + findOption.limit = Number(limit) + } + if (page && limit) { + findOption.offset = Number(page) * Number(limit) + } + } + let res = await models.PerformanceDetail.findAndCountAll(findOption); + if (toExport) {//数据导出相关 + await exportAchievementDetail(ctx, res.rows); + } else { + ctx.status = 200 + ctx.body = { + count: res.count, + rows: res.rows + }; + } + } catch (error) { + ctx.fs.logger.error(`path:${ctx.path},error:${error}`) + ctx.status = 400; + ctx.body = { name: 'FindAllError', message: '获取失败' } + } +} + +async function exportAchievementDetail(ctx, dataList) { + try { + let header = [{ + title: '收到合同日期', + key: 'recConDate', + }, { + title: '月份', + key: 'month', + }, { + title: '部门', + key: 'department', + }, { + title: '销售人员', + key: 'sale', + }, { + title: '客户名称', + key: 'customer', + }, { + title: '项目名称', + key: 'item', + }, { + title: '合同金额', + key: 'amount', + }, { + title: '实际业绩', + key: 'realPerformance', + }, { + title: '考核业绩', + key: 'assessmentPerformance', + }, { + title: '价格是否特批', + key: 'isApproval', + }, { + title: '特批折算比例', + key: 'approvalProp', + }, { + title: '预支提成及委外费用', + key: 'cost', + }, { + title: '业务线', + key: 'serviceLine', + }, { + title: '客户类型', + key: 'cusType', + }, { + title: '行业', + key: 'industry', + }, { + title: '信息来源', + key: 'source', + }, { + title: '项目类型', + key: 'itemType', + }, { + title: '客户省份', + key: 'cusProvince', + }, { + title: '客户属性', + key: 'cusAttribute', + }, { + title: '复购次数', + key: 'repurchaseCount', + }, { + title: '是否可复制的业务路径', + key: 'reproducible', + }, { + title: '省外业务1.1', + key: 'outProvince', + }, { + title: '复购业务1.05', + key: 'repurchase', + }, { + title: '可复制的业务路径1.1', + key: 'isreproduce', + }] + const { utils: { simpleExcelDown } } = ctx.app.fs; + let exportData = [] + for (let { dataValues: item } of dataList) { + item.isApproval = item.isApproval ? '是' : '否'; + item.reproducible = item.reproducible ? '是' : '否'; + exportData.push(item) + } + const fileName = `业绩明细表_${moment().format('YYYYMMDDHHmmss')}` + '.xlsx' + const filePath = await simpleExcelDown({ data: exportData, header, fileName: fileName }) + const fileData = fs.readFileSync(filePath); + ctx.status = 200; + ctx.set('Content-Type', 'application/x-xls'); + ctx.set('Content-disposition', 'attachment; filename=' + encodeURI(fileName)); + ctx.body = fileData; + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + message: typeof error == 'string' ? error : undefined + } + } +} + +/** + * 查询合同明细表数据 + * @param {*} ctx ctx ctx.query:{keywordTarget-关键字项、keyword-关键字内容、limit-页宽, page-页码} + */ +async function getContractDetail(ctx) { + try { + const { models } = ctx.fs.dc; + const { keywordTarget, keyword, limit, page } = ctx.query; + const where = {}; + if (keywordTarget && keyword) { + where[keywordTarget] = { $iLike: `%${keyword}%` }; + } + let contractDetail = await models.ContractDetail.findAndCountAll({ where: where, offset: Number(page) * Number(limit), limit: Number(limit), order: [['id', 'DESC']] - }) + }); ctx.status = 200 - ctx.body = { - count: res.count, - rows: res.rows - }; + ctx.body = contractDetail; } catch (error) { ctx.fs.logger.error(`path:${ctx.path},error:${error}`) ctx.status = 400; - ctx.body = { name: 'FindAllError', message: '获取失败' } + ctx.body = { name: 'FindError', message: '查询合同明细表数据失败' } } } - module.exports = { getReceivedDetail,//回款 getAchievementDetail,//业绩 + getContractDetail, } \ No newline at end of file diff --git a/api/app/lib/controllers/report/index.js b/api/app/lib/controllers/report/index.js index 3b98a20..460b1cf 100644 --- a/api/app/lib/controllers/report/index.js +++ b/api/app/lib/controllers/report/index.js @@ -133,12 +133,12 @@ async function exportSalesDetail(ctx, dataList) { let exportData = [] for (let item of dataList) { item.department = item.department.map(t => t.name).join('、') || '-'; - item.cities = item.cities || '-'; - item.businessLines = item.businessLines || '-'; - item.post = item.post || '-'; - item.regularDate = item.regularDate || '-'; + // item.cities = item.cities || '-'; + // item.businessLines = item.businessLines || '-'; + // item.post = item.post || '-'; + // item.regularDate = item.regularDate || '-'; item.workYears = item.hireDate ? String(moment(new Date()).diff(item.hireDate, 'years', true)).substring(0, 3) + '年' : '-' - item.hireDate = item.hireDate || '-'; + //item.hireDate = item.hireDate || '-'; exportData.push(item) } const fileName = `销售人员分布明细表_${moment().format('YYYYMMDDHHmmss')}` + '.xlsx' diff --git a/api/app/lib/models/customerContactsFollup.js b/api/app/lib/models/customerContactsFollup.js new file mode 100644 index 0000000..53537c1 --- /dev/null +++ b/api/app/lib/models/customerContactsFollup.js @@ -0,0 +1,65 @@ +/* eslint-disable*/ +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const ReserveItemReport = sequelize.define("reserveItemReport", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + field: "id", + autoIncrement: true, + }, + customer: { + type: DataTypes.STRING, + allowNull: true, + field: "customer", + }, + items: { + type: DataTypes.STRING, + allowNull: true, + field: "items", + }, + department: { + type: DataTypes.STRING, + allowNull: false, + field: "department", + }, + sale: { + type: DataTypes.STRING, + allowNull: true, + field: "sale", + }, + updatetime: { + type: DataTypes.DATE, + allowNull: false, + field: "updatetime", + }, + customerContacts: { + type: DataTypes.STRING, + allowNull: true, + field: "customer_contacts", + }, + phone: { + type: DataTypes.STRING, + allowNull: true, + field: "phone", + }, + visitStyle: { + type: DataTypes.STRING, + allowNull: true, + field: "visit_style", + }, + itemText: { + type: DataTypes.STRING, + allowNull: true, + field: "item_text", + } + }, { + tableName: "customer_contacts_followup", + }); + dc.models.ReserveItemReport = ReserveItemReport; + return ReserveItemReport; +}; \ No newline at end of file diff --git a/api/app/lib/routes/customerContactsFollup/index.js b/api/app/lib/routes/customerContactsFollup/index.js new file mode 100644 index 0000000..b8c0cee --- /dev/null +++ b/api/app/lib/routes/customerContactsFollup/index.js @@ -0,0 +1,8 @@ +'use strict'; + +const report = require('../../controllers/customerContactsFollup'); + +module.exports = function (app, router, opts) { + app.fs.api.logAttr['GET/customerContactsFollup'] = { content: '客户联系人对接跟进', visible: false }; + router.get('/customerContactsFollup', report.getReserveItemReport); +}; \ No newline at end of file diff --git a/api/app/lib/routes/report/index.js b/api/app/lib/routes/report/index.js index 9ad3518..b986cd6 100644 --- a/api/app/lib/routes/report/index.js +++ b/api/app/lib/routes/report/index.js @@ -15,4 +15,7 @@ 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/contract/detail'] = { content: '查询合同明细表', visible: false }; + router.get('/contract/detail', achieve.getContractDetail); }; \ No newline at end of file diff --git a/web/client/src/sections/business/actions/achievement-report.js b/web/client/src/sections/business/actions/achievement-report.js index 9f06b51..b7d5e94 100644 --- a/web/client/src/sections/business/actions/achievement-report.js +++ b/web/client/src/sections/business/actions/achievement-report.js @@ -26,4 +26,17 @@ export function getAchievementDetail(query) { msg: { option: "查询业绩明细表" }, reducer: { name: "AchievementDetail", params: { noClear: true } }, }); +} + +//查询合同明细表 +export function getContractDetail(query) { + return (dispatch) => basicAction({ + type: "get", + dispatch: dispatch, + actionType: "GET_CONTRACT_DETAIL", + query: query, + url: `${ApiTable.getContractDetail}`, + msg: { option: "查询合同明细表" }, + reducer: { name: "ContractDetail", params: { noClear: true } }, + }); } \ No newline at end of file diff --git a/web/client/src/sections/business/containers/performanceReport/achievementDetails.jsx b/web/client/src/sections/business/containers/performanceReport/achievementDetails.jsx index c138a4d..51e8039 100644 --- a/web/client/src/sections/business/containers/performanceReport/achievementDetails.jsx +++ b/web/client/src/sections/business/containers/performanceReport/achievementDetails.jsx @@ -1,18 +1,20 @@ import React, { useRef, useEffect, useState, useMemo } from 'react'; import { connect } from 'react-redux'; -import { Select, Input, Button, Popconfirm, Radio, Tooltip, Table, Pagination, Skeleton } from '@douyinfe/semi-ui'; +import moment from 'moment' +import { Select, Input, Button, Toast, Radio, Tooltip, Table, Pagination, Skeleton } from '@douyinfe/semi-ui'; import { IconSearch } from '@douyinfe/semi-icons'; import { SkeletonScreen } from "$components"; import '../../style.less'; const AchievementDetails = (props) => { - const { dispatch, actions } = props + const { dispatch, actions, user } = props const { businessManagement } = actions const [keywordTarget, setKeywordTarget] = useState('saler'); const [keyword, setKeyword] = useState('');//搜索内容 const [limits, setLimits] = useState()//每页实际条数 const [query, setQuery] = useState({ limit: 10, page: 0 }); //页码信息 const [tableData, setTableData] = useState([]); + const [exportUrl, setExportUrl] = useState('') const page = useRef(query.page); function seachValueChange(value) { setKeyword(value) @@ -22,6 +24,10 @@ const AchievementDetails = (props) => { getAchievementDetails() }, []); + useEffect(() => { + getAchievementDetails() + }, [query]) + function getAchievementDetails() { dispatch(businessManagement.getAchievementDetail({ keywordTarget, keyword, ...query })).then(r => { if (r.success) { @@ -36,121 +42,145 @@ const AchievementDetails = (props) => { dataIndex: 'recConDate', key: 'recConDate', width: 120, + render: (text, record) => {text || '-'} }, { title: '月份', dataIndex: 'month', key: 'month', width: 120, + render: (text, record) => {text || '-'} }, { title: '部门', dataIndex: 'department', key: 'department', width: 140, + render: (text, record) => {text || '-'} }, { title: '销售人员', dataIndex: 'sale', key: 'sale', width: 140, + render: (text, record) => {text || '-'} }, { title: '客户名称', dataIndex: 'customer', key: 'customer', width: 140, + render: (text, record) => {text || '-'} }, { title: '项目名称', dataIndex: 'item', key: 'item', width: 140, + render: (text, record) => {text || '-'} }, { title: '合同金额', dataIndex: 'amount', key: 'amount', width: 140, + render: (text, record) => {text || '-'} }, { title: '实际业绩', dataIndex: 'realPerformance', key: 'realPerformance', width: 140, + render: (text, record) => {text || '-'} }, { title: '考核业绩', dataIndex: 'assessmentPerformance', key: 'assessmentPerformance', width: 120, + render: (text, record) => {text || '-'} }, { title: '价格是否特批', dataIndex: 'isApproval', key: 'isApproval', width: 120, + render: (text, record) => {text ? '是' : '否'} }, { title: '特批折算比例', dataIndex: 'approvalProp', key: 'approvalProp', width: 120, + render: (text, record) => {text || '-'} }, { title: '预支提成及委外费用', dataIndex: 'cost', key: 'cost', width: 140, + render: (text, record) => {text || '-'} }, { title: '业务线', dataIndex: 'serviceLine', key: 'serviceLine', width: 120, + render: (text, record) => {text || '-'} }, { title: '客户类型', dataIndex: 'cusType', key: 'cusType', width: 140, + render: (text, record) => {text || '-'} }, { title: '行业', dataIndex: 'industry', key: 'industry', width: 140, + render: (text, record) => {text || '-'} }, { title: '信息来源', dataIndex: 'source', key: 'source', width: 120, + render: (text, record) => {text || '-'} }, { title: '项目类型', dataIndex: 'itemType', key: 'itemType', width: 120, + render: (text, record) => {text || '-'} }, { title: '客户省份', dataIndex: 'cusProvince', key: 'cusProvince', width: 140, + render: (text, record) => {text || '-'} }, { title: '客户属性', dataIndex: 'cusAttribute', key: 'cusAttribute', width: 140, + render: (text, record) => {text || '-'} }, { title: '复购次数', dataIndex: 'repurchaseCount', key: 'repurchaseCount', width: 120, + render: (text, record) => {text || '-'} }, { title: '是否可复制的业务路径', dataIndex: 'reproducible', key: 'reproducible', width: 120, + render: (text, record) => {text ? '是' : '否'} }, { title: '省外业务1.1', dataIndex: 'outProvince', key: 'outProvince', width: 140, + render: (text, record) => {text || '-'} }, { title: '复购业务1.05', dataIndex: 'repurchase', key: 'repurchase', width: 140, + render: (text, record) => {text || '-'} }, { title: '可复制的业务路径1.1', dataIndex: 'isreproduce', key: 'isreproduce', width: 120, + render: (text, record) => {text || '-'} }] function handleRow(record, index) {// 给偶数行设置斑马纹 if (index % 2 === 0) { @@ -163,6 +193,23 @@ const AchievementDetails = (props) => { return {}; } } + + const exportAllData = () => { + dispatch(businessManagement.getAchievementDetail({ limit: 1 })).then((res) => {//看看有没有 + if (res.success) { + if (res.payload.data.count) { + let url = `detail/achievement?token=${user.token}&toExport=1×tamp=${moment().valueOf()}` + setExportUrl(url); + } else { + Toast.info({ + content: '暂无可导出的数据', + duration: 3, + }) + } + } + }) + } + const scroll = useMemo(() => ({}), []); return ( <> @@ -206,12 +253,15 @@ const AchievementDetails = (props) => { setQuery({ limit: 10, page: 0 }) }}>查询 -
+
{ setImportModalV(true); }}> 导入
-
+
{ + exportAllData() + }}> 导出全部
@@ -257,6 +307,9 @@ const AchievementDetails = (props) => {
+ { + exportUrl ?