diff --git a/.vscode/launch.json b/.vscode/launch.json index 4951e72..dd08d4d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -63,8 +63,9 @@ // "--clickHouseCamworkflow camworkflow", // "--clickHouseHr hr_dev", // 测试 - "--clickHouseUrl http://10.8.16.221", - "--clickHousePepEmis pg_pepca", + "--clickHouseUrl http://10.8.30.161", + "--clickHousePepEmis pepca8", + "--clickHouseHr hrm", ] }, { 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/report/achievement.js b/api/app/lib/controllers/report/achievement.js new file mode 100644 index 0000000..9a027ef --- /dev/null +++ b/api/app/lib/controllers/report/achievement.js @@ -0,0 +1,290 @@ +'use strict'; +const fs = require('fs'); +const moment = require('moment'); +//业绩报表相关 +async function getReceivedDetail(ctx) { + try { + const { models } = ctx.fs.dc; + const { keywordTarget, keyword, limit, page, toExport } = ctx.query + let where = {} + if (keywordTarget == 'contract' && keyword) { + where.contractNo = { $like: `%${keyword}%` } + } + let findOption = { + where: where, + order: [['id', 'DESC']] + } + if (!toExport) {//非导出时考虑分页 + if (limit) { + findOption.limit = Number(limit) + } + if (page && limit) { + findOption.offset = Number(page) * Number(limit) + } + } + let res = await models.ReceivableDetail.findAndCountAll(findOption); + if (toExport) {//数据导出相关 + await exportReceivedDetail(ctx, res.rows); + } else { + ctx.status = 200 + ctx.body = { + count: res.count, + rows: res.rows + }; + } + } catch (error) { + ctx.fs.logger.error(`path:${ctx.path},error:${error}`) + ctx.status = 400; + ctx.body = { name: 'FindAllError', message: '获取失败' } + } +} + +async function exportReceivedDetail(ctx, dataList) { + try { + let header = [{ + title: '年度', + key: 'year', + }, { + title: '序号', + key: 'serialNo', + }, { + title: '编号', + key: 'number', + }, { + title: '部门', + key: 'department', + }, { + title: '销售人员', + key: 'sale', + }, { + title: '合同编号', + key: 'contractNo', + }, { + title: '客户名称', + key: 'customer', + }, { + title: '项目名称', + key: 'item', + }, { + title: '合同金额', + key: 'amount', + }, { + title: '变更后合同金额', + key: 'changeAmount', + }, { + title: '回款年份', + key: 'receivableYear', + }, { + title: '回款日期', + key: 'receivableDate', + }, { + title: '回款金额', + key: 'receivableAmount', + }, { + title: '开票-回款', + key: 'invoicedBack', + }, { + title: '剩余合同金额', + key: 'remainConAmount', + }, { + title: '收入确认时间', + key: 'incomeConfirmdate', + }, { + title: '第三方付款单位', + key: 'thirdPayment', + }, { + title: '备注', + key: 'remark', + }] + const { utils: { simpleExcelDown } } = ctx.app.fs; + let exportData = [] + for (let { dataValues: item } of dataList) { + exportData.push(item) + } + const fileName = `回款明细表_${moment().format('YYYYMMDDHHmmss')}` + '.xlsx' + const filePath = await simpleExcelDown({ data: exportData, header, fileName: fileName }) + const fileData = fs.readFileSync(filePath); + ctx.status = 200; + ctx.set('Content-Type', 'application/x-xls'); + ctx.set('Content-disposition', 'attachment; filename=' + encodeURI(fileName)); + ctx.body = fileData; + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + message: typeof error == 'string' ? error : undefined + } + } +} +async function getAchievementDetail(ctx) { + try { + const { models } = ctx.fs.dc; + const { keywordTarget, keyword, limit, page, toExport } = ctx.query + let where = {} + if (keywordTarget == 'saler' && keyword) { + where.sale = { $like: `%${keyword}%` } + } + if (keywordTarget == 'line' && keyword) { + where.serviceLine = { $like: `%${keyword}%` } + } + if (keywordTarget == 'dep' && keyword) { + where.department = { $like: `%${keyword}%` } + } + let findOption = { + where: where, + order: [['id', 'DESC']] + } + if (!toExport) {//非导出时考虑分页 + if (limit) { + findOption.limit = Number(limit) + } + if (page && limit) { + findOption.offset = Number(page) * Number(limit) + } + } + let res = await models.PerformanceDetail.findAndCountAll(findOption); + if (toExport) {//数据导出相关 + await exportAchievementDetail(ctx, res.rows); + } else { + ctx.status = 200 + ctx.body = { + count: res.count, + rows: res.rows + }; + } + } catch (error) { + ctx.fs.logger.error(`path:${ctx.path},error:${error}`) + ctx.status = 400; + ctx.body = { name: 'FindAllError', message: '获取失败' } + } +} + +async function exportAchievementDetail(ctx, dataList) { + try { + let header = [{ + title: '收到合同日期', + key: 'recConDate', + }, { + title: '月份', + key: 'month', + }, { + title: '部门', + key: 'department', + }, { + title: '销售人员', + key: 'sale', + }, { + title: '客户名称', + key: 'customer', + }, { + title: '项目名称', + key: 'item', + }, { + title: '合同金额', + key: 'amount', + }, { + title: '实际业绩', + key: 'realPerformance', + }, { + title: '考核业绩', + key: 'assessmentPerformance', + }, { + title: '价格是否特批', + key: 'isApproval', + }, { + title: '特批折算比例', + key: 'approvalProp', + }, { + title: '预支提成及委外费用', + key: 'cost', + }, { + title: '业务线', + key: 'serviceLine', + }, { + title: '客户类型', + key: 'cusType', + }, { + title: '行业', + key: 'industry', + }, { + title: '信息来源', + key: 'source', + }, { + title: '项目类型', + key: 'itemType', + }, { + title: '客户省份', + key: 'cusProvince', + }, { + title: '客户属性', + key: 'cusAttribute', + }, { + title: '复购次数', + key: 'repurchaseCount', + }, { + title: '是否可复制的业务路径', + key: 'reproducible', + }, { + title: '省外业务1.1', + key: 'outProvince', + }, { + title: '复购业务1.05', + key: 'repurchase', + }, { + title: '可复制的业务路径1.1', + key: 'isreproduce', + }] + const { utils: { simpleExcelDown } } = ctx.app.fs; + let exportData = [] + for (let { dataValues: item } of dataList) { + item.isApproval = item.isApproval ? '是' : '否'; + item.reproducible = item.reproducible ? '是' : '否'; + exportData.push(item) + } + const fileName = `业绩明细表_${moment().format('YYYYMMDDHHmmss')}` + '.xlsx' + const filePath = await simpleExcelDown({ data: exportData, header, fileName: fileName }) + const fileData = fs.readFileSync(filePath); + ctx.status = 200; + ctx.set('Content-Type', 'application/x-xls'); + ctx.set('Content-disposition', 'attachment; filename=' + encodeURI(fileName)); + ctx.body = fileData; + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + message: typeof error == 'string' ? error : undefined + } + } +} + +/** + * 查询合同明细表数据 + * @param {*} ctx ctx ctx.query:{keywordTarget-关键字项、keyword-关键字内容、limit-页宽, page-页码} + */ +async function getContractDetail(ctx) { + try { + const { models } = ctx.fs.dc; + const { keywordTarget, keyword, limit, page } = ctx.query; + const where = {}; + if (keywordTarget && keyword) { + where[keywordTarget] = { $iLike: `%${keyword}%` }; + } + let contractDetail = await models.ContractDetail.findAndCountAll({ + where: where, + offset: Number(page) * Number(limit), + limit: Number(limit), + order: [['id', 'DESC']] + }); + ctx.status = 200 + ctx.body = contractDetail; + } catch (error) { + ctx.fs.logger.error(`path:${ctx.path},error:${error}`) + ctx.status = 400; + ctx.body = { name: 'FindError', message: '查询合同明细表数据失败' } + } +} +module.exports = { + getReceivedDetail,//回款 + getAchievementDetail,//业绩 + getContractDetail, +} \ 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 930fd12..460b1cf 100644 --- a/api/app/lib/controllers/report/index.js +++ b/api/app/lib/controllers/report/index.js @@ -1,5 +1,6 @@ 'use strict'; - +const fs = require('fs'); +const moment = require('moment'); // 查询储备项目统计表 async function getReserveItemReport(ctx, next) { const { type } = ctx.params; @@ -27,6 +28,136 @@ async function getReserveItemReport(ctx, next) { } } +async function getSalersReport(ctx) { + try { + const { clickHouse } = ctx.app.fs + const { memberList, packageUserData } = ctx.app.fs.utils + const { + keywordTarget, keyword, limit, page, state, + hiredateStart, hiredateEnd, marital, native, workPlace, + orderBy, orderDirection, placeSearch, toExport + } = ctx.query + + const userRes = await memberList({ + keywordTarget, keyword, limit: '', page: '', state, + hiredateStart, hiredateEnd, marital, native, workPlace, + orderBy, orderDirection, + nowAttendanceTime: true + }) + + let { packageUser: members } = await packageUserData(userRes, { + state: true, + }) + let mIds = members.map(m => m.pepUserId); + + let innerSelectQuery = `where del=false and pep_user_id in [${mIds}]` + + `${placeSearch ? ` + and (sales.provinces LIKE '%${placeSearch}%' or sales.cities LIKE '%${placeSearch}%') + `: ''}` + + const salersRes = await clickHouse.hr.query(` + SELECT * from sales_distribution as sales + ${innerSelectQuery} + order by id desc + ${!toExport && limit ? `LIMIT ${limit}` : ''} + ${!toExport && limit && page ? 'OFFSET ' + parseInt(limit) * parseInt(page) : ''} + `).toPromise() + + const countRes = await clickHouse.hr.query(` + SELECT + count(sales.pep_user_id) AS count from sales_distribution as sales + ${innerSelectQuery} + `).toPromise() + let rslt = [] + salersRes.map(d => { + let info = members.find(m => m.pepUserId == d.pep_user_id); + let item = { + name: info.userName, + userCode: info.userCode, + post: info.userPost, + department: info.departmrnt, + hireDate: info.hiredate,//入职时间 + regularDate: info.regularDate,//转正时间 + businessLines: d.business_lines,//业务线 + ...d + } + rslt.push(item); + }) + if (toExport) { + await exportSalesDetail(ctx, rslt);//导出 + } else { + ctx.status = 200; + ctx.body = { + count: countRes.length ? countRes[0].count : 0, + rows: rslt + }; + } + } catch (error) { + ctx.fs.logger.error(`path:${ctx.path},error:${error}`) + ctx.status = 400; + ctx.body = { name: 'FindAllError', message: '获取销售人员分布明细表数据失败' } + } +} + +async function exportSalesDetail(ctx, dataList) { + try { + let header = [{ + title: "姓名", + key: 'name' + }, { + title: "部门名称", + key: 'department' + }, { + title: "销售区域(省/直辖市)", + key: 'provinces' + }, { + title: "销售区域(市)", + key: 'cities' + }, { + title: "业务线", + key: 'businessLines' + }, { + title: "岗位", + key: 'post' + }, { + title: "入职时间", + key: 'hireDate' + }, { + title: "转正时间", + key: 'regularDate' + }, { + title: "工龄", + key: 'workYears' + }]; + const { utils: { simpleExcelDown } } = ctx.app.fs; + let exportData = [] + for (let item of dataList) { + item.department = item.department.map(t => t.name).join('、') || '-'; + // item.cities = item.cities || '-'; + // item.businessLines = item.businessLines || '-'; + // item.post = item.post || '-'; + // item.regularDate = item.regularDate || '-'; + item.workYears = item.hireDate ? String(moment(new Date()).diff(item.hireDate, 'years', true)).substring(0, 3) + '年' : '-' + //item.hireDate = item.hireDate || '-'; + exportData.push(item) + } + const fileName = `销售人员分布明细表_${moment().format('YYYYMMDDHHmmss')}` + '.xlsx' + const filePath = await simpleExcelDown({ data: exportData, header, fileName: fileName }) + const fileData = fs.readFileSync(filePath); + ctx.status = 200; + ctx.set('Content-Type', 'application/x-xls'); + ctx.set('Content-disposition', 'attachment; filename=' + encodeURI(fileName)); + ctx.body = fileData; + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + message: typeof error == 'string' ? error : undefined + } + } +} + module.exports = { - getReserveItemReport + getReserveItemReport, + getSalersReport } \ No newline at end of file diff --git a/api/app/lib/models/contract_detail.js b/api/app/lib/models/contract_detail.js new file mode 100644 index 0000000..fbcb77a --- /dev/null +++ b/api/app/lib/models/contract_detail.js @@ -0,0 +1,430 @@ +/* eslint-disable*/ + +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const ContractDetail = sequelize.define("contractDetail", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: true, + field: "id", + autoIncrement: true + }, + year: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "年度", + primaryKey: false, + field: "year ", + autoIncrement: false + }, + serialNo: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + comment: "序号", + primaryKey: false, + field: "serial_no", + autoIncrement: false + }, + number: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "编号", + primaryKey: false, + field: "number", + autoIncrement: false + }, + introduction: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "简介", + primaryKey: false, + field: "introduction", + autoIncrement: false + }, + contractNo: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "合同编号", + primaryKey: false, + field: "contract_no", + autoIncrement: false + }, + applyDate: { + type: DataTypes.DATEONLY, + allowNull: true, + defaultValue: null, + comment: "申请日期", + primaryKey: false, + field: "apply_date", + autoIncrement: false + }, + recConDate: { + type: DataTypes.DATEONLY, + allowNull: true, + defaultValue: null, + comment: "收到合同日期", + primaryKey: false, + field: "rec_con_date", + autoIncrement: false + }, + contractPaper: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "合同纸质版情况", + primaryKey: false, + field: "contract_paper", + autoIncrement: false + }, + contractElec: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "合同电子版情况", + primaryKey: false, + field: "contract_elec", + autoIncrement: false + }, + department: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "部门", + primaryKey: false, + field: "department", + autoIncrement: false + }, + business: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "业务线", + primaryKey: false, + field: "business", + autoIncrement: false + }, + sale: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "销售人员", + primaryKey: false, + field: "sale", + autoIncrement: false + }, + customer: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "客户名称", + primaryKey: false, + field: "customer", + autoIncrement: false + }, + item: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "项目名称", + primaryKey: false, + field: "item", + autoIncrement: false + }, + itemType: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "项目类型", + primaryKey: false, + field: "item_type", + autoIncrement: false + }, + amount: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "合同金额", + primaryKey: false, + field: "amount", + autoIncrement: false + }, + changeAmount: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "变更后合同金额", + primaryKey: false, + field: "change_amount", + autoIncrement: false + }, + cInvoicedAmount: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "累计开票金额", + primaryKey: false, + field: "c_invoiced_amount", + autoIncrement: false + }, + cBackAmount: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "累计回款金额", + primaryKey: false, + field: "c_back_amount", + autoIncrement: false + }, + invoicedBack: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "开票-回款", + primaryKey: false, + field: "invoiced_back", + autoIncrement: false + }, + unInvoicedAmount: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "未开票金额", + primaryKey: false, + field: "un_invoiced_amount", + autoIncrement: false + }, + remainConAmount: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "剩余合同金额", + primaryKey: false, + field: "remain_con_amount", + autoIncrement: false + }, + backPercent: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "回款率", + primaryKey: false, + field: "back_percent", + autoIncrement: false + }, + retentionMoney: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "质保金", + primaryKey: false, + field: "retention_money", + autoIncrement: false + }, + retentionPercent: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "质保金比例", + primaryKey: false, + field: "retention_percent", + autoIncrement: false + }, + retentionTerm: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "质保期", + primaryKey: false, + field: "retention_term", + autoIncrement: false + }, + invoiceTax1: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "发票税率金额", + primaryKey: false, + field: "invoice_tax1", + autoIncrement: false + }, + invoiceTax2: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: null, + primaryKey: false, + field: "invoice_tax2", + autoIncrement: false + }, + invoiceTax3: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: null, + primaryKey: false, + field: "invoice_tax3", + autoIncrement: false + }, + payType: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "合同付款方式", + primaryKey: false, + field: "pay_type", + autoIncrement: false + }, + cost: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "预支提成及委外费用", + primaryKey: false, + field: "cost", + autoIncrement: false + }, + performanceRatio: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "业绩折算比例", + primaryKey: false, + field: "performance_ratio", + autoIncrement: false + }, + realPerformance: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "实际业绩", + primaryKey: false, + field: "real_performance", + autoIncrement: false + }, + assessmentPerformance: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "考核业绩", + primaryKey: false, + field: "assessment_performance", + autoIncrement: false + }, + acceptanceDate: { + type: DataTypes.DATEONLY, + allowNull: true, + defaultValue: null, + comment: "验收日期", + primaryKey: false, + field: "acceptance_date", + autoIncrement: false + }, + backConfirmDate: { + type: DataTypes.DATEONLY, + allowNull: true, + defaultValue: null, + comment: "收入确认时间", + primaryKey: false, + field: "back_confirm_date", + autoIncrement: false + }, + recYear: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "接单年份", + primaryKey: false, + field: "rec_year", + autoIncrement: false + }, + recMonth: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "接单月份", + primaryKey: false, + field: "rec_month", + autoIncrement: false + }, + cusAttribute: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "客户属性", + primaryKey: false, + field: "cus_attribute", + autoIncrement: false + }, + cusType: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "客户类型", + primaryKey: false, + field: "cus_type", + autoIncrement: false + }, + industry: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "行业", + primaryKey: false, + field: "industry", + autoIncrement: false + }, + source: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "信息来源", + primaryKey: false, + field: "source", + autoIncrement: false + }, + cusProvince: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "客户省份", + primaryKey: false, + field: "cus_province", + autoIncrement: false + }, + cusArea: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "项目所在地", + primaryKey: false, + field: "cus_area", + autoIncrement: false + }, + text: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: null, + primaryKey: false, + field: "text", + autoIncrement: false + } + }, { + tableName: "contract_detail", + comment: "", + indexes: [] + }); + dc.models.ContractDetail = ContractDetail; + return ContractDetail; +}; \ No newline at end of file diff --git a/api/app/lib/models/invoice_detail.js b/api/app/lib/models/invoice_detail.js new file mode 100644 index 0000000..cc79220 --- /dev/null +++ b/api/app/lib/models/invoice_detail.js @@ -0,0 +1,196 @@ +/* eslint-disable*/ + +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const InvoiceDetail = sequelize.define("invoiceDetail", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: true, + field: "id", + autoIncrement: false + }, + year: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "年度:【申请日期】-年份", + primaryKey: false, + field: "year", + autoIncrement: false + }, + serialNo: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: "序号:自动生成\n(自动升序),每一年自动从1开始", + primaryKey: false, + field: "serial_no", + autoIncrement: false + }, + number: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "编号:年份+序号 如2022年1", + primaryKey: false, + field: "number", + autoIncrement: false + }, + department: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "部门:申请部门", + primaryKey: false, + field: "department", + autoIncrement: false + }, + sale: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "销售人员:申请人", + primaryKey: false, + field: "sale", + autoIncrement: false + }, + contractNo: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "合同编号", + primaryKey: false, + field: "contract_no", + autoIncrement: false + }, + customer: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "客户名称:【甲方名称】", + primaryKey: false, + field: "customer", + autoIncrement: false + }, + item: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "项目名称", + primaryKey: false, + field: "item", + autoIncrement: false + }, + amount: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "合同金额:【合同金额\n(元)】", + primaryKey: false, + field: "amount", + autoIncrement: false + }, + changeAmount: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "变更后合同金额", + primaryKey: false, + field: "change_amount", + autoIncrement: false + }, + invoiceNo: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "发票号码:《发票申请单》【发票号】一个发票号码一个行数据", + primaryKey: false, + field: "invoice_no", + autoIncrement: false + }, + invoiceType: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "开票类型:《发票申请单》\n【发票类型】", + primaryKey: false, + field: "invoice_type", + autoIncrement: false + }, + invoiceDate: { + type: DataTypes.DATEONLY, + allowNull: false, + defaultValue: null, + comment: "开票日期:《发票申请单》\n【开票日期】", + primaryKey: false, + field: "invoice_date", + autoIncrement: false + }, + invoiceAmount: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "开票金额:《发票申请单》【发票金额】取财务填写数据", + primaryKey: false, + field: "invoice_amount", + autoIncrement: false + }, + outputTax: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "销项税额:《发票申请单》-销项税额", + primaryKey: false, + field: "output_tax", + autoIncrement: false + }, + total: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "合计:自动算出\n(开票金额+销项税额)", + primaryKey: false, + field: "total", + autoIncrement: false + }, + taxRate13: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "税率13%:《发票申请单》", + primaryKey: false, + field: "tax_rate13", + autoIncrement: false + }, + taxRate9: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "税率9%:《发票申请单》", + primaryKey: false, + field: "tax_rate9", + autoIncrement: false + }, + taxRate6: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "税率6%:《发票申请单》", + primaryKey: false, + field: "tax_rate6", + autoIncrement: false + } + }, { + tableName: "invoice_detail", + comment: "", + indexes: [] + }); + dc.models.InvoiceDetail = InvoiceDetail; + return InvoiceDetail; +}; \ No newline at end of file diff --git a/api/app/lib/models/performance_detail.js b/api/app/lib/models/performance_detail.js new file mode 100644 index 0000000..ff431ee --- /dev/null +++ b/api/app/lib/models/performance_detail.js @@ -0,0 +1,241 @@ +/* eslint-disable*/ + +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const PerformanceDetail = sequelize.define("performanceDetail", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: true, + field: "id", + autoIncrement: false + }, + recConDate: { + type: DataTypes.DATEONLY, + allowNull: false, + defaultValue: null, + comment: "收到合同日期", + primaryKey: false, + field: "rec_con_date", + autoIncrement: false + }, + month: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "月份", + primaryKey: false, + field: "month", + autoIncrement: false + }, + department: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "部门:申请部门", + primaryKey: false, + field: "department", + autoIncrement: false + }, + sale: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "销售人员", + primaryKey: false, + field: "sale", + autoIncrement: false + }, + customer: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "客户名称:【甲方名称】", + primaryKey: false, + field: "customer", + autoIncrement: false + }, + item: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "项目名称", + primaryKey: false, + field: "item", + autoIncrement: false + }, + amount: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "合同金额:【合同金额\n(元)】", + primaryKey: false, + field: "amount", + autoIncrement: false + }, + realPerformance: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "实际业绩;(合同金额-预\n支提成及委外费用)*特批折算比例\n(G-L)*K", + primaryKey: false, + field: "real_performance", + autoIncrement: false + }, + assessmentPerformance: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "合同金额*省外业务(1.1)*复购业务\n(1.05)*可复制的业务路径(1.1)", + primaryKey: false, + field: "assessment_performance", + autoIncrement: false + }, + isApproval: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: null, + comment: "价格是否特批:根据折算比例-《合同明细表》推\n算(100%——“是”;其他为否)", + primaryKey: false, + field: "isApproval", + autoIncrement: false + }, + approvalProp: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "特批折算比例:【业绩折\n算比例】", + primaryKey: false, + field: "approval_prop", + autoIncrement: false + }, + cost: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "预支提成及委外费用", + primaryKey: false, + field: "cost", + autoIncrement: false + }, + serviceLine: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "业务线", + primaryKey: false, + field: "service_line", + autoIncrement: false + }, + cusType: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "客户类型", + primaryKey: false, + field: "cus_type", + autoIncrement: false + }, + industry: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "行业", + primaryKey: false, + field: "industry", + autoIncrement: false + }, + source: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "信息来源", + primaryKey: false, + field: "source", + autoIncrement: false + }, + itemType: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "项目类型", + primaryKey: false, + field: "item_type", + autoIncrement: false + }, + cusProvince: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "客户省份", + primaryKey: false, + field: "cus_province", + autoIncrement: false + }, + cusAttribute: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "客户属性:G:政府、事业单位\nB:央企、国企、平台商\nC:资源方", + primaryKey: false, + field: "cus_attribute", + autoIncrement: false + }, + repurchaseCount: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: "复购次数:同一客户储备成单-成单项目数量\n(PM)", + primaryKey: false, + field: "repurchase_count", + autoIncrement: false + }, + reproducible: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: null, + comment: "是否可复制的业务路径:《合同评审》表单获取", + primaryKey: false, + field: "reproducible", + autoIncrement: false + }, + outProvince: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "省外业务1.1:当【客户省份】=“江西”时,省外业务=1;当【客户省份】<>“江西”时,省外业务=1.1", + primaryKey: false, + field: "out_province", + autoIncrement: false + }, + repurchase: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "复购业务1.05:当【复购次数】\n<2时,复购业务=1;当【复购次数】\n>=2时,复购业务=1.05;", + primaryKey: false, + field: "repurchase", + autoIncrement: false + }, + isreproduce: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "可复制的业务路径1.1:当【是否可复\n制的业务路径】=“是”,可复制的业务路径=1.1;当【是否可复制的业务路径】=“否”,可复制的业务路径=1;", + primaryKey: false, + field: "isreproduce", + autoIncrement: false + } + }, { + tableName: "performance_detail", + comment: "", + indexes: [] + }); + dc.models.PerformanceDetail = PerformanceDetail; + return PerformanceDetail; +}; \ No newline at end of file diff --git a/api/app/lib/models/receivable_detail.js b/api/app/lib/models/receivable_detail.js new file mode 100644 index 0000000..bd9faa0 --- /dev/null +++ b/api/app/lib/models/receivable_detail.js @@ -0,0 +1,187 @@ +/* eslint-disable*/ + +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const ReceivableDetail = sequelize.define("receivableDetail", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: true, + field: "id", + autoIncrement: false + }, + year: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "年度:【申请日期】-年份", + primaryKey: false, + field: "year", + autoIncrement: false + }, + serialNo: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: "序号:自动生成\n(自动升序),每一年自动从1开始", + primaryKey: false, + field: "serial_no", + autoIncrement: false + }, + number: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "编号:年份+序号 如2022年1", + primaryKey: false, + field: "number", + autoIncrement: false + }, + department: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "部门:申请部门", + primaryKey: false, + field: "department", + autoIncrement: false + }, + sale: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "销售人员:申请人", + primaryKey: false, + field: "sale", + autoIncrement: false + }, + contractNo: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "合同编号", + primaryKey: false, + field: "contract_no", + autoIncrement: false + }, + customer: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "客户名称:【甲方名称】", + primaryKey: false, + field: "customer", + autoIncrement: false + }, + item: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "项目名称", + primaryKey: false, + field: "item", + autoIncrement: false + }, + amount: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "合同金额:【合同金额\n(元)】", + primaryKey: false, + field: "amount", + autoIncrement: false + }, + changeAmount: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "变更后合同金额", + primaryKey: false, + field: "change_amount", + autoIncrement: false + }, + receivableYear: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "回款年份", + primaryKey: false, + field: "receivable_year", + autoIncrement: false + }, + receivableDate: { + type: DataTypes.DATEONLY, + allowNull: false, + defaultValue: null, + comment: "回款日期", + primaryKey: false, + field: "receivable_date", + autoIncrement: false + }, + receivableAmount: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "回款金额", + primaryKey: false, + field: "receivable_amount", + autoIncrement: false + }, + invoicedBack: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "开票-回款", + primaryKey: false, + field: "invoiced_back ", + autoIncrement: false + }, + remainConAmount: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "剩余合同金额", + primaryKey: false, + field: "remain_con_amount", + autoIncrement: false + }, + incomeConfirmdate: { + type: DataTypes.DATEONLY, + allowNull: false, + defaultValue: null, + comment: "收入确认时间", + primaryKey: false, + field: "income_confirmdate", + autoIncrement: false + }, + thirdPayment: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "第三方付款单位", + primaryKey: false, + field: "third_payment", + autoIncrement: false + }, + remark: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "备注:回款流程》-备注", + primaryKey: false, + field: "remark", + autoIncrement: false + } + }, { + tableName: "receivable_detail", + comment: "", + indexes: [] + }); + dc.models.ReceivableDetail = ReceivableDetail; + return ReceivableDetail; +}; \ 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 0d4e04f..b986cd6 100644 --- a/api/app/lib/routes/report/index.js +++ b/api/app/lib/routes/report/index.js @@ -1,8 +1,21 @@ 'use strict'; const report = require('../../controllers/report'); +const achieve = require('../../controllers/report/achievement'); module.exports = function (app, router, opts) { app.fs.api.logAttr['GET/reserveItem/report/:type'] = { content: '查询储备项目统计表', visible: false }; router.get('/reserveItem/report/:type', report.getReserveItemReport); + + app.fs.api.logAttr['GET/sales/member/list'] = { content: '查询销售人员分布明细表', visible: false }; + router.get('/sales/member/list', report.getSalersReport); + + app.fs.api.logAttr['GET/detail/received/back'] = { content: '查询回款明细表', visible: false }; + router.get('/detail/received/back', achieve.getReceivedDetail); + + app.fs.api.logAttr['GET/detail/achievement'] = { content: '查询业绩明细表', visible: false }; + router.get('/detail/achievement', achieve.getAchievementDetail); + + app.fs.api.logAttr['GET/contract/detail'] = { content: '查询合同明细表', visible: false }; + router.get('/contract/detail', achieve.getContractDetail); }; \ No newline at end of file diff --git a/api/app/lib/utils/constant.js b/api/app/lib/utils/constant.js index 8012eca..d1bf58e 100644 --- a/api/app/lib/utils/constant.js +++ b/api/app/lib/utils/constant.js @@ -2,19 +2,91 @@ module.exports = function (app, opts) { - const dayType = { - dayoff: '普假', - workday: '工作日', - festivals: '法定假', - } + const dayType = { + dayoff: '普假', + workday: '工作日', + festivals: '法定假', + } - const overtimeType = { - '发放加班补偿': '折算', - '调休': '调休' - } + const overtimeType = { + '发放加班补偿': '折算', + '调休': '调休' + } - return { - dayType, - overtimeType, - } + const contractDetailsColumnKeys = { + year: '年度', + serialNo: '序号', + number: '编号', + introduction: '简介', + contractNo: '合同编号', + applyDate: '申请日期', + recConDate: '收到合同日期', + contractPaper: '合同纸质版情况', + contractElec: '合同电子版情况', + department: '部门', + business: '业务线', + sale: '销售人员', + customer: '客户名称', + item: '项目名称', + itemType: '项目类型', + amount: '合同金额', + changeAmount: '变更后合同金额', + cInvoicedAmount: '累计开票金额', + cBackAmount: '累计回款金额', + invoicedBack: '开票-回款', + unInvoicedAmount: '未开票金额', + remainConAmount: '剩余合同金额', + backPercent: '回款率', + retentionMoney: '质保金', + retentionPercent: '质保金比例', + retentionTerm: '质保期', + invoiceTax1: '发票税率金额13%', + invoiceTax2: '发票税率金额9%', + invoiceTax3: '发票税率金额6%', + payType: '合同付款方式', + cost: '预支提成及委外费用', + performanceRatio: '业绩折算比例', + realPerformance: '实际业绩', + assessmentPerformance: '考核业绩', + acceptanceDate: '验收日期', + backConfirmDate: '收入确认时间', + recYear: '接单年份', + recMonth: '接单月份', + cusAttribute: '客户属性', + cusType: '客户类型', + industry: '行业', + source: '信息来源', + cusProvince: '客户省份', + cusArea: '项目所在地', + text: '备注', + } + + const invoicingDetailsColumnKeys = { + year: '年度', + serialNo: '序号', + number: '编号', + department: '部门', + sale: '销售人员', + contractNo: '合同编号', + customer: '客户名称', + item: '项目名称', + amount: '合同金额', + changeAmount: '变更后合同金额', + invoiceNo: '发票号码', + invoiceType: '开票类型', + invoiceDate: '开票日期', + invoiceAmount: '开票金额', + outputTax: '销项税额', + total: '合计', + taxRate13: '税率13%', + taxRate9: '税率9%', + taxRate6: '税率6%' + } + + return { + dayType, + overtimeType, + contractDetailsColumnKeys, + invoicingDetailsColumnKeys + } } \ No newline at end of file diff --git a/api/app/lib/utils/member.js b/api/app/lib/utils/member.js new file mode 100644 index 0000000..6088ef8 --- /dev/null +++ b/api/app/lib/utils/member.js @@ -0,0 +1,415 @@ +'use strict'; +const moment = require('moment') +const request = require('superagent'); + +module.exports = function (app, opts) { + + async function memberList({ + keywordTarget, keyword, limit, page, state, + hiredateStart, hiredateEnd, marital, native, workPlace, + orderBy, orderDirection, + nowAttendanceTime, + overtimeDayStatisticStartDate, overtimeDayStatisticendDate, + overtimeCountStatistic, overtimeCountStatisticStartDate, overtimeCountStatisticendDate, + vacateDayStatisticStartDate, vacateDayStatisticendDate, + vacateDurationStatistic, vacateCountStatistic, vacateCountStatisticStartDate, vacateCountStatisticendDate + }) { + //const { judgeHoliday } = app.fs.utils + const { clickHouse } = app.fs + const { database: pepEmis } = clickHouse.pepEmis.opts.config + + const curDay = moment().format('YYYY-MM-DD') + const nowTime = moment() + let whereOption = [] + let whereFromSelectOption = [] + let returnEmpty = false + if (state == 'inOffice') { + // 在岗 + //const holidayJudge = await judgeHoliday(curDay) + // if (holidayJudge) { + // if ( + // holidayJudge.workday + // && nowTime.isAfter(moment(curDay + ' 08:30')) + // && nowTime.isBefore(moment(curDay + ' 17:30')) + // ) { + // // 在工作日的工作时间范围 无请假记录 + // whereFromSelectOption.push(`vacateStartTime = '1970-01-01 00:00:00.000000'`) + // } else { + // returnEmpty = true + // } + // } else { + // returnEmpty = true + // } + } + if (state == 'dayoff') { + // 放假 + // const holidayJudge = await judgeHoliday(curDay) + // if (holidayJudge) { + // if ( + // holidayJudge.dayoff || holidayJudge.festivals + // ) { + // // 在休息日范围内且无加班申请 + // whereFromSelectOption.push(`overtimeStartTime = '1970-01-01 00:00:00.000000'`) + // } else { + // returnEmpty = true + // } + // } else { + // returnEmpty = true + // } + } + if (returnEmpty) { + return { + count: 0, + rows: [] + } + } + + let overtimeDayStatisticWhere = [] + // if (overtimeDayStatisticStartDate) { + // overtimeDayStatisticWhere.push(`overtime_day.day >= '${moment(overtimeDayStatisticStartDate).format('YYYY-MM-DD')}'`) + // } + // if (overtimeDayStatisticendDate) { + // overtimeDayStatisticWhere.push(`overtime_day.day <= '${moment(overtimeDayStatisticendDate).format('YYYY-MM-DD')}'`) + // } + + let overtimeCountStatisticWhere = [] + // if (overtimeCountStatisticStartDate) { + // overtimeCountStatisticWhere.push(`overtime.start_time >= '${moment(overtimeCountStatisticStartDate).startOf('day').format('YYYY-MM-DD HH:mm:ss')}'`) + // } + // if (overtimeCountStatisticendDate) { + // overtimeCountStatisticWhere.push(`overtime.end_time <= '${moment(overtimeCountStatisticendDate).endOf('day').format('YYYY-MM-DD HH:mm:ss')}'`) + // } + + let vacateDayStatisticWhere = [] + // if (vacateDayStatisticStartDate) { + // vacateDayStatisticWhere.push(`vacate_day.day >= '${moment(vacateDayStatisticStartDate).format('YYYY-MM-DD')}'`) + // } + // if (vacateDayStatisticendDate) { + // vacateDayStatisticWhere.push(`vacate_day.day <= '${moment(vacateDayStatisticendDate).format('YYYY-MM-DD')}'`) + // } + + let vacateCountStatisticWhere = [] + // if (vacateCountStatisticStartDate) { + // vacateCountStatisticWhere.push(`vacate.start_time >= '${moment(vacateCountStatisticStartDate).startOf('day').format('YYYY-MM-DD HH:mm:ss')}'`) + // } + // if (vacateCountStatisticendDate) { + // vacateCountStatisticWhere.push(`vacate.end_time <= '${moment(vacateCountStatisticendDate).endOf('day').format('YYYY-MM-DD HH:mm:ss')}'`) + // } + // CRAZY + const innerSelectQuery = ` + FROM member + INNER JOIN ${pepEmis}.user AS user + ON member.pep_user_id = user.id + ${keywordTarget == 'number' && keyword ? ` + AND user.people_code LIKE '%${keyword}%' + `: ''} + ${keywordTarget == 'name' && keyword ? ` + AND user.name LIKE '%${keyword}%' + `: ''} + + ${nowAttendanceTime ? ` + ${state == 'vacate' ? 'INNER' : 'LEFT'} JOIN ( + SELECT + pep_user_id, + any(start_time) AS vacateStartTime, + any(end_time) AS vacateEndTime + FROM vacate + WHERE + start_time <= '${nowTime.format('YYYY-MM-DD HH:mm:ss')}' + AND end_time > '${nowTime.format('YYYY-MM-DD HH:mm:ss')}' + GROUP BY pep_user_id + ) AS hrVacate + ON hrVacate.pep_user_id = member.pep_user_id + `: ''} + ${nowAttendanceTime ? ` + LEFT JOIN ( + SELECT + pep_user_id, + any(start_time) AS overtimeStartTime, + any(end_time) AS overtimeEndTime + FROM overtime + WHERE + start_time <= '${nowTime.format('YYYY-MM-DD HH:mm:ss')}' + AND end_time > '${nowTime.format('YYYY-MM-DD HH:mm:ss')}' + GROUP BY pep_user_id + ) AS hrOvertime + ON hrOvertime.pep_user_id = member.pep_user_id + `: ''} + + ${orderBy == 'overtimeTakeRestSum' || + orderBy == 'overtimePaySum' || + orderBy == 'overtimeSum' ? + `LEFT JOIN ( + SELECT + overtime.pep_user_id AS pepUserId, + sum(overtime_day.duration) AS duration + FROM overtime_day + INNER JOIN overtime + ON overtime.id = overtime_day.overtime_id + ${orderBy == 'overtimeTakeRestSum' ? `AND overtime.compensate = '调休'` : ''} + ${orderBy == 'overtimePaySum' ? `AND overtime.compensate = '发放加班补偿'` : ''} + ${overtimeDayStatisticWhere.length ? ` + WHERE ${overtimeDayStatisticWhere.join(' AND ')} + `: ''} + GROUP BY overtime.pep_user_id + ) AS overtimeDayStatistic + ON overtimeDayStatistic.pepUserId = member.pep_user_id`: ''} + + ${overtimeCountStatistic ? ` + LEFT JOIN ( + SELECT + overtime.pep_user_id AS pepUserId, + count(pep_process_story_id) AS count + FROM overtime + ${overtimeCountStatisticWhere.length ? ` + WHERE ${overtimeCountStatisticWhere.join(' AND ')} + `: ''} + GROUP BY overtime.pep_user_id + ) AS overtimeCountStatistic + ON overtimeCountStatistic.pepUserId = member.pep_user_id + `: ''} + + ${vacateDurationStatistic || + orderBy == 'vacateSum' ? + `LEFT JOIN ( + SELECT + vacate.pep_user_id AS pepUserId, + sum(vacate_day.duration) AS duration + FROM vacate_day + INNER JOIN vacate + ON vacate.id = vacate_day.vacate_id + ${vacateDayStatisticWhere.length ? ` + WHERE ${vacateDayStatisticWhere.join(' AND ')} + `: ''} + GROUP BY vacate.pep_user_id + ) AS vacateDayStatistic + ON vacateDayStatistic.pepUserId = member.pep_user_id`: ''} + + ${vacateCountStatistic ? ` + LEFT JOIN ( + SELECT + vacate.pep_user_id AS pepUserId, + count(pep_process_story_id) AS count + FROM vacate + ${vacateCountStatisticWhere.length ? ` + WHERE ${vacateCountStatisticWhere.join(' AND ')} + `: ''} + GROUP BY vacate.pep_user_id + ) AS vacateCountStatistic + ON vacateCountStatistic.pepUserId = member.pep_user_id + `: ''} + WHERE + member.del = '0' + ${keywordTarget == 'post' && keyword ? ` + AND user.post IN ( + SELECT basicDataPost.id + FROM ${pepEmis}.basicdata_post AS basicDataPost + where basicDataPost.name LIKE '%${keyword}%' + ) + ` : ''} + ${keywordTarget == 'dep' && keyword ? ` + AND user.id IN ( + SELECT department_user.user + FROM ${pepEmis}.department_user AS department_user + INNER JOIN ${pepEmis}.department AS department + ON department.id = department_user.department + AND department.name LIKE '%${keyword}%' + ) + ` : ''} + ${state == 'dimission' ? `AND member.dimission_date IS NOT null` : ''} + ${state == 'onJob' ? `AND member.dimission_date IS null` : ''} + ${whereFromSelectOption.length && nowAttendanceTime ? `AND ${whereFromSelectOption.join('AND')}` : ''} + ${hiredateStart ? ` + AND member.hiredate >= '${moment(hiredateStart).format('YYYY-MM-DD')}' + `: ''} + ${hiredateEnd ? ` + AND member.hiredate <= '${moment(hiredateEnd).format('YYYY-MM-DD')}' + ` : ''} + ${marital ? ` + AND member.marital = '${marital}' + `: ''} + ${native ? ` + AND member.native_place = '${native}' + `: ''} + ${workPlace ? ` + AND member.work_place = '${workPlace}' + `: ''} + ` + + const userRes = await clickHouse.hr.query(` + SELECT + hrMember."member.pep_user_id" AS pepUserId, + hrMember.*, + user.name AS userName, + user.people_code AS userCode, + basicDataPost.name AS userPost, + role.name AS roleName, + role.id AS roleId, + department.name AS depName, + department.id AS depId, + user.job AS userJob, + user.active_status AS userActiveStatus, + user.organization AS userOrganization + FROM ( + SELECT + ${orderBy == 'overtimeTakeRestSum' + || orderBy == 'overtimePaySum' + || orderBy == 'overtimeSum' ? ` + overtimeDayStatistic.duration AS overtimeDayStatisticDuration, + `: ''} + + ${overtimeCountStatistic ? ` + overtimeCountStatistic.count AS overtimeCount, + `: ''} + + ${orderBy == 'vacateSum' || vacateDurationStatistic ? ` + vacateDayStatistic.duration AS vacateDayStatisticDuration, + `: ''} + + ${vacateCountStatistic ? ` + vacateCountStatistic.count AS vacateCount, + `: ''} + + ${nowAttendanceTime ? ` + hrVacate.vacateStartTime AS vacateStartTime, + hrVacate.vacateEndTime AS vacateEndTime, + hrOvertime.overtimeStartTime AS overtimeStartTime, + hrOvertime.overtimeEndTime AS overtimeEndTime, + `: ''} + + member.* + ${innerSelectQuery} + ${limit ? `LIMIT ${limit}` : ''} + ${limit && page ? 'OFFSET ' + parseInt(limit) * parseInt(page) : ''} + ) AS hrMember + + LEFT JOIN ${pepEmis}.user AS user + ON pepUserId = user.id + LEFT JOIN ${pepEmis}.user_role AS user_role + ON ${pepEmis}.user_role.user = user.id + LEFT JOIN ${pepEmis}.role AS role + ON ${pepEmis}.role.id = user_role.role + LEFT JOIN ${pepEmis}.basicdata_post AS basicDataPost + ON ${pepEmis}.basicdata_post.id = user.post + LEFT JOIN ${pepEmis}.department_user AS department_user + ON department_user.user = user.id + LEFT JOIN ${pepEmis}.department AS department + ON department.id = department_user.department + ${whereOption.length ? `WHERE ${whereOption.join(' AND ')}` : ''} + ORDER BY ${orderBy == 'code' ? + 'user.people_code' + : orderBy == 'hiredate' + ? 'hrMember."member.hiredate"' + : orderBy == 'age' + ? 'hrMember."member.birthday"' + : orderBy == 'overtimeTakeRestSum' + || orderBy == 'overtimePaySum' + || orderBy == 'overtimeSum' ? + 'hrMember.overtimeDayStatisticDuration' + : orderBy == 'overtimeCount' ? + 'hrMember.overtimeCount' + : orderBy == 'vacateSum' ? + 'hrMember.vacateDayStatisticDuration' + : orderBy == 'vacateCount' ? + 'hrMember.vacateCount' + : 'user.people_code'} + ${orderDirection || 'ASC'} + `).toPromise() + + const countRes = await clickHouse.hr.query(` + SELECT + count(member.pep_user_id) AS count + ${innerSelectQuery} + `).toPromise() + + return { + count: countRes[0].count, + rows: userRes + } + } + + async function packageUserData(userRes, option = {}) { + + //const { judgeHoliday, } = app.fs.utils + let workTime = false + let dayoffTime = false + if (option.state) { + const curDay = moment().format('YYYY-MM-DD') + const nowTime = moment() + //const holidayJudge = await judgeHoliday(curDay) + // if (holidayJudge) { + // if ( + // holidayJudge.workday + // && nowTime.isAfter(moment(curDay + ' 08:30')) + // && nowTime.isBefore(moment(curDay + ' 17:30')) + // ) { + // workTime = true + // } else if (holidayJudge.dayoff || holidayJudge.festivals) { + // dayoffTime = true + // } + // } + } + + let returnD = [] + let pepUserIds = [-1] + userRes.rows.forEach(u => { + let existUser = returnD.find(r => r.pepUserId == u.pepUserId) + if (existUser) { + if (u.depId && !existUser.departmrnt.some(d => d.id == u.depId)) { + existUser.departmrnt.push({ + id: u.depId, + name: u.depName + }) + } + if (u.roleId && !existUser.role.some(r => r.id == u.roleId)) { + existUser.role.push({ + id: u.roleId, + name: u.roleName + }) + } + } else { + let obj = {} + for (let k in u) { + let nextKey = k.replace('hrMember.', '') + .replace('member.', '') + if (nextKey.includes('_')) { + nextKey = nextKey.toLowerCase() + .replace( + /(_)[a-z]/g, + (L) => L.toUpperCase() + ) + .replace(/_/g, '') + } + obj[nextKey] = u[k] == '1970-01-01 00:00:00.000000' || u[k] == '1970-01-01 08:00:00.000000' ? null : u[k] + } + pepUserIds.push(u.pepUserId) + console.log("查询到的用户信息:", obj); + returnD.push({ + ...obj, + departmrnt: u.depId ? [{ + id: u.depId, + name: u.depName + }] : [], + role: u.roleId ? [{ + id: u.roleId, + name: u.roleName + }] : [], + state: option.state ? + obj['dimissionDate'] ? 'dimission' : + obj['vacateStartTime'] ? 'vacate' : + workTime ? 'inOffice' : + dayoffTime ? 'dayoff' : 'rest' + : undefined, + del: undefined, + pepuserid: undefined + }) + } + }) + return { packageUser: returnD, pepUserIds } + } + + return { + memberList, + packageUserData + } +} \ No newline at end of file diff --git a/api/app/lib/utils/xlsxDownload.js b/api/app/lib/utils/xlsxDownload.js index bcd060a..1aa03e7 100644 --- a/api/app/lib/utils/xlsxDownload.js +++ b/api/app/lib/utils/xlsxDownload.js @@ -7,76 +7,76 @@ const moment = require('moment') module.exports = function (app, opts) { - //递归创建目录 同步方法 - async function makeDir (dir) { - if (!fs.existsSync(dir)) { - makeDir(path.dirname(dir)) - fs.mkdirSync(dir, function (err) { - if (err) { - throw err - } - }); - } - } + //递归创建目录 同步方法 + async function makeDir(dir) { + if (!fs.existsSync(dir)) { + makeDir(path.dirname(dir)) + fs.mkdirSync(dir, function (err) { + if (err) { + throw err + } + }); + } + } - async function simpleExcelDown ({ data = [], header = [], fileName = moment().format('YYYY-MM-DD HH:mm:ss') } = {}) { - const fileDirPath = path.join(__dirname, `../../downloadFiles`) - makeDir(fileDirPath) - const file = new xlsx.File(); - const sheet_1 = file.addSheet('sheet_1'); + async function simpleExcelDown({ data = [], header = [], fileName = moment().format('YYYY-MM-DD HH:mm:ss') } = {}) { + const fileDirPath = path.join(__dirname, `../../downloadFiles`) + makeDir(fileDirPath) + const file = new xlsx.File(); + const sheet_1 = file.addSheet('sheet_1'); - // header - const headerStyle = new xlsx.Style(); - headerStyle.align.h = 'center'; - headerStyle.align.v = 'center'; - headerStyle.border.right = 'thin'; - headerStyle.border.rightColor = '#000000'; - headerStyle.border.bottom = 'thin'; - headerStyle.border.bottomColor = '#000000'; + // header + const headerStyle = new xlsx.Style(); + headerStyle.align.h = 'center'; + headerStyle.align.v = 'center'; + headerStyle.border.right = 'thin'; + headerStyle.border.rightColor = '#000000'; + headerStyle.border.bottom = 'thin'; + headerStyle.border.bottomColor = '#000000'; - const headerRow = sheet_1.addRow(); - const indexCell = headerRow.addCell(); - indexCell.value = '序号' - indexCell.style = headerStyle - for (let h of header) { - const cell = headerRow.addCell(); - cell.value = h.title; - cell.style = headerStyle - } + const headerRow = sheet_1.addRow(); + const indexCell = headerRow.addCell(); + indexCell.value = '序号' + indexCell.style = headerStyle + for (let h of header) { + const cell = headerRow.addCell(); + cell.value = h.title; + cell.style = headerStyle + } - // data - const style = new xlsx.Style(); - style.align.h = 'left'; - style.align.v = 'center'; - style.border.right = 'thin'; - style.border.rightColor = '#000000'; - style.border.bottom = 'thin'; - style.border.bottomColor = '#000000'; - for (let i = 0; i < data.length; i++) { - const row = sheet_1.addRow(); - const indexCell = row.addCell(); - indexCell.value = i + 1 - indexCell.style = headerStyle - for (let h of header) { - const cell = row.addCell(); - cell.value = data[i][h.key] || h.defaultValue || ''; - cell.style = style - } - } + // data + const style = new xlsx.Style(); + style.align.h = 'left'; + style.align.v = 'center'; + style.border.right = 'thin'; + style.border.rightColor = '#000000'; + style.border.bottom = 'thin'; + style.border.bottomColor = '#000000'; + for (let i = 0; i < data.length; i++) { + const row = sheet_1.addRow(); + const indexCell = row.addCell(); + indexCell.value = i + 1 + indexCell.style = headerStyle + for (let h of header) { + const cell = row.addCell(); + cell.value = data[i][h.key] || h.defaultValue || '-'; + cell.style = style + } + } - const savePath = path.join(fileDirPath, fileName) - await new Promise(function (resolve, reject) { - file.saveAs() - .pipe(fs.createWriteStream(savePath)) - .on('finish', () => { - resolve() - }); - }) - return savePath - } + const savePath = path.join(fileDirPath, fileName) + await new Promise(function (resolve, reject) { + file.saveAs() + .pipe(fs.createWriteStream(savePath)) + .on('finish', () => { + resolve() + }); + }) + return savePath + } - return { - simpleExcelDown, - makeDir - } + return { + simpleExcelDown, + makeDir + } } \ No newline at end of file diff --git a/api/config.js b/api/config.js index c20c7b2..d4891d1 100644 --- a/api/config.js +++ b/api/config.js @@ -27,6 +27,7 @@ args.option('qndmn', 'qiniuDomain'); args.option('clickHouseUrl', 'clickHouse Url'); args.option('clickHousePort', 'clickHouse Port'); args.option('clickHousePepEmis', 'clickHouse 项企数据库名称'); +args.option('clickHouseHr', 'clickHouse 人资数据库名称'); const flags = args.parse(process.argv); @@ -52,6 +53,7 @@ const CLICKHOUST_PORT = process.env.CLICKHOUST_PORT || flags.clickHousePort const CLICKHOUST_USER = process.env.CLICKHOUST_USER || flags.clickHouseUser const CLICKHOUST_PASSWORD = process.env.CLICKHOUST_PASSWORD || flags.clickHousePassword const CLICKHOUST_PEP_EMIS = process.env.CLICKHOUST_PEP_EMIS || flags.clickHousePepEmis +const CLICKHOUST_HR = process.env.CLICKHOUST_HR || flags.clickHouseHr if ( !RC_DB @@ -59,7 +61,7 @@ if ( || !API_EMIS_URL || !QINIU_DOMAIN_QNDMN_RESOURCE || !QINIU_BUCKET_RESOURCE || !QINIU_AK || !QINIU_SK || !CLICKHOUST_URL || !CLICKHOUST_PORT - || !CLICKHOUST_PEP_EMIS + || !CLICKHOUST_PEP_EMIS || !CLICKHOUST_HR ) { console.log('缺少启动参数,异常退出'); args.showHelp(); @@ -117,6 +119,10 @@ const product = { { name: 'pepEmis', db: CLICKHOUST_PEP_EMIS + }, + { + name: 'hr', + db: CLICKHOUST_HR } ] } diff --git a/api/package.json b/api/package.json index 5ed11ec..628ca3c 100644 --- a/api/package.json +++ b/api/package.json @@ -33,6 +33,7 @@ "pg": "^7.9.0", "redis": "^3.1.2", "request": "^2.88.2", + "sequelize-automate-freesun": "^1.2.2", "superagent": "^3.5.2", "uuid": "^3.3.2" }, diff --git a/api/sequelize-automate.config.js b/api/sequelize-automate.config.js index 6a31e50..a8e72a9 100644 --- a/api/sequelize-automate.config.js +++ b/api/sequelize-automate.config.js @@ -1,35 +1,35 @@ module.exports = { - // 数据库配置 与 sequelize 相同 - dbOptions: { - database: 'hr-dev', - username: 'postgres', - password: '123', - dialect: 'postgres', - host: '10.8.30.166', - port: 5432, - define: { - underscored: false, - freezeTableName: false, - charset: 'utf8mb4', - timezone: '+00: 00', - dialectOptions: { - collate: 'utf8_general_ci', - }, - timestamps: false, - }, - }, - options: { - type: 'freesun', // 指定 models 代码风格 - camelCase: true, // Models 文件中代码是否使用驼峰命名 - modalNameSuffix: false, // 模型名称是否带 ‘Model’ 后缀 - fileNameCamelCase: false, // Model 文件名是否使用驼峰法命名,默认文件名会使用表名,如 `user_post.js`;如果为 true,则文件名为 `userPost.js` - dir: './app/lib/models', // 指定输出 models 文件的目录 - typesDir: 'models', // 指定输出 TypeScript 类型定义的文件目录,只有 TypeScript / Midway 等会有类型定义 - emptyDir: false, // !!! 谨慎操作 生成 models 之前是否清空 `dir` 以及 `typesDir` - tables: ['overtime_day','vacate_day'], // 指定生成哪些表的 models,如 ['user', 'user_post'];如果为 null,则忽略改属性 - skipTables: [], // 指定跳过哪些表的 models,如 ['user'];如果为 null,则忽略改属性 - tsNoCheck: false, // 是否添加 `@ts-nocheck` 注释到 models 文件中 - ignorePrefix: [], // 生成的模型名称忽略的前缀,因为 项目中有以下表名是以 t_ 开头的,在实际模型中不需要, 可以添加多个 [ 't_data_', 't_',] ,长度较长的 前缀放前面 - attrLength: false, // 在生成模型的字段中 是否生成 如 var(128)这种格式,公司一般使用 String ,则配置为 false - }, + // 数据库配置 与 sequelize 相同 + dbOptions: { + database: 'data_center', + username: 'FashionAdmin', + password: '123456', + dialect: 'postgres', + host: '10.8.30.36', + port: 5432, + define: { + underscored: false, + freezeTableName: false, + charset: 'utf8mb4', + timezone: '+00: 00', + dialectOptions: { + collate: 'utf8_general_ci', + }, + timestamps: false, + }, + }, + options: { + type: 'freesun', // 指定 models 代码风格 + camelCase: true, // Models 文件中代码是否使用驼峰命名 + modalNameSuffix: false, // 模型名称是否带 ‘Model’ 后缀 + fileNameCamelCase: false, // Model 文件名是否使用驼峰法命名,默认文件名会使用表名,如 `user_post.js`;如果为 true,则文件名为 `userPost.js` + dir: './app/lib/models', // 指定输出 models 文件的目录 + typesDir: 'models', // 指定输出 TypeScript 类型定义的文件目录,只有 TypeScript / Midway 等会有类型定义 + emptyDir: false, // !!! 谨慎操作 生成 models 之前是否清空 `dir` 以及 `typesDir` + tables: ['contract_detail', 'invoice_detail', 'receivable_detail'], // 指定生成哪些表的 models,如 ['user', 'user_post'];如果为 null,则忽略改属性 + skipTables: [], // 指定跳过哪些表的 models,如 ['user'];如果为 null,则忽略改属性 + tsNoCheck: false, // 是否添加 `@ts-nocheck` 注释到 models 文件中 + ignorePrefix: [], // 生成的模型名称忽略的前缀,因为 项目中有以下表名是以 t_ 开头的,在实际模型中不需要, 可以添加多个 [ 't_data_', 't_',] ,长度较长的 前缀放前面 + attrLength: false, // 在生成模型的字段中 是否生成 如 var(128)这种格式,公司一般使用 String ,则配置为 false + } } \ No newline at end of file diff --git a/web/client/index.ejs b/web/client/index.ejs index b79c2a9..822647c 100644 --- a/web/client/index.ejs +++ b/web/client/index.ejs @@ -9,7 +9,7 @@ - + diff --git a/web/client/index.html b/web/client/index.html index ff55403..8a03f94 100644 --- a/web/client/index.html +++ b/web/client/index.html @@ -7,7 +7,7 @@ - +
diff --git a/web/client/src/components/setup.jsx b/web/client/src/components/setup.jsx index 41d8792..ce0a719 100644 --- a/web/client/src/components/setup.jsx +++ b/web/client/src/components/setup.jsx @@ -5,12 +5,13 @@ import { Checkbox, } from "@douyinfe/semi-ui"; -function Setup (props) { +function Setup(props) { const { close, tableType, tableList, - length + length, + objWidth } = props; const [check, setCheck] = useState([]); @@ -22,7 +23,7 @@ function Setup (props) { setCheck(checkItem ? JSON.parse(checkItem) : []) ischeck(); }, []); - function ischeck (value) { + function ischeck(value) { if (check.length >= length) { if (check.includes(value)) { return false; @@ -57,7 +58,7 @@ function Setup (props) { } visible={true} - style={{ width: 600 }} + style={{ width: objWidth?.modalWidth || 600 }} onOk={() => { localStorage.setItem(tableType, JSON.stringify(check)); close(); @@ -82,7 +83,7 @@ function Setup (props) {