From 97c1f52f279f640ebc09d071399832a46246dd8b Mon Sep 17 00:00:00 2001 From: "gao.zhiyuan" Date: Tue, 18 Oct 2022 21:06:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E7=8F=AD=E6=95=B0=E6=8D=AE=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/app/lib/controllers/attendance/index.js | 132 +++++++ api/app/lib/controllers/member/index.js | 28 +- api/app/lib/models/member.js | 418 ++++++++++---------- api/app/lib/routes/attendance/index.js | 8 + api/app/lib/utils/member.js | 52 ++- 5 files changed, 409 insertions(+), 229 deletions(-) create mode 100644 api/app/lib/controllers/attendance/index.js create mode 100644 api/app/lib/routes/attendance/index.js diff --git a/api/app/lib/controllers/attendance/index.js b/api/app/lib/controllers/attendance/index.js new file mode 100644 index 0000000..c60cc48 --- /dev/null +++ b/api/app/lib/controllers/attendance/index.js @@ -0,0 +1,132 @@ +'use strict'; + +async function overtimeStatistic (ctx) { + try { + const { models } = ctx.fs.dc; + const { clickHouse } = ctx.app.fs + const { judgeHoliday, memberList } = ctx.app.fs.utils + const { + keywordTarget, keyword, limit, page, orderBy, orderDirection, + startDate, endDate, + } = ctx.query + + const userRes = await memberList({ + keywordTarget, keyword, limit, page, + orderBy, orderDirection, + overtimeDayStatisticStartDate: startDate, + overtimeDayStatisticendDate: endDate + }) + + let returnD = [] + let pepUserIds = [] + 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] + } + pepUserIds.push(u.pepUserId) + returnD.push({ + ...obj, + departmrnt: u.depId ? [{ + id: u.depId, + name: u.depName + }] : [], + role: u.roleId ? [{ + id: u.roleId, + name: u.roleName + }] : [], + del: undefined, + pepuserid: undefined, + "vacateStartTime": undefined, + "vacateEndTime": undefined, + "overtimeStartTime": undefined, + "overtimeEndTime": undefined, + }) + } + }) + + // 查加班总数 + const countRes = await clickHouse.hr.query(` + SELECT + overtime.pep_user_id AS pepUserId, + count(pep_process_story_id) AS count + FROM overtime + WHERE overtime.pep_user_id IN (${pepUserIds.join(',')}) + ${startDate ? ` + AND overtime.start_time >= '${moment(startDate).format('YYYY-MM-DD')}' + `: ''} + ${endDate ? ` + AND overtime.start_time <= '${moment(endDate).format('YYYY-MM-DD')}' + ` : ''} + GROUP BY overtime.pep_user_id + `).toPromise() + + const sumRes = await clickHouse.hr.query(` + SELECT + overtime.pep_user_id AS pepUserId, + holiday.type AS dayType, + overtime.compensate AS compensate, + sum(overtime_day.duration) AS duration + FROM overtime_day + INNER JOIN overtime + ON overtime.id = overtime_day.overtime_id + AND overtime.pep_user_id IN (${pepUserIds.join(',')}) + LEFT JOIN holiday + ON holiday.day = overtime_day.day + WHERE overtime.pep_user_id IN (${pepUserIds.join(',')}) + ${startDate ? ` + AND overtime_day.day >= '${moment(startDate).format('YYYY-MM-DD')}' + `: ''} + ${endDate ? ` + AND overtime_day.day <= '${moment(endDate).format('YYYY-MM-DD')}' + ` : ''} + GROUP BY holiday.type, overtime.compensate, overtime.pep_user_id + `).toPromise() + + returnD.forEach(u => { + u.overtimeCount = (countRes.find(c => c.pepUserId == u.pepUserId) || {}).count || 0 + + u.overtimeStatistic = sumRes.filter(s => s.pepUserId == u.pepUserId) + }) + ctx.status = 200; + ctx.body = { + count: userRes.count, + rows: returnD + } + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + message: typeof error == 'string' ? error : undefined + } + } +} + +module.exports = { + overtimeStatistic, +}; \ No newline at end of file diff --git a/api/app/lib/controllers/member/index.js b/api/app/lib/controllers/member/index.js index de5d31f..f40fd47 100644 --- a/api/app/lib/controllers/member/index.js +++ b/api/app/lib/controllers/member/index.js @@ -37,7 +37,7 @@ async function add (ctx) { ctx.status = 204; } catch (error) { - ctx.fs.logger.error(`path: ${ctx.path}, error: error`); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : undefined @@ -77,7 +77,7 @@ async function edit (ctx) { ctx.status = 204; } catch (error) { - ctx.fs.logger.error(`path: ${ctx.path}, error: error`); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : undefined @@ -161,7 +161,7 @@ async function searchPepMember (ctx) { ctx.status = 200; ctx.body = returnD } catch (error) { - ctx.fs.logger.error(`path: ${ctx.path}, error: error`); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : undefined @@ -184,7 +184,7 @@ async function del (ctx) { ctx.status = 204; } catch (error) { - ctx.fs.logger.error(`path: ${ctx.path}, error: error`); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : undefined @@ -208,7 +208,7 @@ async function nativePlaceList (ctx) { ctx.status = 200; ctx.body = nRes } catch (error) { - ctx.fs.logger.error(`path: ${ctx.path}, error: error`); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : undefined @@ -232,7 +232,7 @@ async function workPlaceList (ctx) { ctx.status = 200; ctx.body = wRes } catch (error) { - ctx.fs.logger.error(`path: ${ctx.path}, error: error`); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : undefined @@ -256,7 +256,7 @@ async function maritalList (ctx) { ctx.status = 200; ctx.body = mRes } catch (error) { - ctx.fs.logger.error(`path: ${ctx.path}, error: error`); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : undefined @@ -270,7 +270,8 @@ async function list (ctx) { const { judgeHoliday, memberList } = ctx.app.fs.utils const { keywordTarget, keyword, limit, page, state, - hiredateStart, hiredateEnd, marital, native, workPlace + hiredateStart, hiredateEnd, marital, native, workPlace, + orderBy, orderDirection, } = ctx.query const curDay = moment().format('YYYY-MM-DD') @@ -291,7 +292,8 @@ async function list (ctx) { } const userRes = await memberList({ keywordTarget, keyword, limit, page, state, - hiredateStart, hiredateEnd, marital, native, workPlace + hiredateStart, hiredateEnd, marital, native, workPlace, + orderBy, orderDirection, }) let returnD = [] @@ -351,7 +353,7 @@ async function list (ctx) { rows: returnD } } catch (error) { - ctx.fs.logger.error(`path: ${ctx.path}, error: error`); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : undefined @@ -440,7 +442,7 @@ async function overTimeStatistics (ctx) { ctx.status = 200; ctx.body = returnD } catch (error) { - ctx.fs.logger.error(`path: ${ctx.path}, error: error`); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : undefined @@ -520,7 +522,7 @@ async function vacateStatistics (ctx) { ctx.status = 200; ctx.body = returnD } catch (error) { - ctx.fs.logger.error(`path: ${ctx.path}, error: error`); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : undefined @@ -668,7 +670,7 @@ async function exportData (ctx) { ctx.set('Content-disposition', 'attachment; filename=' + encodeURI(fileName)); ctx.body = fileData; } catch (error) { - ctx.fs.logger.error(`path: ${ctx.path}, error: error`); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.status = 400; ctx.body = { message: typeof error == 'string' ? error : undefined diff --git a/api/app/lib/models/member.js b/api/app/lib/models/member.js index 3171167..910bad5 100644 --- a/api/app/lib/models/member.js +++ b/api/app/lib/models/member.js @@ -2,213 +2,213 @@ 'use strict'; module.exports = dc => { - const DataTypes = dc.ORM; - const sequelize = dc.orm; - const Member = sequelize.define("member", { - pepUserId: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: null, - comment: "项企用户id", - primaryKey: true, - field: "pep_user_id", - autoIncrement: false, - unique: "member_pep_user_id_uindex" - }, - idNumber: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - comment: "证件号", - primaryKey: false, - field: "id_number", - autoIncrement: false - }, - idPhoto: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - comment: "证件照", - primaryKey: false, - field: "id_photo", - autoIncrement: false - }, - gender: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - comment: "性别", - primaryKey: false, - field: "gender", - autoIncrement: false - }, - birthday: { - type: DataTypes.DATEONLY, - allowNull: true, - defaultValue: null, - comment: "出生年月", - primaryKey: false, - field: "birthday", - autoIncrement: false - }, - nativePlace: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - comment: "籍贯", - primaryKey: false, - field: "native_place", - autoIncrement: false - }, - marital: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - comment: "婚育状态", - primaryKey: false, - field: "marital", - autoIncrement: false - }, - politicsStatus: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - comment: "整治面貌", - primaryKey: false, - field: "politics_status", - autoIncrement: false - }, - phoneNumber: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - comment: null, - primaryKey: false, - field: "phone_number", - autoIncrement: false - }, - workPlace: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - comment: "工作地点", - primaryKey: false, - field: "work_place", - autoIncrement: false - }, - graduatedFrom: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - comment: "毕业院校", - primaryKey: false, - field: "graduated_from", - autoIncrement: false - }, - educationBackground: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - comment: "学历", - primaryKey: false, - field: "education_background", - autoIncrement: false - }, - specialty: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - comment: "专业", - primaryKey: false, - field: "specialty", - autoIncrement: false - }, - graduationDate: { - type: DataTypes.DATEONLY, - allowNull: true, - defaultValue: null, - comment: "毕业时间", - primaryKey: false, - field: "graduation_date", - autoIncrement: false - }, - hiredate: { - type: DataTypes.DATEONLY, - allowNull: true, - defaultValue: null, - comment: "入职时间", - primaryKey: false, - field: "hiredate", - autoIncrement: false - }, - turnProbationPeriod: { - type: DataTypes.DATEONLY, - allowNull: true, - defaultValue: null, - comment: "转试用期时间", - primaryKey: false, - field: "turn_probation_period", - autoIncrement: false - }, - regularDate: { - type: DataTypes.DATEONLY, - allowNull: true, - defaultValue: null, - comment: "转正时间", - primaryKey: false, - field: "regular_date", - autoIncrement: false - }, - dimissionDate: { - type: DataTypes.DATEONLY, - allowNull: true, - defaultValue: null, - comment: "离职时间", - primaryKey: false, - field: "dimission_date", - autoIncrement: false - }, - experienceYear: { - type: DataTypes.DOUBLE, - allowNull: true, - defaultValue: null, - comment: "工作经验/年", - primaryKey: false, - field: "experience_year", - autoIncrement: false - }, - occupationalHistory: { - type: DataTypes.TEXT, - allowNull: true, - defaultValue: null, - comment: "工作经历", - primaryKey: false, - field: "occupational_history", - autoIncrement: false - }, - vitae: { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - comment: "简历", - primaryKey: false, - field: "vitae", - autoIncrement: false - }, - del: { - type: DataTypes.BOOLEAN, - allowNull: true, - defaultValue: null, - comment: null, - primaryKey: false, - field: "del", - autoIncrement: false - } - }, { - tableName: "member", - comment: "", - indexes: [] - }); - dc.models.Member = Member; - return Member; + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const Member = sequelize.define("member", { + pepUserId: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: "项企用户id", + primaryKey: true, + field: "pep_user_id", + autoIncrement: false, + unique: "member_pep_user_id_uindex" + }, + idNumber: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "证件号", + primaryKey: false, + field: "id_number", + autoIncrement: false + }, + idPhoto: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "证件照", + primaryKey: false, + field: "id_photo", + autoIncrement: false + }, + gender: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "性别", + primaryKey: false, + field: "gender", + autoIncrement: false + }, + birthday: { + type: DataTypes.DATEONLY, + allowNull: true, + defaultValue: null, + comment: "出生年月", + primaryKey: false, + field: "birthday", + autoIncrement: false + }, + nativePlace: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "籍贯", + primaryKey: false, + field: "native_place", + autoIncrement: false + }, + marital: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "婚育状态", + primaryKey: false, + field: "marital", + autoIncrement: false + }, + politicsStatus: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "整治面貌", + primaryKey: false, + field: "politics_status", + autoIncrement: false + }, + phoneNumber: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: '联系方式', + primaryKey: false, + field: "phone_number", + autoIncrement: false + }, + workPlace: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "工作地点", + primaryKey: false, + field: "work_place", + autoIncrement: false + }, + graduatedFrom: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "毕业院校", + primaryKey: false, + field: "graduated_from", + autoIncrement: false + }, + educationBackground: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "学历", + primaryKey: false, + field: "education_background", + autoIncrement: false + }, + specialty: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "专业", + primaryKey: false, + field: "specialty", + autoIncrement: false + }, + graduationDate: { + type: DataTypes.DATEONLY, + allowNull: true, + defaultValue: null, + comment: "毕业时间", + primaryKey: false, + field: "graduation_date", + autoIncrement: false + }, + hiredate: { + type: DataTypes.DATEONLY, + allowNull: true, + defaultValue: null, + comment: "入职时间", + primaryKey: false, + field: "hiredate", + autoIncrement: false + }, + turnProbationPeriod: { + type: DataTypes.DATEONLY, + allowNull: true, + defaultValue: null, + comment: "转试用期时间", + primaryKey: false, + field: "turn_probation_period", + autoIncrement: false + }, + regularDate: { + type: DataTypes.DATEONLY, + allowNull: true, + defaultValue: null, + comment: "转正时间", + primaryKey: false, + field: "regular_date", + autoIncrement: false + }, + dimissionDate: { + type: DataTypes.DATEONLY, + allowNull: true, + defaultValue: null, + comment: "离职时间", + primaryKey: false, + field: "dimission_date", + autoIncrement: false + }, + experienceYear: { + type: DataTypes.DOUBLE, + allowNull: true, + defaultValue: null, + comment: "工作经验/年", + primaryKey: false, + field: "experience_year", + autoIncrement: false + }, + occupationalHistory: { + type: DataTypes.TEXT, + allowNull: true, + defaultValue: null, + comment: "工作经历", + primaryKey: false, + field: "occupational_history", + autoIncrement: false + }, + vitae: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "简历", + primaryKey: false, + field: "vitae", + autoIncrement: false + }, + del: { + type: DataTypes.BOOLEAN, + allowNull: true, + defaultValue: null, + comment: null, + primaryKey: false, + field: "del", + autoIncrement: false + } + }, { + tableName: "member", + comment: "", + indexes: [] + }); + dc.models.Member = Member; + return Member; }; \ No newline at end of file diff --git a/api/app/lib/routes/attendance/index.js b/api/app/lib/routes/attendance/index.js new file mode 100644 index 0000000..24c4e6a --- /dev/null +++ b/api/app/lib/routes/attendance/index.js @@ -0,0 +1,8 @@ +'use strict'; + +const attendance = require('../../controllers/attendance'); + +module.exports = function (app, router, opts) { + app.fs.api.logAttr['GET/attendance/overtime'] = { content: '加班统计', visible: true }; + router.get('/attendance/overtime', attendance.overtimeStatistic); +}; \ No newline at end of file diff --git a/api/app/lib/utils/member.js b/api/app/lib/utils/member.js index 85d353d..d68021d 100644 --- a/api/app/lib/utils/member.js +++ b/api/app/lib/utils/member.js @@ -6,7 +6,9 @@ module.exports = function (app, opts) { async function memberList ({ keywordTarget, keyword, limit, page, state, - hiredateStart, hiredateEnd, marital, native, workPlace + hiredateStart, hiredateEnd, marital, native, workPlace, + orderBy, orderDirection, + overtimeDayStatisticStartDate, overtimeDayStatisticendDate }) { const { judgeHoliday } = app.fs.utils const { clickHouse } = app.fs @@ -58,19 +60,27 @@ module.exports = function (app, opts) { } } + let overtimeDayStatisticWhere = [] + if (overtimeDayStatisticStartDate) { + overtimeDayStatisticWhere.push(`overtime_day.day >= '${moment(startDate).format('YYYY-MM-DD')}'`) + } + if (overtimeDayStatisticendDate) { + overtimeDayStatisticWhere.push(`overtime_day.day <= '${moment(endDate).format('YYYY-MM-DD')}'`) + } + // CRAZY const innerSelectQuery = ` FROM member INNER JOIN ${pepEmis}.user AS user ON member.pep_user_id = user.id ${keywordTarget == 'number' && keyword ? ` - AND toString(user.id) LIKE '%${keyword}%' + AND user.people_code LIKE '%${keyword}%' `: ''} ${keywordTarget == 'name' && keyword ? ` AND user.name LIKE '%${keyword}%' `: ''} ${state == 'vacate' ? 'INNER' : 'LEFT'} JOIN ( - SELECT - pep_user_id, + SELECT + pep_user_id, any(start_time) AS vacateStartTime, any(end_time) AS vacateEndTime FROM vacate @@ -94,7 +104,24 @@ module.exports = function (app, opts) { ) AS hrOvertime ON hrOvertime.pep_user_id = member.pep_user_id - WHERE + ${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`: ''} + + WHERE member.del = 0 ${keywordTarget == 'role' && keyword ? ` AND user.id IN ( @@ -147,6 +174,11 @@ module.exports = function (app, opts) { FROM ( SELECT member.*, + + ${orderBy == 'overtimeTakeRestSum' || orderBy == 'overtimePaySum' || orderBy == 'overtimeSum' ? ` + overtimeDayStatistic.duration AS overtimeDayStatisticDuration + `: ''} + hrVacate.vacateStartTime AS vacateStartTime, hrVacate.vacateEndTime AS vacateEndTime, hrOvertime.overtimeStartTime AS overtimeStartTime, @@ -167,12 +199,18 @@ module.exports = function (app, opts) { 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."member.overtimeDayStatisticDuration"' + : 'user.people_code'} + ${orderDirection || 'ASC'} `).toPromise() const countRes = await clickHouse.hr.query(` SELECT - count(member.pep_user_id) AS count - ${innerSelectQuery} + count(member.pep_user_id) AS count + ${innerSelectQuery} `).toPromise() return {