From 67196c9efe1f10ed7c60c2d3bea98619d54083fe Mon Sep 17 00:00:00 2001 From: Nandinbold Norovsambuu Date: Wed, 13 Mar 2024 18:29:18 +0800 Subject: [PATCH] fix(timeclocks): fix time clock list ui --- packages/plugin-timeclock-api/src/configs.ts | 17 + .../plugin-timeclock-api/src/constants.ts | 28 +- .../src/graphql/resolvers/queries.ts | 169 ++++-- .../src/graphql/resolvers/utils.ts | 569 +++++++++++------- .../src/graphql/schema.ts | 12 +- .../src/models/definitions/timeclock.ts | 133 ++-- .../src/timeclockExport.ts | 496 +++++++++++++++ packages/plugin-timeclock-api/src/utils.ts | 67 +++ .../src/components/List.tsx | 26 +- .../src/components/sidebar/SideBar.tsx | 70 +-- .../src/components/timeclock/ActionBar.tsx | 75 +++ .../timeclock/TimeclockEditForm.tsx | 184 ++++++ .../components/timeclock/TimeclockList2.tsx | 562 +++++++++++++++++ packages/plugin-timeclock-ui/src/constants.ts | 25 +- .../containers/timeclock/TimeclockList2.tsx | 94 +++ .../src/graphql/queries.ts | 42 +- packages/plugin-timeclock-ui/src/styles.ts | 127 +++- packages/plugin-timeclock-ui/src/types.ts | 10 + 18 files changed, 2258 insertions(+), 448 deletions(-) create mode 100644 packages/plugin-timeclock-api/src/timeclockExport.ts create mode 100644 packages/plugin-timeclock-ui/src/components/timeclock/ActionBar.tsx create mode 100644 packages/plugin-timeclock-ui/src/components/timeclock/TimeclockEditForm.tsx create mode 100644 packages/plugin-timeclock-ui/src/components/timeclock/TimeclockList2.tsx create mode 100644 packages/plugin-timeclock-ui/src/containers/timeclock/TimeclockList2.tsx diff --git a/packages/plugin-timeclock-api/src/configs.ts b/packages/plugin-timeclock-api/src/configs.ts index 59030af7ccd..48678133849 100644 --- a/packages/plugin-timeclock-api/src/configs.ts +++ b/packages/plugin-timeclock-api/src/configs.ts @@ -10,6 +10,7 @@ import { buildFile } from './reportExport'; import * as permissions from './permissions'; import { removeDuplicates } from './removeDuplicateTimeclocks'; import app from '@erxes/api-utils/src/app'; +import { buildFile as timeclockBuildFile } from './timeclockExport'; export default { name: 'timeclock', @@ -59,6 +60,22 @@ export default { return res.send(result.response); }), ); + + app.get( + '/timeclock-export', + routeErrorHandling(async (req: any, res) => { + const { query } = req; + + const subdomain = getSubdomain(req); + const models = await generateModels(subdomain); + + const result = await timeclockBuildFile(models, subdomain, query); + + res.attachment(`${result.name}.xlsx`); + + return res.send(result.response); + }), + ); }, setupMessageConsumers, }; diff --git a/packages/plugin-timeclock-api/src/constants.ts b/packages/plugin-timeclock-api/src/constants.ts index 134cd4f78a8..c4e2b48c862 100644 --- a/packages/plugin-timeclock-api/src/constants.ts +++ b/packages/plugin-timeclock-api/src/constants.ts @@ -6,12 +6,12 @@ export const PRELIMINARY_REPORT_COLUMNS = [ 'Албан тушаал', 'Ажиллавал зохих хоног', 'Ажилласан хоног', - 'Тайлбар' + 'Тайлбар', ]; export const FINAL_REPORT_COLUMNS = [ [ ['Хүнтэй холбоотой мэдээлэл'], - ['Ажилтаны код', 'Овог', 'Нэр', 'Албан тушаал'] + ['Ажилтаны код', 'Овог', 'Нэр', 'Албан тушаал'], ], [['Ажиллах ёстой цаг'], ['Хоног', 'Цаг', 'Нийт цайны цаг']], [ @@ -23,8 +23,8 @@ export const FINAL_REPORT_COLUMNS = [ 'Шөнийн цаг 1.2', 'Нийт цайны цаг', 'Нийт ажилласан цаг', - 'Хоцролт тооцох' - ] + 'Хоцролт тооцох', + ], ], [[' Томилолт'], ['Shift request', 'Томилолтоор ажилласан цаг']], [ @@ -32,14 +32,14 @@ export const FINAL_REPORT_COLUMNS = [ [ 'Чөлөөтэй цаг цалинтай', 'Чөлөөтэй цаг цалингүй', - 'Өвдсөн цаг /ХЧТАТ бодох цаг/' - ] - ] + 'Өвдсөн цаг /ХЧТАТ бодох цаг/', + ], + ], ]; export const PIVOT_REPORT_COLUMNS = [ [ ['Хүнтэй холбоотой мэдээлэл'], - ['№', 'Ажилтаны код', 'Овог', 'Нэр', 'Албан тушаал'] + ['№', 'Ажилтаны код', 'Овог', 'Нэр', 'Албан тушаал'], ], [['Хугацаа'], ['Өдөр']], [['Төлөвлөгөө'], ['Эхлэх', 'Дуусах', 'Нийт төлөвлөсөн', 'Цайны цаг']], @@ -54,7 +54,13 @@ export const PIVOT_REPORT_COLUMNS = [ 'Шөнийн цаг', 'Илүү цаг', 'Нийт ажилласан', - 'Хоцролт' - ] - ] + 'Хоцролт', + ], + ], +]; + +export const TIMECLOCK_EXPORT_COLUMNS = [ + [[''], ['Д/Д']], + [[''], ['Овог нэр']], + [[''], ['Ажилтаны код']], ]; diff --git a/packages/plugin-timeclock-api/src/graphql/resolvers/queries.ts b/packages/plugin-timeclock-api/src/graphql/resolvers/queries.ts index e51e5fd9e4a..b9affc3fa31 100644 --- a/packages/plugin-timeclock-api/src/graphql/resolvers/queries.ts +++ b/packages/plugin-timeclock-api/src/graphql/resolvers/queries.ts @@ -1,10 +1,12 @@ import { IContext } from '../../connectionResolver'; import { + findTimeclockTeamMemberIds, paginateArray, timeclockReportByUser, + timeclockReportByUsers, timeclockReportFinal, timeclockReportPivot, - timeclockReportPreliminary + timeclockReportPreliminary, } from './utils'; import { customFixDate, @@ -13,7 +15,7 @@ import { generateCommonUserIds, generateFilter, returnDepartmentsBranchesDict, - returnSupervisedUsers + returnSupervisedUsers, } from '../../utils'; import { IReport } from '../../models/definitions/timeclock'; import { moduleRequireLogin } from '@erxes/api-utils/src/permissions'; @@ -23,7 +25,7 @@ import { sendCoreMessage } from '../../messageBroker'; const timeclockQueries = { async absences(_root, queryParams, { models, subdomain, user }: IContext) { return models.Absences.find( - await generateFilter(queryParams, subdomain, models, 'absence', user) + await generateFilter(queryParams, subdomain, models, 'absence', user), ); }, @@ -42,11 +44,11 @@ const timeclockQueries = { action: `branches.find`, data: { query: { - supervisorId: user._id - } + supervisorId: user._id, + }, }, isRPC: true, - defaultValue: [] + defaultValue: [], }); }, @@ -55,17 +57,17 @@ const timeclockQueries = { subdomain, action: `departments.find`, data: { - supervisorId: user._id + supervisorId: user._id, }, isRPC: true, - defaultValue: [] + defaultValue: [], }); }, timeclocksPerUser( _root, { userId, startDate, endDate, shiftActive }, - { models, user }: IContext + { models, user }: IContext, ) { const getUserId = userId || user._id; @@ -74,16 +76,16 @@ const timeclockQueries = { { shiftStart: { $gte: fixDate(startDate), - $lte: fixDate(endDate) - } + $lte: fixDate(endDate), + }, }, { shiftEnd: { $gte: fixDate(startDate), - $lte: fixDate(endDate) - } - } - ] + $lte: fixDate(endDate), + }, + }, + ], }; const selector: any = [{ userId: getUserId }, timeField]; @@ -98,14 +100,14 @@ const timeclockQueries = { async timeclocksMain( _root, queryParams, - { subdomain, models, user }: IContext + { subdomain, models, user }: IContext, ) { const [selector, commonUserFound] = await generateFilter( queryParams, subdomain, models, 'timeclock', - user + user, ); // if there's no common user, return empty list @@ -117,10 +119,10 @@ const timeclockQueries = { const list = paginate(models.Timeclocks.find(selector), { perPage: queryParams.perPage, - page: queryParams.page + page: queryParams.page, }) .sort({ - shiftStart: -1 + shiftStart: -1, }) .limit(queryParams.perPage || 20); @@ -133,7 +135,7 @@ const timeclockQueries = { // return the latest started active shift const getActiveTimeclock = await models.Timeclocks.find({ userId: getUserId, - shiftActive: true + shiftActive: true, }) .sort({ shiftStart: 1 }) .limit(1); @@ -144,14 +146,14 @@ const timeclockQueries = { async timelogsMain( _root, queryParams, - { subdomain, models, user }: IContext + { subdomain, models, user }: IContext, ) { const [selector, commonUserFound] = await generateFilter( queryParams, subdomain, models, 'timelog', - user + user, ); const totalCount = models.TimeLogs.count(selector); @@ -162,7 +164,7 @@ const timeclockQueries = { const list = paginate(models.TimeLogs.find(selector), { perPage: queryParams.perPage, - page: queryParams.page + page: queryParams.page, }) .sort({ userId: 1, timelog: -1 }) .limit(queryParams.perPage || 20); @@ -174,26 +176,26 @@ const timeclockQueries = { const timeField = { timelog: { $gte: fixDate(startDate), - $lte: customFixDate(endDate) - } + $lte: customFixDate(endDate), + }, }; return models.TimeLogs.find({ - $and: [{ userId }, timeField] + $and: [{ userId }, timeField], }).sort({ timelog: 1 }); }, async schedulesMain( _root, queryParams, - { models, subdomain, user }: IContext + { models, subdomain, user }: IContext, ) { const [selector, commonUserFound] = await generateFilter( queryParams, subdomain, models, 'schedule', - user + user, ); // if there's no common user, return empty list @@ -205,7 +207,7 @@ const timeclockQueries = { const list = paginate(models.Schedules.find(selector), { perPage: queryParams.perPage, - page: queryParams.page + page: queryParams.page, }); return { list, totalCount }; @@ -228,13 +230,13 @@ const timeclockQueries = { if (searchValue) { query.$or = [ { deviceName: new RegExp(`.*${searchValue}.*`, 'gi') }, - { serialNo: new RegExp(`.*${searchValue}.*`, 'gi') } + { serialNo: new RegExp(`.*${searchValue}.*`, 'gi') }, ]; } const list = paginate(models.DeviceConfigs.find(query), { perPage: queryParams.perPage, - page: queryParams.page + page: queryParams.page, }); return { list, totalCount }; @@ -248,14 +250,14 @@ const timeclockQueries = { async requestsMain( _root, queryParams, - { models, subdomain, user }: IContext + { models, subdomain, user }: IContext, ) { const [selector, commonUserFound] = await generateFilter( queryParams, subdomain, models, 'absence', - user + user, ); const totalCount = models.Absences.count(selector); @@ -266,7 +268,7 @@ const timeclockQueries = { const list = paginate(models.Absences.find(selector), { perPage: queryParams.perPage, - page: queryParams.page + page: queryParams.page, }).sort({ startTime: -1 }); return { list, totalCount }; @@ -296,7 +298,7 @@ const timeclockQueries = { async timeclockReportByUser( _root, { selectedUser, selectedMonth, selectedYear, selectedDate }, - { models, user }: IContext + { models, user }: IContext, ) { const userId = selectedUser || user._id; @@ -305,7 +307,7 @@ const timeclockQueries = { userId, selectedMonth, selectedYear, - selectedDate + selectedDate, ); }, @@ -320,9 +322,9 @@ const timeclockQueries = { page, perPage, reportType, - isCurrentUserAdmin + isCurrentUserAdmin, }, - { subdomain, user }: IContext + { subdomain, user }: IContext, ) { let filterGiven = false; let totalTeamMemberIds; @@ -347,7 +349,7 @@ const timeclockQueries = { subdomain, userIds, branchIds, - departmentIds + departmentIds, ); totalMembers = await findTeamMembers(subdomain, totalTeamMemberIds); @@ -355,11 +357,11 @@ const timeclockQueries = { if (isCurrentUserAdmin) { // return all team member ids totalMembers = await findAllTeamMembersWithEmpId(subdomain); - totalTeamMemberIds = totalMembers.map(usr => usr._id); + totalTeamMemberIds = totalMembers.map((usr) => usr._id); } else { // return supervisod users including current user totalMembers = await returnSupervisedUsers(user, subdomain); - totalTeamMemberIds = totalMembers.map(usr => usr._id); + totalTeamMemberIds = totalMembers.map((usr) => usr._id); } } @@ -372,19 +374,19 @@ const timeclockQueries = { paginateArray(totalTeamMemberIds, perPage, page), startDate, endDate, - false + false, ); for (const userId of Object.keys(reportPreliminary)) { returnReport.push({ - groupReport: [{ userId, ...reportPreliminary[userId] }] + groupReport: [{ userId, ...reportPreliminary[userId] }], }); } break; case 'Сүүлд' || 'Final': const paginatedTeamMembers = paginateArray(totalMembers, perPage, page); - const paginatedTeamMemberIds = paginatedTeamMembers.map(e => e._id); + const paginatedTeamMemberIds = paginatedTeamMembers.map((e) => e._id); for (const teamMember of paginatedTeamMembers) { if (teamMember.branchIds) { @@ -399,14 +401,14 @@ const timeclockQueries = { branchIds: teamMember.branchIds ? teamMember.branchIds : [], departmentIds: teamMember.departmentIds ? teamMember.departmentIds - : [] + : [], }; } const structuresDict = await returnDepartmentsBranchesDict( subdomain, totalBranchIdsOfMembers, - totalDeptIdsOfMembers + totalDeptIdsOfMembers, ); const reportFinal: any = await timeclockReportFinal( @@ -414,7 +416,7 @@ const timeclockQueries = { paginatedTeamMemberIds, startDate, endDate, - false + false, ); for (const userId of Object.keys(reportFinal)) { @@ -437,8 +439,13 @@ const timeclockQueries = { returnReport.push({ groupReport: [ - { userId, branchTitles, departmentTitles, ...reportFinal[userId] } - ] + { + userId, + branchTitles, + departmentTitles, + ...reportFinal[userId], + }, + ], }); } break; @@ -448,12 +455,12 @@ const timeclockQueries = { paginateArray(totalTeamMemberIds, perPage, page), startDate, endDate, - false + false, ); for (const userId of Object.keys(reportPivot)) { returnReport.push({ - groupReport: [{ userId, ...reportPivot[userId] }] + groupReport: [{ userId, ...reportPivot[userId] }], }); } break; @@ -461,9 +468,65 @@ const timeclockQueries = { return { list: returnReport, - totalCount: totalTeamMemberIds.length + totalCount: totalTeamMemberIds.length, + }; + }, + async timeclockReportByUsers( + _root, + { + userIds, + branchIds, + departmentIds, + startDate, + endDate, + page, + perPage, + isCurrentUserAdmin, + }, + { subdomain, models, user }: IContext, + ) { + let filterGiven = false; + let totalTeamMemberIds; + let totalTeamMembers; + + if (userIds || branchIds || departmentIds) { + filterGiven = true; + } + + if (filterGiven) { + totalTeamMemberIds = await generateCommonUserIds( + subdomain, + userIds, + branchIds, + departmentIds, + ); + + totalTeamMembers = await findTeamMembers(subdomain, totalTeamMemberIds); + } else { + if (isCurrentUserAdmin) { + // return all team member ids + totalTeamMemberIds = await findTimeclockTeamMemberIds( + models, + startDate, + endDate, + ); + totalTeamMembers = await findTeamMembers(subdomain, totalTeamMemberIds); + } else { + // return supervisod users including current user + totalTeamMembers = await returnSupervisedUsers(user, subdomain); + totalTeamMemberIds = totalTeamMembers.map((usr) => usr._id); + } + } + + return { + list: await timeclockReportByUsers( + paginateArray(totalTeamMemberIds, perPage, page), + models, + { startDate, endDate }, + ), + totalCount: totalTeamMemberIds.length, }; - } + }, }; moduleRequireLogin(timeclockQueries); diff --git a/packages/plugin-timeclock-api/src/graphql/resolvers/utils.ts b/packages/plugin-timeclock-api/src/graphql/resolvers/utils.ts index dd1d07a2465..3a51a040e11 100644 --- a/packages/plugin-timeclock-api/src/graphql/resolvers/utils.ts +++ b/packages/plugin-timeclock-api/src/graphql/resolvers/utils.ts @@ -9,7 +9,8 @@ import { IScheduleDocument, IShiftDocument, IUserAbsenceInfo, - IUsersReport + IUserReport, + IUsersReport, } from '../../models/definitions/timeclock'; import { customFixDate } from '../../utils'; import { sendMobileNotification } from '../utils'; @@ -31,7 +32,7 @@ export const findBranches = async (subdomain: string, branchIds: string[]) => { subdomain, action: 'branches.find', data: { query: { _id: { $in: branchIds } } }, - isRPC: true + isRPC: true, }); return branches; @@ -39,13 +40,13 @@ export const findBranches = async (subdomain: string, branchIds: string[]) => { export const findDepartments = async ( subdomain: string, - departmentIds: string[] + departmentIds: string[], ) => { const branches = await sendCoreMessage({ subdomain, action: 'departments.find', data: { _id: { $in: departmentIds } }, - isRPC: true + isRPC: true, }); return branches; @@ -56,9 +57,9 @@ export const findUser = async (subdomain: string, userId: string) => { subdomain, action: 'users.findOne', data: { - _id: userId + _id: userId, }, - isRPC: true + isRPC: true, }); return user; @@ -66,26 +67,26 @@ export const findUser = async (subdomain: string, userId: string) => { export const findBranchUsers = async ( subdomain: string, - branchIds: string[] + branchIds: string[], ) => { const branchUsers = await sendCoreMessage({ subdomain, action: 'users.find', data: { query: { branchIds: { $in: branchIds }, isActive: true } }, - isRPC: true + isRPC: true, }); return branchUsers; }; export const findDepartmentUsers = async ( subdomain: string, - departmentIds: string[] + departmentIds: string[], ) => { const deptUsers = await sendCoreMessage({ subdomain, action: 'users.find', data: { query: { departmentIds: { $in: departmentIds }, isActive: true } }, - isRPC: true + isRPC: true, }); return deptUsers; }; @@ -94,7 +95,7 @@ export const returnUnionOfUserIds = async ( branchIds: string[], departmentIds: string[], userIds: string[], - subdomain: any + subdomain: any, ) => { if (userIds.length) { return userIds; @@ -103,13 +104,13 @@ export const returnUnionOfUserIds = async ( if (branchIds) { const branchUsers = await findBranchUsers(subdomain, branchIds); - const branchUserIds = branchUsers.map(branchUser => branchUser._id); + const branchUserIds = branchUsers.map((branchUser) => branchUser._id); concatBranchDept.push(...branchUserIds); } if (departmentIds) { const departmentUsers = await findDepartmentUsers(subdomain, departmentIds); const departmentUserIds = departmentUsers.map( - departmentUser => departmentUser._id + (departmentUser) => departmentUser._id, ); concatBranchDept.push(...departmentUserIds); } @@ -126,7 +127,7 @@ export const createScheduleShiftsByUserIds = async ( userIds: string[], scheduleShifts, models: IModels, - totalBreakInMins?: number + totalBreakInMins?: number, ) => { const shiftsBulkCreateOps: any[] = []; @@ -137,7 +138,7 @@ export const createScheduleShiftsByUserIds = async ( solved: true, status: 'Approved', submittedByAdmin: true, - totalBreakInMins + totalBreakInMins, }); for (const shift of scheduleShifts) { @@ -150,9 +151,9 @@ export const createScheduleShiftsByUserIds = async ( scheduleConfigId: shift.scheduleConfigId, lunchBreakInMins: shift.lunchBreakInMins, solved: true, - status: 'Approved' - } - } + status: 'Approved', + }, + }, }); } } @@ -170,19 +171,19 @@ export const timeclockReportByUser = async ( userId: string, selectedMonth: string, selectedYear: string, - selectedDate?: string + selectedDate?: string, ) => { let report: any = { scheduleReport: [], userId, - totalHoursScheduledSelectedMonth: 0 + totalHoursScheduledSelectedMonth: 0, }; // get 1st of the next Month const NOW = new Date(); const selectedMonthIndex = new Date( - Date.parse(selectedMonth + ' 1, 2000') + Date.parse(selectedMonth + ' 1, 2000'), ).getMonth(); const nextMonthIndex = selectedMonthIndex === 11 ? 0 : selectedMonthIndex + 1; @@ -190,13 +191,13 @@ export const timeclockReportByUser = async ( // get 1st of month const startOfSelectedMonth = new Date( parseFloat(selectedYear), - selectedMonthIndex + selectedMonthIndex, ); // start of the next month const startOfNextMonth = new Date( parseFloat(selectedYear), nextMonthIndex, - 1 + 1, ); // start, end Time filter for queries @@ -216,10 +217,10 @@ export const timeclockReportByUser = async ( const totalSchedulesOfUser = await models.Schedules.find({ userId, solved: true, - status: 'Approved' + status: 'Approved', }); - const totalScheduleIds = totalSchedulesOfUser.map(schedule => schedule._id); + const totalScheduleIds = totalSchedulesOfUser.map((schedule) => schedule._id); // schedule shifts of selected month scheduleShiftsSelectedMonth.push( @@ -228,15 +229,15 @@ export const timeclockReportByUser = async ( status: 'Approved', shiftStart: { $gte: startOfSelectedMonth, - $lte: startOfNextMonth - } - })) + $lte: startOfNextMonth, + }, + })), ); const scheduleShiftConfigsSelectedMonth = await models.ScheduleConfigs.find({ _id: { - $in: scheduleShiftsSelectedMonth.map(shift => shift.scheduleConfigId) - } + $in: scheduleShiftsSelectedMonth.map((shift) => shift.scheduleConfigId), + }, }); const scheduleShiftConfisMap: { @@ -253,31 +254,33 @@ export const timeclockReportByUser = async ( { shiftStart: { $gte: startOfSelectedMonth, - $lte: startOfNextMonth - } + $lte: startOfNextMonth, + }, }, - { shiftActive: false } - ] + { shiftActive: false }, + ], }); const totalRequestsOfUser = await models.Absences.find({ userId, solved: true, checkInOutRequest: { $exists: false }, - status: { $regex: /Approved/, $options: 'gi' } + status: { $regex: /Approved/, $options: 'gi' }, }); const requestsOfSelectedMonth = totalRequestsOfUser.filter( - req => + (req) => dayjs(req.startTime) >= dayjs(startOfSelectedMonth) && - dayjs(req.startTime) < dayjs(startOfNextMonth) + dayjs(req.startTime) < dayjs(startOfNextMonth), ); - const absenceTypeIds = requestsOfSelectedMonth.map(req => req.absenceTypeId); + const absenceTypeIds = requestsOfSelectedMonth.map( + (req) => req.absenceTypeId, + ); let totalHoursAbsenceSelectedMonth = 0; const absenceTypes = await models.AbsenceTypes.find({ - _id: { $in: absenceTypeIds } + _id: { $in: absenceTypeIds }, }); for (const request of requestsOfSelectedMonth) { @@ -298,7 +301,7 @@ export const timeclockReportByUser = async ( // absence by day if (request.absenceTimeType === 'by day' && request.requestDates) { const getAbsenceType = absenceTypes.find( - absType => absType._id === request.absenceTypeId + (absType) => absType._id === request.absenceTypeId, ); const getTotalHoursOfAbsence = @@ -360,21 +363,21 @@ export const timeclockReportByUser = async ( checked: false, recordedStart: timeclock.shiftStart, recordedEnd: timeclock.shiftEnd, - shiftDuration - }) + shiftDuration, + }), }; } const totalDaysWorkedSelectedMonth = new Set( - timeclocksOfSelectedMonth.map(shift => { + timeclocksOfSelectedMonth.map((shift) => { return new Date(shift.shiftStart).toLocaleDateString(); - }) + }), ).size; const totalDaysScheduledSelectedMonth = new Set( - scheduleShiftsSelectedMonth.map(scheduleShift => - new Date(scheduleShift.shiftStart || '').toLocaleDateString() - ) + scheduleShiftsSelectedMonth.map((scheduleShift) => + new Date(scheduleShift.shiftStart || '').toLocaleDateString(), + ), ).size; // schedules @@ -382,7 +385,7 @@ export const timeclockReportByUser = async ( let lunchBreakOfDay = 0; const scheduleDateString = new Date( - scheduleShift.shiftStart || '' + scheduleShift.shiftStart || '', ).toLocaleDateString(); // schedule duration per shift @@ -411,10 +414,10 @@ export const timeclockReportByUser = async ( report.totalHoursScheduledSelectedDay = scheduleDuration; const recordedShiftOfSelectedDay = report.scheduleReport.find( - shift => + (shift) => shift.date === selectedDayString && shift.recordedStart && - !shift.checked + !shift.checked, ); scheduledShiftStartSelectedDay = scheduleShift.shiftStart; @@ -439,14 +442,15 @@ export const timeclockReportByUser = async ( } totalHoursBreakSelecteDay = lunchBreakOfDay; - report.totalHoursWorkedSelectedDay = report.totalHoursWorkedSelectedDay - ? report.totalHoursWorkedSelectedDay - lunchBreakOfDay - : 0; + report.totalHoursWorkedSelectedDay = + report.totalHoursWorkedSelectedDay + ? report.totalHoursWorkedSelectedDay - lunchBreakOfDay + : 0; } } const recordedShiftIdx = report.scheduleReport.findIndex( - shift => shift.date === scheduleDateString && !shift.checked + (shift) => shift.date === scheduleDateString && !shift.checked, ); // no timeclock found, thus not worked on a scheduled day @@ -476,14 +480,14 @@ export const timeclockReportByUser = async ( // calcute shifts worked outside schedule const shiftsWorkedOutsideSchedule = report.scheduleReport.filter( - shift => !shift.checked + (shift) => !shift.checked, ); totalDaysWorkedOutsideSchedule = shiftsWorkedOutsideSchedule.length; totalHoursWorkedOutsideSchedule = shiftsWorkedOutsideSchedule.reduce( (partialHoursSum, shift) => partialHoursSum + shift.shiftDuration || 0, - 0 + 0, ); totalDaysNotWorked = new Set(notWorkedDays).size; @@ -495,8 +499,8 @@ export const timeclockReportByUser = async ( timeclockEnd: recordedShiftEndSelectedDay, scheduledStart: scheduledShiftStartSelectedDay, - scheduledEnd: scheduledShiftEndSelectedDay - } + scheduledEnd: scheduledShiftEndSelectedDay, + }, ]; report = { @@ -519,7 +523,7 @@ export const timeclockReportByUser = async ( scheduledShifts: scheduleShiftsSelectedMonth, timeclocks: timeclocksOfSelectedMonth, - scheduleReport + scheduleReport, }; } @@ -532,7 +536,7 @@ export const timeclockReportPreliminary = async ( startDate: Date, endDate: Date, teamMembersObj?: any, - exportToXlsx?: boolean + exportToXlsx?: boolean, ) => { const models = await generateModels(subdomain); @@ -541,12 +545,12 @@ export const timeclockReportPreliminary = async ( // get the schedule data of this month const schedules = await models.Schedules.find({ - userId: { $in: userIds } + userId: { $in: userIds }, }).sort({ - userId: 1 + userId: 1, }); - const scheduleIds = schedules.map(schedule => schedule._id); + const scheduleIds = schedules.map((schedule) => schedule._id); const timeclocks = await models.Timeclocks.find({ $and: [ @@ -554,16 +558,16 @@ export const timeclockReportPreliminary = async ( { shiftStart: { $gte: fixDate(startDate), - $lte: customFixDate(endDate) - } + $lte: customFixDate(endDate), + }, }, { shiftEnd: { $gte: fixDate(startDate), - $lte: customFixDate(endDate) - } - } - ] + $lte: customFixDate(endDate), + }, + }, + ], }).sort({ userId: 1 }); shiftsOfSchedule.push( @@ -574,14 +578,14 @@ export const timeclockReportPreliminary = async ( { shiftStart: { $gte: fixDate(startDate), - $lte: customFixDate(endDate) - } - } - ] - })) + $lte: customFixDate(endDate), + }, + }, + ], + })), ); - userIds.forEach(async currUserId => { + userIds.forEach(async (currUserId) => { // assign team member info from teamMembersObj if (exportToXlsx) { @@ -589,20 +593,20 @@ export const timeclockReportPreliminary = async ( } const currUserTimeclocks = timeclocks.filter( - timeclock => timeclock.userId === currUserId + (timeclock) => timeclock.userId === currUserId, ); const currUserSchedules = schedules.filter( - schedule => schedule.userId === currUserId + (schedule) => schedule.userId === currUserId, ); // get shifts of schedule const currUserScheduleShifts: any = []; - currUserSchedules.forEach(async userSchedule => { + currUserSchedules.forEach(async (userSchedule) => { currUserScheduleShifts.push( ...shiftsOfSchedule.filter( - scheduleShift => scheduleShift.scheduleId === userSchedule._id - ) + (scheduleShift) => scheduleShift.scheduleId === userSchedule._id, + ), ); }); @@ -611,18 +615,18 @@ export const timeclockReportPreliminary = async ( if (currUserTimeclocks) { totalDaysWorkedPerUser = new Set( - currUserTimeclocks.map(shift => { + currUserTimeclocks.map((shift) => { if (!shift.shiftActive) { return new Date(shift.shiftStart).toLocaleDateString(); } - }) + }), ).size; } if (currUserScheduleShifts) { totalDaysScheduledPerUser += new Set( - currUserScheduleShifts.map(shiftOfSchedule => - new Date(shiftOfSchedule.shiftStart).toLocaleDateString() - ) + currUserScheduleShifts.map((shiftOfSchedule) => + new Date(shiftOfSchedule.shiftStart).toLocaleDateString(), + ), ).size; } @@ -632,7 +636,7 @@ export const timeclockReportPreliminary = async ( } else { usersReport[currUserId] = { totalDaysScheduled: totalDaysScheduledPerUser, - totalDaysWorked: totalDaysWorkedPerUser + totalDaysWorked: totalDaysWorkedPerUser, }; } }); @@ -646,7 +650,7 @@ export const timeclockReportFinal = async ( startDate?: Date, endDate?: Date, teamMembersObj?: any, - exportToXlsx?: boolean + exportToXlsx?: boolean, ) => { const models = await generateModels(subdomain); const usersReport: IUsersReport = {}; @@ -656,12 +660,12 @@ export const timeclockReportFinal = async ( const schedules = await models.Schedules.find({ userId: { $in: userIds }, solved: true, - status: { $regex: /Approved/gi } + status: { $regex: /Approved/gi }, }).sort({ - userId: 1 + userId: 1, }); - const scheduleIds = schedules.map(schedule => schedule._id); + const scheduleIds = schedules.map((schedule) => schedule._id); // get all approved absence requests const requests = await models.Absences.find({ @@ -672,20 +676,20 @@ export const timeclockReportFinal = async ( { startTime: { $gte: fixDate(startDate), - $lte: customFixDate(endDate) - } + $lte: customFixDate(endDate), + }, }, { endTime: { $gte: fixDate(startDate), - $lte: customFixDate(endDate) - } - } - ] + $lte: customFixDate(endDate), + }, + }, + ], }); const relatedAbsenceTypes = await models.AbsenceTypes.find({ - _id: { $in: requests.map(request => request.absenceTypeId) } + _id: { $in: requests.map((request) => request.absenceTypeId) }, }); // get all related absences @@ -698,16 +702,16 @@ export const timeclockReportFinal = async ( { shiftStart: { $gte: fixDate(startDate), - $lte: customFixDate(endDate) - } + $lte: customFixDate(endDate), + }, }, { shiftEnd: { $gte: fixDate(startDate), - $lte: customFixDate(endDate) - } - } - ] + $lte: customFixDate(endDate), + }, + }, + ], }).sort({ userId: 1 }); shiftsOfSchedule.push( @@ -718,36 +722,36 @@ export const timeclockReportFinal = async ( { shiftStart: { $gte: fixDate(startDate), - $lte: customFixDate(endDate) - } - } - ] - })) + $lte: customFixDate(endDate), + }, + }, + ], + })), ); const shiftsOfScheduleConfigIds = shiftsOfSchedule.map( - scheduleShift => scheduleShift.scheduleConfigId + (scheduleShift) => scheduleShift.scheduleConfigId, ); const scheduleShiftsConfigs = await models.ScheduleConfigs.find({ - _id: { $in: shiftsOfScheduleConfigIds } + _id: { $in: shiftsOfScheduleConfigIds }, }); const scheduleShiftConfigsMap: { [scheduleConfigId: string]: number } = {}; scheduleShiftsConfigs.map( - scheduleConfig => + (scheduleConfig) => (scheduleShiftConfigsMap[scheduleConfig._id] = - scheduleConfig.lunchBreakInMins) + scheduleConfig.lunchBreakInMins), ); const schedulesObj = createSchedulesObj( userIds, schedules, shiftsOfSchedule, - scheduleShiftConfigsMap + scheduleShiftConfigsMap, ); - userIds.forEach(async currUserId => { + userIds.forEach(async (currUserId) => { // assign team member info from teamMembersObj if (exportToXlsx) { @@ -755,27 +759,30 @@ export const timeclockReportFinal = async ( } const currUserTimeclocks = timeclocks.filter( - timeclock => timeclock.userId === currUserId + (timeclock) => timeclock.userId === currUserId, ); const filterSchedules = shiftsOfSchedule.map( - scheduleShift => scheduleShift.scheduleId + (scheduleShift) => scheduleShift.scheduleId, ); const currUserSchedules = schedules.filter( - schedule => - schedule.userId === currUserId && filterSchedules.includes(schedule._id) + (schedule) => + schedule.userId === currUserId && + filterSchedules.includes(schedule._id), ); - const currUserScheduleIds = currUserSchedules.map(schedule => schedule._id); + const currUserScheduleIds = currUserSchedules.map( + (schedule) => schedule._id, + ); // get shifts of schedule const currUserScheduleShifts: any = []; for (const userSchedule of currUserSchedules) { currUserScheduleShifts.push( ...shiftsOfSchedule.filter( - scheduleShift => scheduleShift.scheduleId === userSchedule._id - ) + (scheduleShift) => scheduleShift.scheduleId === userSchedule._id, + ), ); } @@ -800,14 +807,14 @@ export const timeclockReportFinal = async ( (userScheduleShift.lunchBreakInMins || scheduleShiftConfigsMap[userScheduleShift.scheduleConfigId] || 0), - 0 + 0, ) / 60; if (currUserTimeclocks) { totalDaysWorkedPerUser = new Set( - currUserTimeclocks.map(shift => - new Date(shift.shiftStart).toLocaleDateString() - ) + currUserTimeclocks.map((shift) => + new Date(shift.shiftStart).toLocaleDateString(), + ), ).size; for (const currUserTimeclock of currUserTimeclocks) { @@ -839,7 +846,7 @@ export const timeclockReportFinal = async ( totalHoursOvernightPerUser += returnOvernightHours( shiftStart, - shiftEnd + shiftEnd, ); if ( @@ -853,11 +860,11 @@ export const timeclockReportFinal = async ( const scheduleShiftEnd = getScheduleOfTheDay.shiftEnd; const getScheduleDuration = Math.abs( - scheduleShiftEnd.getTime() - scheduleShiftStart.getTime() + scheduleShiftEnd.getTime() - scheduleShiftStart.getTime(), ); const getTimeClockDuration = Math.abs( - shiftEnd.getTime() - shiftStart.getTime() + shiftEnd.getTime() - shiftStart.getTime(), ); // get difference in schedule duration and time clock duration @@ -890,12 +897,12 @@ export const timeclockReportFinal = async ( if (currUserScheduleShifts) { totalDaysScheduledPerUser += new Set( - currUserScheduleShifts.map(shiftOfSchedule => - new Date(shiftOfSchedule.shiftStart).toLocaleDateString() - ) + currUserScheduleShifts.map((shiftOfSchedule) => + new Date(shiftOfSchedule.shiftStart).toLocaleDateString(), + ), ).size; - currUserScheduleShifts.forEach(scheduledDay => { + currUserScheduleShifts.forEach((scheduledDay) => { const shiftStart = scheduledDay.shiftStart; const shiftEnd = scheduledDay.shiftEnd; // get time in hours @@ -911,22 +918,22 @@ export const timeclockReportFinal = async ( const userAbsenceInfo: IUserAbsenceInfo = returnUserAbsenceInfo( { requestsShiftRequest: relatedAbsences.requestsShiftRequest.filter( - absence => absence.userId === currUserId + (absence) => absence.userId === currUserId, ), requestsWorkedAbroad: relatedAbsences.requestsWorkedAbroad.filter( - absence => absence.userId === currUserId + (absence) => absence.userId === currUserId, ), requestsPaidAbsence: relatedAbsences.requestsPaidAbsence.filter( - absence => absence.userId === currUserId + (absence) => absence.userId === currUserId, ), requestsUnpaidAbsence: relatedAbsences.requestsUnpaidAbsence.filter( - absence => absence.userId === currUserId + (absence) => absence.userId === currUserId, ), requestsSick: relatedAbsences.requestsSick.filter( - absence => absence.userId === currUserId - ) + (absence) => absence.userId === currUserId, + ), }, - relatedAbsenceTypes + relatedAbsenceTypes, ); // deduct lunch breaks from total scheduled hours @@ -947,7 +954,7 @@ export const timeclockReportFinal = async ( totalHoursBreakTaken: totalBreakOfTimeclocksInHrs.toFixed(2), totalHoursWorked: totalHoursWorkedPerUser.toFixed(2), totalMinsLate: totalMinsLatePerUser.toFixed(2), - ...userAbsenceInfo + ...userAbsenceInfo, }; return; } @@ -964,7 +971,7 @@ export const timeclockReportFinal = async ( totalHoursOvernight: totalHoursOvernightPerUser.toFixed(2), totalHoursWorked: totalHoursWorkedPerUser.toFixed(2), totalMinsLate: totalMinsLatePerUser.toFixed(2), - absenceInfo: userAbsenceInfo + absenceInfo: userAbsenceInfo, }; }); @@ -977,7 +984,7 @@ export const timeclockReportPivot = async ( startDate?: Date, endDate?: Date, teamMembersObj?: any, - exportToXlsx?: boolean + exportToXlsx?: boolean, ) => { const models = await generateModels(subdomain); const usersReport: any = {}; @@ -985,12 +992,12 @@ export const timeclockReportPivot = async ( // get the schedule data of this month const schedules = await models.Schedules.find({ - userId: { $in: userIds } + userId: { $in: userIds }, }).sort({ - userId: 1 + userId: 1, }); - const scheduleIds = schedules.map(schedule => schedule._id); + const scheduleIds = schedules.map((schedule) => schedule._id); const timeclocks = await models.Timeclocks.find({ $and: [ @@ -998,16 +1005,16 @@ export const timeclockReportPivot = async ( { shiftStart: { $gte: fixDate(startDate), - $lte: customFixDate(endDate) - } + $lte: customFixDate(endDate), + }, }, { shiftEnd: { $gte: fixDate(startDate), - $lte: customFixDate(endDate) - } - } - ] + $lte: customFixDate(endDate), + }, + }, + ], }).sort({ userId: 1 }); shiftsOfSchedule.push( @@ -1018,36 +1025,36 @@ export const timeclockReportPivot = async ( { shiftStart: { $gte: fixDate(startDate), - $lte: customFixDate(endDate) - } - } - ] - })) + $lte: customFixDate(endDate), + }, + }, + ], + })), ); const shiftsOfScheduleConfigIds = shiftsOfSchedule.map( - scheduleShift => scheduleShift.scheduleConfigId + (scheduleShift) => scheduleShift.scheduleConfigId, ); const scheduleShiftsConfigs = await models.ScheduleConfigs.find({ - _id: { $in: shiftsOfScheduleConfigIds } + _id: { $in: shiftsOfScheduleConfigIds }, }); const scheduleShiftConfigsMap: { [scheduleConfigId: string]: number } = {}; scheduleShiftsConfigs.map( - scheduleConfig => + (scheduleConfig) => (scheduleShiftConfigsMap[scheduleConfig._id] = - scheduleConfig.lunchBreakInMins) + scheduleConfig.lunchBreakInMins), ); const schedulesObj = createSchedulesObj( userIds, schedules, shiftsOfSchedule, - scheduleShiftConfigsMap + scheduleShiftConfigsMap, ); - userIds.forEach(async currUserId => { + userIds.forEach(async (currUserId) => { // assign team member info from teamMembersObj if (exportToXlsx) { @@ -1055,27 +1062,27 @@ export const timeclockReportPivot = async ( } const currUserTimeclocks = timeclocks.filter( - timeclock => timeclock.userId === currUserId + (timeclock) => timeclock.userId === currUserId, ); const currUserSchedules = schedules.filter( - schedule => schedule.userId === currUserId + (schedule) => schedule.userId === currUserId, ); // get shifts of schedule const currUserScheduleShifts: any = []; - currUserSchedules.forEach(userSchedule => { + currUserSchedules.forEach((userSchedule) => { currUserScheduleShifts.push( ...shiftsOfSchedule.filter( - scheduleShift => scheduleShift.scheduleId === userSchedule._id - ) + (scheduleShift) => scheduleShift.scheduleId === userSchedule._id, + ), ); }); const totalShiftsOfUser: any = []; if (currUserTimeclocks) { - currUserTimeclocks.forEach(currUserTimeclock => { + currUserTimeclocks.forEach((currUserTimeclock) => { let totalHoursOvertimePerShift = 0; let totalMinsLatePerShift = 0; let totalHoursOvernightPerShift = 0; @@ -1085,12 +1092,12 @@ export const timeclockReportPivot = async ( if (shiftStart && shiftEnd) { totalHoursOvernightPerShift = returnOvernightHours( shiftStart, - shiftEnd + shiftEnd, ); const scheduledDay = shiftStart.toLocaleDateString(); const getTimeClockDuration = Math.abs( - shiftEnd.getTime() - shiftStart.getTime() + shiftEnd.getTime() - shiftStart.getTime(), ); let scheduleShiftStart; @@ -1108,7 +1115,7 @@ export const timeclockReportPivot = async ( lunchBreakInHrs = getScheduleOfTheDay.lunchBreakInMins / 60; getScheduleDuration = Math.abs( - scheduleShiftEnd.getTime() - scheduleShiftStart.getTime() + scheduleShiftEnd.getTime() - scheduleShiftStart.getTime(), ); // get difference in schedule duration and time clock duration @@ -1159,7 +1166,7 @@ export const timeclockReportPivot = async ( scheduledDuration: scheduledDurationInHrs.toFixed(2), totalMinsLate: totalMinsLatePerShift.toFixed(2), totalHoursOvertime: totalHoursOvertimePerShift.toFixed(2), - totalHoursOvernight: totalHoursOvernightPerShift.toFixed(2) + totalHoursOvernight: totalHoursOvernightPerShift.toFixed(2), }); } }); @@ -1170,8 +1177,8 @@ export const timeclockReportPivot = async ( scheduleReport: totalShiftsOfUser.sort( (a, b) => new Date(a.timeclockStart).getTime() - - new Date(b.timeclockStart).getTime() - ) + new Date(b.timeclockStart).getTime(), + ), }; }); @@ -1180,7 +1187,7 @@ export const timeclockReportPivot = async ( const returnTotalAbsences = async ( totalRequests: IAbsence[], - models: IModels + models: IModels, ): Promise<{ requestsShiftRequest: IAbsence[]; requestsWorkedAbroad: IAbsence[]; @@ -1191,49 +1198,49 @@ const returnTotalAbsences = async ( // get all paid absence types' ids except sick absence const paidAbsenceTypes = await models.AbsenceTypes.find({ requestType: 'paid absence', - name: { $not: /өвдсөн цаг/gi } + name: { $not: /өвдсөн цаг/gi }, }); const paidAbsenceTypeIds = paidAbsenceTypes.map( - paidAbsence => paidAbsence._id + (paidAbsence) => paidAbsence._id, ); const shiftRequestAbsenceTypes = await models.AbsenceTypes.find({ - requestType: 'shift request' + requestType: 'shift request', }); const shiftRequestAbsenceTypeIds = shiftRequestAbsenceTypes.map( - absenceType => absenceType._id + (absenceType) => absenceType._id, ); // get all unpaid absence types' ids const unpaidAbsenceTypes = await models.AbsenceTypes.find({ - requestType: 'unpaid absence' + requestType: 'unpaid absence', }); const unpaidAbsenceTypeIds = unpaidAbsenceTypes.map( - unpaidAbsence => unpaidAbsence._id + (unpaidAbsence) => unpaidAbsence._id, ); // find Absences - const requestsShiftRequest = totalRequests.filter(request => - shiftRequestAbsenceTypeIds.includes(request.absenceTypeId || '') + const requestsShiftRequest = totalRequests.filter((request) => + shiftRequestAbsenceTypeIds.includes(request.absenceTypeId || ''), ); - const requestsWorkedAbroad = totalRequests.filter(request => - request.reason.toLocaleLowerCase().includes('томилолт') + const requestsWorkedAbroad = totalRequests.filter((request) => + request.reason.toLocaleLowerCase().includes('томилолт'), ); - const requestsPaidAbsence = totalRequests.filter(request => - paidAbsenceTypeIds.includes(request.absenceTypeId || '') + const requestsPaidAbsence = totalRequests.filter((request) => + paidAbsenceTypeIds.includes(request.absenceTypeId || ''), ); - const requestsUnpaidAbsence = totalRequests.filter(request => - unpaidAbsenceTypeIds.includes(request.absenceTypeId || '') + const requestsUnpaidAbsence = totalRequests.filter((request) => + unpaidAbsenceTypeIds.includes(request.absenceTypeId || ''), ); - const requestsSick = totalRequests.filter(request => - request.reason.toLowerCase().includes('өвдсөн цаг') + const requestsSick = totalRequests.filter((request) => + request.reason.toLowerCase().includes('өвдсөн цаг'), ); return { @@ -1241,13 +1248,13 @@ const returnTotalAbsences = async ( requestsWorkedAbroad, requestsPaidAbsence, requestsUnpaidAbsence, - requestsSick + requestsSick, }; }; const returnUserAbsenceInfo = ( relatedAbsences: any, - relatedAbsenceTypes: IAbsenceTypeDocument[] + relatedAbsenceTypes: IAbsenceTypeDocument[], ): IUserAbsenceInfo => { let totalHoursShiftRequest = 0; let totalHoursWorkedAbroad = 0; @@ -1255,14 +1262,14 @@ const returnUserAbsenceInfo = ( let totalHoursUnpaidAbsence = 0; let totalHoursSick = 0; - relatedAbsences.requestsShiftRequest.forEach(request => { + relatedAbsences.requestsShiftRequest.forEach((request) => { if (request.totalHoursOfAbsence) { totalHoursShiftRequest += parseFloat(request.totalHoursOfAbsence); return; } const absenceType = relatedAbsenceTypes.find( - absType => absType._id === request.absenceTypeId + (absType) => absType._id === request.absenceTypeId, ); if (absenceType && absenceType.requestTimeType === 'by day') { @@ -1270,7 +1277,7 @@ const returnUserAbsenceInfo = ( ? request.requestDates.length : Math.ceil( (request.endTime.getTime() - request.startTime.getTime()) / - MMSTODAYS + MMSTODAYS, ); totalHoursShiftRequest += getTotalDays * absenceType.requestHoursPerDay; @@ -1281,14 +1288,14 @@ const returnUserAbsenceInfo = ( (request.endTime.getTime() - request.startTime.getTime()) / MMSTOHRS; }); - relatedAbsences.requestsWorkedAbroad.forEach(request => { + relatedAbsences.requestsWorkedAbroad.forEach((request) => { if (request.totalHoursOfAbsence) { totalHoursWorkedAbroad += parseFloat(request.totalHoursOfAbsence); return; } const absenceType = relatedAbsenceTypes.find( - absType => absType._id === request.absenceTypeId + (absType) => absType._id === request.absenceTypeId, ); if (absenceType && absenceType.requestTimeType === 'by day') { @@ -1296,7 +1303,7 @@ const returnUserAbsenceInfo = ( ? request.requestDates.length : Math.ceil( (request.endTime.getTime() - request.startTime.getTime()) / - MMSTODAYS + MMSTODAYS, ); totalHoursWorkedAbroad += getTotalDays * absenceType.requestHoursPerDay; @@ -1307,9 +1314,9 @@ const returnUserAbsenceInfo = ( (request.endTime.getTime() - request.startTime.getTime()) / MMSTOHRS; }); - relatedAbsences.requestsPaidAbsence.forEach(request => { + relatedAbsences.requestsPaidAbsence.forEach((request) => { const absenceType = relatedAbsenceTypes.find( - absType => absType._id === request.absenceTypeId + (absType) => absType._id === request.absenceTypeId, ); if (absenceType && absenceType.requestTimeType === 'by day') { @@ -1317,7 +1324,7 @@ const returnUserAbsenceInfo = ( ? request.requestDates.length : Math.ceil( (request.endTime.getTime() - request.startTime.getTime()) / - (1000 * 3600 * 24) + (1000 * 3600 * 24), ); totalHoursPaidAbsence += getTotalDays * absenceType.requestHoursPerDay; return; @@ -1326,14 +1333,14 @@ const returnUserAbsenceInfo = ( (request.endTime.getTime() - request.startTime.getTime()) / MMSTOHRS; }); - relatedAbsences.requestsUnpaidAbsence.forEach(request => { + relatedAbsences.requestsUnpaidAbsence.forEach((request) => { if (request.totalHoursOfAbsence) { totalHoursUnpaidAbsence += parseFloat(request.totalHoursOfAbsence); return; } const absenceType = relatedAbsenceTypes.find( - absType => absType._id === request.absenceTypeId + (absType) => absType._id === request.absenceTypeId, ); if (absenceType && absenceType.requestTimeType === 'by day') { @@ -1341,7 +1348,7 @@ const returnUserAbsenceInfo = ( ? request.requestDates.length : Math.ceil( (request.endTime.getTime() - request.startTime.getTime()) / - MMSTODAYS + MMSTODAYS, ); totalHoursUnpaidAbsence += getTotalDays * absenceType.requestHoursPerDay; @@ -1352,14 +1359,14 @@ const returnUserAbsenceInfo = ( (request.endTime.getTime() - request.startTime.getTime()) / MMSTOHRS; }); - relatedAbsences.requestsSick.forEach(request => { + relatedAbsences.requestsSick.forEach((request) => { if (request.totalHoursOfAbsence) { totalHoursSick += parseFloat(request.totalHoursOfAbsence); return; } const absenceType = relatedAbsenceTypes.find( - absType => absType._id === request.absenceTypeId + (absType) => absType._id === request.absenceTypeId, ); if (absenceType && absenceType.requestTimeType === 'by day') { @@ -1367,7 +1374,7 @@ const returnUserAbsenceInfo = ( ? request.requestDates.length : Math.ceil( (request.endTime.getTime() - request.startTime.getTime()) / - MMSTODAYS + MMSTODAYS, ); totalHoursSick += getTotalDays * absenceType.requestHoursPerDay; @@ -1383,17 +1390,14 @@ const returnUserAbsenceInfo = ( totalHoursWorkedAbroad, totalHoursPaidAbsence, totalHoursUnpaidAbsence, - totalHoursSick + totalHoursSick, }; }; const returnOvernightHours = (shiftStart: Date, shiftEnd: Date) => { // check whether shift is between 22:00 - 06:00, if so return how many hours is overnight const shiftDay = shiftStart.toLocaleDateString(); - const nextDay = dayjs(shiftDay) - .add(1, 'day') - .toDate() - .toLocaleDateString(); + const nextDay = dayjs(shiftDay).add(1, 'day').toDate().toLocaleDateString(); const overnightStart = dayjs(shiftDay + ' ' + '22:00:00').toDate(); const overnightEnd = dayjs(nextDay + ' ' + '06:00:00').toDate(); @@ -1419,13 +1423,13 @@ const createSchedulesObj = ( userIds: string[], totalSchedules: IScheduleDocument[], totalScheduleShifts: IShiftDocument[], - scheduleShiftsConfigsMap?: { [scheduleConfigId: string]: number } + scheduleShiftsConfigsMap?: { [scheduleConfigId: string]: number }, ) => { const returnObject = {}; for (const userId of userIds) { const currEmpSchedules = totalSchedules.filter( - schedule => schedule.userId === userId + (schedule) => schedule.userId === userId, ); if (currEmpSchedules.length) { @@ -1433,10 +1437,10 @@ const createSchedulesObj = ( } for (const empSchedule of currEmpSchedules) { const currEmpScheduleShifts = totalScheduleShifts.filter( - scheduleShift => scheduleShift.scheduleId === empSchedule._id + (scheduleShift) => scheduleShift.scheduleId === empSchedule._id, ); - currEmpScheduleShifts.forEach(currEmpScheduleShift => { + currEmpScheduleShifts.forEach((currEmpScheduleShift) => { const date_key = currEmpScheduleShift.shiftStart?.toLocaleDateString(); const getScheduleConfigId = currEmpScheduleShift.scheduleConfigId; @@ -1449,7 +1453,7 @@ const createSchedulesObj = ( returnObject[userId][date_key] = { lunchBreakInMins, shiftStart: currEmpScheduleShift.shiftStart, - shiftEnd: currEmpScheduleShift.shiftEnd + shiftEnd: currEmpScheduleShift.shiftEnd, }; }); } @@ -1457,3 +1461,132 @@ const createSchedulesObj = ( return returnObject; }; +export const findTimeclockTeamMemberIds = async ( + models: any, + startDate: Date, + endDate: Date, +) => { + const timeclockUserIds = await models.Timeclocks.find({ + shiftStart: { + $gte: fixDate(startDate), + $lte: customFixDate(endDate), + }, + }).distinct('userId'); + + const requestsUserIds = await models.Absences.find({ + solved: true, + status: /approved/gi, + startTime: { + $gte: fixDate(startDate), + $lte: customFixDate(endDate), + }, + }).distinct('userId'); + + const scheduleIds = await models.Shifts.find({ + status: 'Approved', + shiftStart: { + $gte: fixDate(startDate), + $lte: customFixDate(endDate), + }, + shiftEnd: { + $gte: fixDate(startDate), + $lte: customFixDate(endDate), + }, + }).distinct('scheduleId'); + + const scheduleUserIds = await models.Schedules.find({ + _id: { $in: scheduleIds }, + }).distinct('userId'); + + const allUserIds = Array.from( + new Set([...timeclockUserIds, ...requestsUserIds, ...scheduleUserIds]), + ); + + return allUserIds; +}; + +export const timeclockReportByUsers = async ( + userIds: string[], + models: IModels, + queryParams: any, +): Promise => { + const returnReport: any[] = []; + + const { startDate, endDate } = queryParams; + + const schedules = await models.Schedules.find({ + userId: { $in: userIds }, + solved: true, + status: /approved/gi, + }); + + // find total Timeclocks + const timeclocks = await models.Timeclocks.find({ + $and: [ + { userId: { $in: userIds } }, + { + shiftStart: { + $gte: fixDate(startDate), + $lte: customFixDate(endDate), + }, + }, + ], + }).sort({ userId: 1 }); + + // get all approved absence requests + const requests = await models.Absences.find({ + userId: { $in: userIds }, + solved: true, + status: /approved/gi, + $or: [ + { + startTime: { + $gte: fixDate(startDate), + $lte: customFixDate(endDate), + }, + }, + { + endTime: { + $gte: fixDate(startDate), + $lte: customFixDate(endDate), + }, + }, + ], + }); + + const absTypeObject: { [id: string]: any } = {}; + + const absenceTypes = await models.AbsenceTypes.find({ + _id: { $in: requests.map((r) => r.absenceTypeId) }, + }); + + for (const absType of absenceTypes) { + absTypeObject[absType._id] = absType.requestType; + } + + const requestsFiltered: any = []; + for (const request of requests) { + requestsFiltered.push({ + absenceType: absTypeObject[request.absenceTypeId || ''], + absenceTimeType: request.absenceTimeType, + requestDates: request.requestDates, + userId: request.userId, + solved: request.solved, + reason: request.reason, + totalHoursOfAbsence: request.totalHoursOfAbsence, + startTime: request.startTime, + endTime: request.endTime, + }); + } + + for (const userId of userIds) { + returnReport.push({ + userId, + requests: requestsFiltered.filter((request) => request.userId === userId), + schedules: schedules.filter((schedule) => schedule.userId === userId), + timeclocks: timeclocks.filter((t) => t.userId === userId), + }); + } + + return returnReport; +}; diff --git a/packages/plugin-timeclock-api/src/graphql/schema.ts b/packages/plugin-timeclock-api/src/graphql/schema.ts index 8251a8403a6..b50befe9964 100644 --- a/packages/plugin-timeclock-api/src/graphql/schema.ts +++ b/packages/plugin-timeclock-api/src/graphql/schema.ts @@ -1,6 +1,6 @@ import { attachmentType, - attachmentInput + attachmentInput, } from '@erxes/api-utils/src/commonTypeDefs'; export const types = ` @@ -184,8 +184,9 @@ export const types = ` absenceInfo: IUserAbsenceInfo scheduledShifts: [Shift] + schedules: [Schedule] timeclocks: [Timeclock] - requests: [Absence] + requests: [JSON] totalHoursWorkedSelectedDay: Float totalHoursScheduledSelectedDay: Float @@ -298,6 +299,11 @@ export const types = ` list: [DeviceConfig] totalCount: Float } + + type TimeclockReportByUsersListResponse { + list: [UserReport] + totalCount: Int + } `; const timeclockParams = ` @@ -382,7 +388,7 @@ export const queries = ` absenceTypes:[AbsenceType] timeclockReports(${queryParams}): TimeclockReportsListResponse - + timeclockReportByUsers(${queryParams}):TimeclockReportByUsersListResponse timeclockReportByUser(selectedUser: String, selectedMonth: String, selectedYear: String, selectedDate:String): UserReport diff --git a/packages/plugin-timeclock-api/src/models/definitions/timeclock.ts b/packages/plugin-timeclock-api/src/models/definitions/timeclock.ts index 7e400e1407b..af22557668f 100644 --- a/packages/plugin-timeclock-api/src/models/definitions/timeclock.ts +++ b/packages/plugin-timeclock-api/src/models/definitions/timeclock.ts @@ -1,5 +1,6 @@ import { Document, Schema } from 'mongoose'; import { field } from './utils'; +import { IUserDocument } from '@erxes/api-utils/src/types'; export interface ITimeClock { userId: string; @@ -169,9 +170,9 @@ export const attachmentSchema = new Schema( url: field({ type: String }), type: field({ type: String }), size: field({ type: Number, optional: true }), - duration: field({ type: Number, optional: true }) + duration: field({ type: Number, optional: true }), }, - { _id: false } + { _id: false }, ); export const timeLogSchema = new Schema({ @@ -180,9 +181,9 @@ export const timeLogSchema = new Schema({ deviceSerialNo: field({ type: String, label: 'Terminal device serial number', - optional: true + optional: true, }), - timelog: field({ type: Date, label: 'Shift starting time', index: true }) + timelog: field({ type: Date, label: 'Shift starting time', index: true }), }); timeLogSchema.index({ userId: 1, timelog: 1, deviceSerialNo: 1 }); @@ -195,52 +196,52 @@ export const timeclockSchema = new Schema({ shiftActive: field({ type: Boolean, label: 'Is shift started and active', - default: false + default: false, }), shiftNotClosed: field({ type: Boolean, label: 'Whether shift was not closed by user', - optional: true + optional: true, }), branchName: field({ type: String, - label: 'Name of branch where user clocked in / out' + label: 'Name of branch where user clocked in / out', }), deviceName: field({ type: String, - label: 'Device name, which user used to clock in / out ' + label: 'Device name, which user used to clock in / out ', }), deviceType: field({ type: String, - label: 'Which device used for clock in/out' + label: 'Which device used for clock in/out', }), inDevice: field({ type: String, label: 'check in device name', - optional: true + optional: true, }), outDevice: field({ type: String, label: 'check out device name', - optional: true + optional: true, }), inDeviceType: field({ type: String, label: 'check in device type', - optional: true + optional: true, }), outDeviceType: field({ type: String, label: 'check out device type', - optional: true - }) + optional: true, + }), }); timeclockSchema.index({ userId: 1, shiftStart: 1, shiftEnd: 1, - shiftActive: 1 + shiftActive: 1, }); export const absenceTypeSchema = new Schema({ @@ -250,20 +251,20 @@ export const absenceTypeSchema = new Schema({ requestTimeType: field({ type: String, label: 'Either by day or by hours' }), requestHoursPerDay: field({ type: Number, - label: 'Hours per day if requestTimeType is by day' + label: 'Hours per day if requestTimeType is by day', }), explRequired: field({ type: Boolean, - label: 'whether absence type requires explanation' + label: 'whether absence type requires explanation', }), attachRequired: field({ type: Boolean, - label: 'whether absence type requires attachment' + label: 'whether absence type requires attachment', }), shiftRequest: field({ type: Boolean, - label: 'whether absence type is shift request' - }) + label: 'whether absence type is shift request', + }), }); absenceTypeSchema.index({ name: 1, requestType: 1 }); @@ -279,50 +280,50 @@ export const absenceSchema = new Schema({ requestDates: field({ type: [String], - label: 'Requested dates in string format' + label: 'Requested dates in string format', }), reason: field({ type: String, label: 'reason for absence' }), explanation: field({ type: String, label: 'explanation by a team member', - optional: true + optional: true, }), solved: field({ type: Boolean, default: false, - label: 'whether absence request is solved or pending' + label: 'whether absence request is solved or pending', }), attachment: field({ type: attachmentSchema, label: 'Attachment', - optional: true + optional: true, }), status: field({ type: String, - label: 'Status of absence request, whether approved or rejected' + label: 'Status of absence request, whether approved or rejected', }), checkInOutRequest: field({ type: Boolean, - label: 'Whether request is check in/out request' + label: 'Whether request is check in/out request', }), absenceTypeId: field({ type: String, - label: 'id of an absence type' + label: 'id of an absence type', }), absenceTimeType: field({ type: String, default: 'by hour', - label: 'absence time type either by day or by hour' + label: 'absence time type either by day or by hour', }), totalHoursOfAbsence: field({ type: String, - label: 'total hours of absence request' - }) + label: 'total hours of absence request', + }), }); absenceSchema.index({ userId: 1, startTime: 1, endTime: 1 }); @@ -333,36 +334,36 @@ export const scheduleSchema = new Schema({ solved: field({ type: Boolean, default: false, - label: 'whether schedule request is solved or pending' + label: 'whether schedule request is solved or pending', }), status: field({ type: String, - label: 'Status of schedule request, whether approved or rejected' + label: 'Status of schedule request, whether approved or rejected', }), scheduleConfigId: field({ type: String, - label: 'Schedule Config id used for reports' + label: 'Schedule Config id used for reports', }), scheduleChecked: field({ type: Boolean, label: 'Whether schedule is checked by employee', - default: false + default: false, }), submittedByAdmin: field({ type: Boolean, label: 'Whether schedule was submitted/assigned directly by an admin', - default: false + default: false, }), totalBreakInMins: field({ type: Number, - label: 'Total break time in mins' + label: 'Total break time in mins', }), createdByRequest: field({ type: Boolean, label: 'Whether schedule was created by shift request', default: false, - optional: true - }) + optional: true, + }), }); scheduleSchema.index({ userId: 1, solved: 1, status: 1 }); @@ -372,51 +373,51 @@ export const scheduleShiftSchema = new Schema({ scheduleId: field({ type: String, label: 'id of an according schedule' }), scheduleConfigId: field({ type: String, - label: 'id of an according schedule config' + label: 'id of an according schedule config', }), configName: field({ type: String, - label: 'name of schedule config' + label: 'name of schedule config', }), configShiftStart: field({ type: String, - label: 'starting time of config day shift' + label: 'starting time of config day shift', }), configShiftEnd: field({ type: String, - label: 'ending time of config day shift' + label: 'ending time of config day shift', }), overnightShift: field({ type: Boolean, - label: 'to be sure of whether shift occurs overnight' + label: 'to be sure of whether shift occurs overnight', }), lunchBreakInMins: field({ type: Number, - label: 'lunch break of the shift' + label: 'lunch break of the shift', }), chosenScheduleConfigId: field({ type: String, - label: '_id of a chosen schedule config when creating schedule' + label: '_id of a chosen schedule config when creating schedule', }), solved: field({ type: Boolean, default: false, - label: 'whether shift is solved or pending' + label: 'whether shift is solved or pending', }), status: field({ type: String, - label: 'Status of shift request, whether approved or rejected' + label: 'Status of shift request, whether approved or rejected', }), shiftStart: field({ type: Date, - label: 'starting date and time of the shift' + label: 'starting date and time of the shift', }), - shiftEnd: field({ type: Date, label: 'ending date and time of the shift' }) + shiftEnd: field({ type: Date, label: 'ending date and time of the shift' }), }); export const payDateSchema = new Schema({ _id: field({ pkey: true }), - payDates: field({ type: [Number], label: 'pay dates' }) + payDates: field({ type: [Number], label: 'pay dates' }), }); export const scheduleConfigSchema = new Schema({ @@ -424,21 +425,21 @@ export const scheduleConfigSchema = new Schema({ scheduleName: field({ type: String, label: 'Name of the schedule', - index: true + index: true, }), lunchBreakInMins: field({ type: Number, label: 'Lunch break in mins', - default: 30 + default: 30, }), shiftStart: field({ type: String, - label: 'starting time of shift' + label: 'starting time of shift', }), shiftEnd: field({ type: String, - label: 'ending time of shift' - }) + label: 'ending time of shift', + }), }); export const deviceConfigSchema = new Schema({ @@ -447,12 +448,12 @@ export const deviceConfigSchema = new Schema({ serialNo: field({ type: String, label: 'Serial number of the device', - index: true + index: true, }), extractRequired: field({ type: Boolean, - label: 'whether extract from the device' - }) + label: 'whether extract from the device', + }), }); deviceConfigSchema.index({ serialNo: 1 }); @@ -463,15 +464,15 @@ export const reportCheckSchema = new Schema({ startDate: field({ type: String, label: 'Start date of report' }), endDate: field({ type: String, - label: 'End date of report' - }) + label: 'End date of report', + }), }); export const scheduleConfigOrderItemSchema = new Schema({ scheduleConfigId: field({ type: String, index: true }), pinned: field({ type: Boolean, default: false }), order: field({ type: Number, index: true }), - label: field({ type: String, label: 'startTime ~ endTime (scheduleName)' }) + label: field({ type: String, label: 'startTime ~ endTime (scheduleName)' }), }); export const scheduleConfigOrderSchema = new Schema({ @@ -480,12 +481,12 @@ export const scheduleConfigOrderSchema = new Schema({ type: String, label: 'User of the report', unique: true, - index: true + index: true, }), orderedList: field({ type: [scheduleConfigOrderItemSchema], - label: 'personalized order of schedule configs' - }) + label: 'personalized order of schedule configs', + }), }); // common types export interface IScheduleReport { @@ -614,3 +615,7 @@ export interface IReport { groupTotalAbsenceMins?: number; groupTotalMinsScheduled?: number; } + +export interface ITeamMembersObj { + [userId: string]: IUserDocument; +} diff --git a/packages/plugin-timeclock-api/src/timeclockExport.ts b/packages/plugin-timeclock-api/src/timeclockExport.ts new file mode 100644 index 00000000000..11c1453135b --- /dev/null +++ b/packages/plugin-timeclock-api/src/timeclockExport.ts @@ -0,0 +1,496 @@ +import * as dayjs from 'dayjs'; +import * as xlsxPopulate from 'xlsx-populate'; +import { IModels } from './connectionResolver'; +import { TIMECLOCK_EXPORT_COLUMNS } from './constants'; +import { timeclockReportByUsers } from './graphql/resolvers/utils'; +import { ITeamMembersObj, IUserReport } from './models/definitions/timeclock'; +import { + createTeamMembersObjectWithFullName, + findTeamMember, + findTeamMembers, + generateCommonUserIds, + getNextNthColumnChar, + returnSupervisedUsers, +} from './utils'; + +import { + findTimeclockTeamMemberIds, + paginateArray, +} from '../src/graphql/resolvers/utils'; + +type Column = { + dateField: string; + text: string; + date: Date; +}; + +const daysAndDatesHeaders: { [dateField: string]: Column } = {}; +const dateFormat = 'YYYY.MM.DD'; +const dayOfTheWeekFormat = 'dd'; +const dateOfTheMonthFormat = 'MM/DD'; +const timeFormat = 'HH:mm'; + +/** + * Creates blank workbook + */ +export const createXlsFile = async () => { + // Generating blank workbook + const workbook = await xlsxPopulate.fromBlankAsync(); + + return { workbook, sheet: workbook.sheet(0) }; +}; + +/** + * Generates downloadable xls file on the url + */ +export const generateXlsx = async (workbook: any): Promise => { + return workbook.outputAsync(); +}; + +const addIntoSheet = async ( + values: any, + startRowIdx: string, + endRowIdx: string, + sheet: any, + merged?: boolean, + customStyles?: any, +) => { + const r = sheet.range(`${startRowIdx}:${endRowIdx}`); + + r.style('horizontalAlignment', 'center'); + + if (merged) { + r.style({ horizontalAlignment: 'center', verticalAlignment: 'center' }); + r.merged(true); + r.style('bold', true); + } + + if (customStyles) { + for (const cStyle of customStyles) { + r.style(cStyle.style, cStyle.value); + } + } + + r.value(values); +}; + +const prepareHeader = async (sheet: any, startDate: Date, endDate: Date) => { + const timeclock_headers = [...TIMECLOCK_EXPORT_COLUMNS]; + + let column_start = 'A'; + let column_end = 'A'; + + let total_columns = 0; + + let startRange = dayjs(startDate); + const endRange = dayjs(endDate); + + const days: string[] = []; + const dates: string[] = []; + + for (const header of timeclock_headers) { + total_columns += header[1].length; + column_end = getNextNthColumnChar(column_start, header[1].length - 1); + + if (!header[0][0].length) { + addIntoSheet( + [header[1]], + `${column_start}1`, + `${column_end}2`, + sheet, + true, + ); + column_start = getNextNthColumnChar(column_end, 1); + continue; + } + + addIntoSheet( + [header[0]], + `${column_start}1`, + `${column_end}1`, + sheet, + true, + ); + addIntoSheet([header[1]], `${column_start}2`, `${column_end}2`, sheet); + column_start = getNextNthColumnChar(column_end, 1); + } + + while (startRange <= endRange) { + days.push(startRange.format(dayOfTheWeekFormat)); + dates.push(startRange.format(dateOfTheMonthFormat)); + + const dateField = startRange.format(dateOfTheMonthFormat); + + daysAndDatesHeaders[dateField] = { + dateField, + text: startRange.format(dateOfTheMonthFormat), + date: startRange.toDate(), + }; + + total_columns += 1; + startRange = startRange.add(1, 'day'); + } + + column_end = getNextNthColumnChar(column_start, days.length - 1); + + addIntoSheet([days], `${column_start}1`, `${column_end}1`, sheet, false); + addIntoSheet([dates], `${column_start}2`, `${column_end}2`, sheet, false, [ + { + style: 'bold', + value: true, + }, + ]); + + sheet.column('B').width(50); + sheet.column('C').width(20); + + return total_columns; +}; + +const changeColumnRangeWidths = async ( + sheet: any, + colStart: string, + colEnd: string, + width: number, +) => { + let startRange = colStart; + + while (startRange !== colEnd) { + sheet.column(startRange).width(width); + startRange = getNextNthColumnChar(startRange, 1); + } + + sheet.column(colEnd).width(width); +}; + +const extractAndAddIntoSheet = async ( + models: any, + sheet: any, + empReports: any[], + teamMembersObj: { [userId: string]: any }, + total_columns: number, +) => { + const rowStartIdx = 3; + const colStart = 'A'; + let dataIdx = 1; + + const totalRowsData: any = []; + + for (const empReport of empReports) { + const { userId, requests, timeclocks, schedules } = empReport; + + const timeclocksInfo: any = {}; + const requestsInfo: any = {}; + const scheduleShiftsInfo: any = {}; + + // const scheduleShifts: IShift[] = []; + const getUserInfo: { + fullName: string; + position: string; + employeeId: string; + } = teamMembersObj[userId || '']; + + const rowData: any = []; + + if (getUserInfo) { + const { fullName, employeeId, position } = getUserInfo; + rowData.push(dataIdx, fullName, employeeId); + dataIdx += 1; + } + + // scheduled: true + if (schedules?.length) { + const scheduleIds = schedules.map((schedule: any) => schedule._id); + const scheduleShifts = await models.Shifts.find({ + scheduleId: { $in: scheduleIds }, + }); + + for (const scheduleShift of scheduleShifts) { + const dateField = dayjs(scheduleShift.shiftStart).format( + dateOfTheMonthFormat, + ); + + scheduleShiftsInfo[dateField] = { scheduled: true }; + } + } + + if (timeclocks?.length) { + for (const timeclock of timeclocks) { + // prevent showing duplicate timeclocks created by shift request + if ( + timeclock.deviceType && + timeclock.deviceType.toLocaleLowerCase() === 'shift request' + ) { + continue; + } + + const dateField = dayjs(timeclock.shiftStart).format( + dateOfTheMonthFormat, + ); + + const shiftStart = dayjs(timeclock.shiftStart).format(timeFormat); + const shiftEnd = timeclock.shiftEnd + ? dayjs(timeclock.shiftEnd).format(timeFormat) + : ''; + // if multiple shifts on single day + if (dateField in timeclocksInfo) { + const prevTimeclock = timeclocksInfo[dateField]; + timeclocksInfo[dateField] = [ + { + _id: timeclock._id, + shiftStart, + shiftEnd, + shiftNotClosed: timeclock.shiftNotClosed, + shiftActive: timeclock.shiftActive || !timeclock.shiftEnd, + }, + ...prevTimeclock, + ]; + + continue; + } + + timeclocksInfo[dateField] = [ + { + _id: timeclock._id, + shiftStart, + shiftEnd, + shiftNotClosed: timeclock.shiftNotClosed, + deviceType: timeclock.deviceType, + shiftActive: timeclock.shiftActive || !timeclock.shiftEnd, + }, + ]; + } + } + + if (requests?.length) { + for (const request of requests) { + const { absenceTimeType, reason } = request; + + const lowerCasedReason = reason.toLocaleLowerCase(); + // dont show check in | check out request + if ( + lowerCasedReason.includes('check in') || + lowerCasedReason.includes('check out') + ) { + continue; + } + + if (absenceTimeType === 'by day') { + const abseneDurationPerDay: string = + ( + parseFloat(request.totalHoursOfAbsence) / + request.requestDates.length + ).toFixed(1) + ' hours'; + + for (const requestDate of request.requestDates) { + const date = dayjs(new Date(requestDate)).format( + dateOfTheMonthFormat, + ); + + // if multiple requests per day + if (date in requestsInfo) { + requestsInfo[date].push({ + reason: request.reason, + absenceDuration: abseneDurationPerDay, + }); + + continue; + } + + requestsInfo[date] = [ + { + reason: request.reason, + absenceDuration: abseneDurationPerDay, + }, + ]; + } + + continue; + } + + const absenceDuration: string = + dayjs(request.startTime).format(timeFormat) + + '~' + + dayjs(request.endTime).format(timeFormat); + + // by hour + const dateField = dayjs(request.startTime).format(dateOfTheMonthFormat); + + if (dateField in requestsInfo) { + requestsInfo[dateField].push({ + reason: request.reason, + absenceDuration, + }); + continue; + } + + requestsInfo[dateField] = [ + { + reason: request.reason, + absenceDuration, + }, + ]; + } + } + + const timeclockData: string[] = []; + + for (const dateField of Object.keys(daysAndDatesHeaders)) { + const contentInsideCell: string[] = []; + let emptyCell = true; + + const getDate = new Date( + new Date(dateField).setFullYear(new Date().getFullYear()), + ); + + // absent day + if ( + !timeclocksInfo[dateField] && + !requestsInfo[dateField] && + scheduleShiftsInfo[dateField] && + getDate.getTime() < new Date().getTime() + ) { + contentInsideCell.push('Absent'); + emptyCell = false; + } + + // add timeclock content + if (dateField in timeclocksInfo) { + for (const timeclock of timeclocksInfo[dateField]) { + contentInsideCell.push( + `${timeclock.shiftStart} ~ ${ + timeclock.shiftActive ? 'A' : timeclock.shiftEnd + }`, + ); + } + emptyCell = false; + } + + // add request content + if (requestsInfo[dateField]) { + requestsInfo[dateField].map((request) => { + contentInsideCell.push( + `${request.reason}\n${request.absenceDuration}`, + ); + }); + emptyCell = false; + } + + sheet.row(rowStartIdx + dataIdx).style('verticalAlignment', 'center'); + sheet.row(rowStartIdx + dataIdx).height(contentInsideCell.length * 60); + timeclockData.push(emptyCell ? '-' : contentInsideCell.join('\n')); + } + + if (timeclockData.length) { + rowData.push(...timeclockData); + } + + totalRowsData.push(rowData); + } + + const rowEndIdx = rowStartIdx + dataIdx; + const colEnd = getNextNthColumnChar(colStart, total_columns - 1); + addIntoSheet( + totalRowsData, + `${colStart}${rowStartIdx}`, + `${colEnd}${rowEndIdx}`, + sheet, + false, + ); + + changeColumnRangeWidths(sheet, 'D', colEnd, 20); +}; + +export const buildFile = async ( + models: IModels, + subdomain: string, + params: any, +) => { + const { + currentUserId, + isCurrentUserAdmin, + page, + perPage, + startDate, + endDate, + } = params; + + const userIds = + params.userIds instanceof Array || !params.userIds + ? params.userIds + : [params.userIds]; + + const branchIds = + params.branchIds instanceof Array || !params.branchIds + ? params.branchIds + : [params.branchIds]; + const departmentIds = + params.departmentIds instanceof Array || !params.departmentIds + ? params.departmentIds + : [params.departmentIds]; + + const currentUser = await findTeamMember(subdomain, currentUserId); + + const startDateFormatted = dayjs(startDate).format(dateFormat); + const endDateFormatted = dayjs(endDate).format(dateFormat); + + const { workbook, sheet } = await createXlsFile(); + + let filterGiven = false; + let totalTeamMemberIds; + let totalMembers; + + if (userIds || branchIds || departmentIds) { + filterGiven = true; + } + + if (filterGiven) { + totalTeamMemberIds = await generateCommonUserIds( + subdomain, + userIds, + branchIds, + departmentIds, + ); + + totalMembers = await findTeamMembers(subdomain, totalTeamMemberIds); + } else { + // return supervisod users including current user + if (isCurrentUserAdmin) { + // return all team member ids + totalTeamMemberIds = await findTimeclockTeamMemberIds( + models, + startDate, + endDate, + ); + totalMembers = await findTeamMembers(subdomain, totalTeamMemberIds); + } else { + // return supervisod users including current user + totalMembers = await returnSupervisedUsers(currentUser, subdomain); + totalTeamMemberIds = totalMembers.map((usr) => usr._id); + } + } + + const teamMembersObject: ITeamMembersObj = + await createTeamMembersObjectWithFullName(subdomain, totalTeamMemberIds); + + const report = await timeclockReportByUsers( + paginateArray(totalTeamMemberIds, perPage, page), + models, + { startDate, endDate }, + ); + + const totalColumnsNum = await prepareHeader(sheet, startDate, endDate); + + await extractAndAddIntoSheet( + models, + sheet, + report, + teamMembersObject, + totalColumnsNum, + ); + + return { + name: `Timeclock-${startDateFormatted}-${endDateFormatted}`, + response: await generateXlsx(workbook), + }; +}; diff --git a/packages/plugin-timeclock-api/src/utils.ts b/packages/plugin-timeclock-api/src/utils.ts index daff5fde865..536f158693e 100644 --- a/packages/plugin-timeclock-api/src/utils.ts +++ b/packages/plugin-timeclock-api/src/utils.ts @@ -1090,6 +1090,35 @@ const createTeamMembersObject = async (subdomain: any, userIds: string[]) => { return teamMembersObject; }; +const createTeamMembersObjectWithFullName = async ( + subdomain: any, + userIds: string[], +) => { + const teamMembersObject = {}; + + const teamMembers = await sendCoreMessage({ + subdomain, + action: 'users.find', + data: { + query: { _id: { $in: userIds }, isActive: true }, + }, + isRPC: true, + defaultValue: [], + }); + + for (const teamMember of teamMembers) { + teamMembersObject[teamMember._id] = { + employeeId: teamMember.employeeId, + fullName: `${teamMember.details.lastName?.charAt(0)}.${ + teamMember.details.firstName + }`, + position: teamMember.details.position, + }; + } + + return teamMembersObject; +}; + const returnDepartmentsBranchesDict = async ( subdomain: any, branchIds: string[], @@ -1531,6 +1560,42 @@ const findUnfinishedShiftsAndUpdate = async (subdomain: any) => { return result; }; +const getNextNthColumnChar = (currentChar, n: number) => { + // Convert currentChar to uppercase for consistency + currentChar = currentChar.toUpperCase(); + + // Function to convert a number to a base-26 string representation + function toBase26String(num) { + let result = ''; + while (num > 0) { + let remainder = (num - 1) % 26; // Adjusting the remainder to 0-25 range + result = String.fromCharCode(remainder + 'A'.charCodeAt(0)) + result; + num = Math.floor((num - 1) / 26); // Update num for the next iteration + } + return result; + } + + // Function to convert a base-26 string to a number + function fromBase26String(str) { + let result = 0; + for (let i = 0; i < str.length; i++) { + result = result * 26 + (str.charCodeAt(i) - 'A'.charCodeAt(0) + 1); + } + return result; + } + + // Convert the currentChar to its numeric equivalent in the base-26 system + const currentColumnNumber = fromBase26String(currentChar); + + // Calculate the next column number by adding n to the current column number + const nextColumnNumber = currentColumnNumber + n; + + // Convert the nextColumnNumber back to its corresponding character representation + const nextColumnChar = toBase26String(nextColumnNumber); + + return nextColumnChar; +}; + export { connectAndQueryFromMsSql, connectAndQueryTimeLogsFromMsSql, @@ -1544,4 +1609,6 @@ export { findTeamMember, returnDepartmentsBranchesDict, findUnfinishedShiftsAndUpdate, + getNextNthColumnChar, + createTeamMembersObjectWithFullName, }; diff --git a/packages/plugin-timeclock-ui/src/components/List.tsx b/packages/plugin-timeclock-ui/src/components/List.tsx index 8cf9caca2cb..bf3f4ffd34e 100644 --- a/packages/plugin-timeclock-ui/src/components/List.tsx +++ b/packages/plugin-timeclock-ui/src/components/List.tsx @@ -5,7 +5,7 @@ import Wrapper from '@erxes/ui/src/layout/components/Wrapper'; import DataWithLoader from '@erxes/ui/src/components/DataWithLoader'; import SideBarList from '../containers/sidebar/SideBarList'; import ConfigList from '../containers/config/ConfigList'; -import TimeclockList from '../containers/timeclock/TimeclockList'; +import TimeclockList from '../containers/timeclock/TimeclockList2'; import AbsenceList from '../containers/absence/AbsenceList'; import ReportList from '../containers/report/ReportList'; import ScheduleList from '../containers/schedule/ScheduleList'; @@ -21,7 +21,7 @@ type Props = { departments: IDepartment[]; isCurrentUserAdmin: boolean; - isCurrentUserSupervisor?: boolean; + isCurrentUserSupervisor: boolean; currentDate?: string; queryParams: any; @@ -34,13 +34,8 @@ type Props = { }; function List(props: Props) { - const { - queryParams, - isCurrentUserAdmin, - history, - route, - searchFilter - } = props; + const { queryParams, isCurrentUserAdmin, history, route, searchFilter } = + props; const [showSideBar, setShowSideBar] = useState(true); const [rightActionBar, setRightActionBar] = useState(
); @@ -60,7 +55,7 @@ function List(props: Props) { getActionBar={setRightActionBar} queryParams={queryParams} history={history} - /> + />, ); } setLoading(false); @@ -75,7 +70,7 @@ function List(props: Props) { queryParams={queryParams} getPagination={setPagination} history={history} - /> + />, ); setLoading(false); break; @@ -88,7 +83,7 @@ function List(props: Props) { getActionBar={setRightActionBar} queryParams={queryParams} history={history} - /> + />, ); setLoading(false); break; @@ -101,7 +96,7 @@ function List(props: Props) { getActionBar={setRightActionBar} queryParams={queryParams} history={history} - /> + />, ); setLoading(false); break; @@ -115,7 +110,7 @@ function List(props: Props) { getActionBar={setRightActionBar} queryParams={queryParams} history={history} - /> + />, ); } setLoading(false); @@ -127,10 +122,9 @@ function List(props: Props) { showSideBar={setShowSideBar} getActionBar={setRightActionBar} getPagination={setPagination} - timeclockUser={queryParams.timeclockUser} history={history} queryParams={queryParams} - /> + />, ); setLoading(false); } diff --git a/packages/plugin-timeclock-ui/src/components/sidebar/SideBar.tsx b/packages/plugin-timeclock-ui/src/components/sidebar/SideBar.tsx index 8c0ef8f28a9..cb52de2e552 100644 --- a/packages/plugin-timeclock-ui/src/components/sidebar/SideBar.tsx +++ b/packages/plugin-timeclock-ui/src/components/sidebar/SideBar.tsx @@ -7,7 +7,7 @@ import { FlexRow, SidebarActions, SidebarHeader, - Trigger + Trigger, } from '../../styles'; import { CustomRangeContainer } from '../../styles'; import DateControl from '@erxes/ui/src/components/form/DateControl'; @@ -41,14 +41,14 @@ const LeftSideBar = (props: Props) => { queryParams, departments, currentUser, - isCurrentUserAdmin + isCurrentUserAdmin, } = props; const [currUserIds, setUserIds] = useState(queryParams.userIds); const [selectedBranches, setBranches] = useState(queryParams.branchIds); const [selectedDepartments, setDepartments] = useState( - queryParams.departmentIds + queryParams.departmentIds, ); const [isHovered, setIsHovered] = useState(false); @@ -65,7 +65,7 @@ const LeftSideBar = (props: Props) => { const dateOptions = [ { label: 'Today', value: 'today' }, { label: 'This Week', value: 'thisWeek' }, - { label: 'This Month', value: 'thisMonth' } + { label: 'This Month', value: 'thisMonth' }, ]; const returnTotalUserOptions = () => { @@ -88,14 +88,14 @@ const LeftSideBar = (props: Props) => { ? {} : { ids: returnTotalUserOptions(), - excludeIds: false + excludeIds: false, }; const [startDate, setStartDate] = useState( - queryParams.startDate || startOfThisMonth + queryParams.startDate || startOfThisMonth, ); const [endDate, setEndDate] = useState( - queryParams.endDate || startOfNextMonth + queryParams.endDate || startOfNextMonth, ); const cleanFilter = () => { @@ -110,7 +110,7 @@ const LeftSideBar = (props: Props) => { 'branchIds', 'startDate', 'endDate', - 'departmentIds' + 'departmentIds', ); removePageParams(); }; @@ -122,7 +122,7 @@ const LeftSideBar = (props: Props) => { const setParams = (key: string, value: any) => { if (value) { router.setParams(history, { - [key]: value + [key]: value, }); removePageParams(); @@ -137,57 +137,57 @@ const LeftSideBar = (props: Props) => { } const renderDepartmentOptions = (depts: IDepartment[]) => { - return depts.map(dept => ({ + return depts.map((dept) => ({ value: dept._id, label: dept.title, - userIds: dept.userIds + userIds: dept.userIds, })); }; const renderBranchOptions = (branchesList: IBranch[]) => { - return branchesList.map(branch => ({ + return branchesList.map((branch) => ({ value: branch._id, label: branch.title, - userIds: branch.userIds + userIds: branch.userIds, })); }; - const onBranchSelect = selectedBranch => { + const onBranchSelect = (selectedBranch) => { setBranches(selectedBranch); const selectedBranchIds: string[] = []; - selectedBranch.map(branch => { + selectedBranch.map((branch) => { selectedBranchIds.push(branch.value); }); setParams('branchIds', selectedBranchIds); }; - const onDepartmentSelect = selectedDepartment => { + const onDepartmentSelect = (selectedDepartment) => { setDepartments(selectedDepartment); const selectedDepartmentIds: string[] = []; - selectedDepartment.map(department => { + selectedDepartment.map((department) => { selectedDepartmentIds.push(department.value); }); setParams('departmentIds', selectedDepartmentIds); }; - const onMemberSelect = selectedUsers => { + const onMemberSelect = (selectedUsers) => { setUserIds(selectedUsers); setParams('userIds', selectedUsers); }; - const onStartDateChange = date => { + const onStartDateChange = (date) => { setStartDate(date); setParams('startDate', date); }; - const onEndDateChange = date => { + const onEndDateChange = (date) => { setEndDate(date); setParams('endDate', date); @@ -262,8 +262,6 @@ const LeftSideBar = (props: Props) => { }; const renderDateFilterMenu = () => { - console.log(dateOptions); - return ( { onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} > - {dateOptions.map(d => { + {dateOptions.map((d) => { return (
{ return ( - {/* - - - - */} {renderDateFilterMenu()}
Departments diff --git a/packages/plugin-timeclock-ui/src/components/timeclock/ActionBar.tsx b/packages/plugin-timeclock-ui/src/components/timeclock/ActionBar.tsx new file mode 100644 index 00000000000..a4eb7cad0a4 --- /dev/null +++ b/packages/plugin-timeclock-ui/src/components/timeclock/ActionBar.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { ColoredSquare, FlexColumn, FlexRowLeft } from '../../styles'; +import { COLORS } from '../../constants'; +import { FlexRow } from '@erxes/ui-settings/src/styles'; +import queryString from 'query-string'; +import { getEnv } from '@erxes/ui/src/utils'; +import Button from '@erxes/ui/src/components/Button'; + +type Props = { + queryParams: any; + currentUserId: string; + isCurrentUserAdmin: boolean; +}; + +const TimeclockActionBarLeft = ({ + queryParams, + currentUserId, + isCurrentUserAdmin, +}: Props) => { + const exportPage = () => { + const stringified = queryString.stringify({ + ...queryParams, + currentUserId, + isCurrentUserAdmin, + }); + + const { REACT_APP_API_URL } = getEnv(); + window.open( + `${REACT_APP_API_URL}/pl:timeclock/timeclock-export?${stringified}`, + ); + }; + + return ( + + + + + +
Paid Absence
+
+ + +
Unpaid Absence
+
+ + +
Shift request
+
+
+ + + +
Absent
+
+ + +
Shift active
+
+ + +
Shift not closed
+
+
+
+ +
+ +
+
+ ); +}; + +export default TimeclockActionBarLeft; diff --git a/packages/plugin-timeclock-ui/src/components/timeclock/TimeclockEditForm.tsx b/packages/plugin-timeclock-ui/src/components/timeclock/TimeclockEditForm.tsx new file mode 100644 index 00000000000..32a2c3c82c0 --- /dev/null +++ b/packages/plugin-timeclock-ui/src/components/timeclock/TimeclockEditForm.tsx @@ -0,0 +1,184 @@ +import React, { useState } from 'react'; +import Modal from 'react-bootstrap/Modal'; +import { ITimeclock } from '../../../src/types'; +import DateControl from '@erxes/ui/src/components/form/DateControl'; +import { ControlLabel, FormControl } from '@erxes/ui/src/components/form'; +import Button from '@erxes/ui/src/components/Button'; +import { Alert } from '@erxes/ui/src/utils'; +import { + CustomRangeContainer, + FlexCenter, + FlexColumn, + FlexRow, + FlexRowEven, +} from '../../styles'; + +type Props = { + timeclock: any; + timeclockEdit: (values: any) => void; + setShowModal: (value: boolean) => void; + showModal: boolean; +}; + +const TimeclockEditForm = ({ + timeclock, + timeclockEdit, + showModal, + setShowModal, +}: Props) => { + const [show, setShow] = useState(showModal); + + const [shiftStartInsert, setShiftStartInsert] = useState( + timeclock.shiftStart, + ); + + const [shiftEndInsert, setShiftEndInsert] = useState( + timeclock.shiftEnd || timeclock.shiftStart, + ); + + const [shiftEnded, setShiftEnded] = useState(!timeclock.shiftActive); + + const onShiftStartInsertChange = (date) => { + setShiftStartInsert(date); + }; + + const onShiftEndInsertChange = (date) => { + setShiftEndInsert(date); + }; + + const toggleShiftActive = (e) => { + setShiftEnded(e.target.checked); + }; + + const editTimeClock = () => { + const values = generateDoc(); + + if (checkInput()) { + timeclockEdit(values); + setShow(false); + } + }; + + const generateDoc = () => { + checkInput(); + + let outDeviceType; + let inDeviceType; + + if (shiftStartInsert !== timeclock.shiftStart) { + inDeviceType = 'edit'; + } + + if (!shiftEnded) { + return { + _id: timeclock._id, + shiftStart: shiftStartInsert, + shiftActive: true, + inDeviceType, + }; + } + + if (shiftEndInsert !== timeclock.shiftEnd) { + outDeviceType = 'edit'; + } + + return { + _id: timeclock._id, + shiftStart: shiftStartInsert, + shiftEnd: shiftEndInsert, + shiftActive: false, + inDeviceType, + outDeviceType, + }; + }; + + const checkInput = () => { + if ( + shiftEnded && + shiftStartInsert && + shiftEndInsert && + new Date(shiftEndInsert).getTime() < new Date(shiftStartInsert).getTime() + ) { + Alert.error('Shift end can not be sooner than shift start'); + return false; + } + + return true; + }; + + return ( + setShowModal(false)}> + + Edit Timeclock + + + + + In Device + Location + + +
{timeclock.inDeviceType}
+
{timeclock.inDevice}
+
+ + + Out Device + Location + + +
{timeclock.outDeviceType}
+
{timeclock.outDevice}
+
+ + + Shift Ended + + +
Ended
+
+
+ + + + + {shiftEnded && ( + <> + Shift End + + + + + + )} + + + +
+
+
+ ); +}; + +export default TimeclockEditForm; diff --git a/packages/plugin-timeclock-ui/src/components/timeclock/TimeclockList2.tsx b/packages/plugin-timeclock-ui/src/components/timeclock/TimeclockList2.tsx new file mode 100644 index 00000000000..ad55201fe4f --- /dev/null +++ b/packages/plugin-timeclock-ui/src/components/timeclock/TimeclockList2.tsx @@ -0,0 +1,562 @@ +import React, { useState } from 'react'; +import Table from '@erxes/ui/src/components/table'; +import { __ } from '@erxes/ui/src/utils'; +import * as dayjs from 'dayjs'; +import { + COLORS, + dateOfTheMonthFormat, + dayOfTheWeekFormat, + timeFormat, +} from '../../constants'; +import { + BorderedTd, + FlexRow, + FlexRowLeft, + RequestInfo, + TimeclockInfo, + TimeclockTableWrapper, + ToggleButton, +} from '../../styles'; +import { IShift, IUserReport } from '../../types'; + +import Tip from '@erxes/ui/src/components/Tip'; +import Pagination from '@erxes/ui/src/components/pagination/Pagination'; +import TimeclockEditForm from './TimeclockEditForm'; +import Wrapper from '@erxes/ui/src/layout/components/Wrapper'; +import Icon from '@erxes/ui/src/components/Icon'; +import ModalTrigger from '@erxes/ui/src/components/ModalTrigger'; +import TimeForm from '../../containers/timeclock/TimeFormList'; +import TimeclockActionBar from './ActionBar'; +import { IUser } from '@erxes/ui/src/auth/types'; +import Button from '@erxes/ui/src/components/Button'; + +type Props = { + reportByUsers: [IUserReport] | []; + totalCount: number; + queryParams: any; + history?: any; + + currentUser: IUser; + isCurrentUserAdmin: boolean; + isCurrentUserSupervisor: boolean; + + startClockTime?: (userId: string) => void; + + timeclockEdit: (values: any) => void; + getActionBar: (actionBar: any) => void; + showSideBar: (sideBar: boolean) => void; + getPagination: (pagination: any) => void; +}; + +const TimeclockList = (props: Props) => { + const { + reportByUsers, + queryParams, + getPagination, + totalCount, + timeclockEdit, + currentUser, + isCurrentUserAdmin, + startClockTime, + getActionBar, + showSideBar, + } = props; + const { startDate, endDate } = queryParams; + const [showModal, setShowModal] = useState(false); + const [editTimeclock, setEditTimeclock] = useState({}); + const [isSideBarOpen, setIsOpen] = useState( + localStorage.getItem('isSideBarOpen') === 'true' ? true : false, + ); + + let lastColumnIdx = 1; + + type Column = { + columnNo: number; + dateField: string; + text: string; + backgroundColor: string; + date?: Date; + }; + + const daysAndDatesHeaders: { [dateField: string]: Column } = {}; + + const onToggleSidebar = () => { + const toggleIsOpen = !isSideBarOpen; + setIsOpen(toggleIsOpen); + localStorage.setItem('isSideBarOpen', toggleIsOpen.toString()); + }; + + const prepareTableHeaders = () => { + let startRange = dayjs(startDate); + const endRange = dayjs(endDate); + + let columnNo = 1; + + while (startRange <= endRange) { + const backgroundColor = + startRange.toDate().getDay() === 0 || startRange.toDate().getDay() === 6 + ? COLORS.weekend + : COLORS.white; + + const dateField = startRange.format(dateOfTheMonthFormat); + + daysAndDatesHeaders[dateField] = { + columnNo, + dateField, + text: startRange.format(dateOfTheMonthFormat), + backgroundColor, + date: startRange.toDate(), + }; + + columnNo += 1; + startRange = startRange.add(1, 'day'); + } + + lastColumnIdx = columnNo; + }; + + const renderTableHeaders = () => { + prepareTableHeaders(); + return ( + + + + {''} + + + {__('Employee Id')} + + + {__('Team members')} + + + {Object.keys(daysAndDatesHeaders).map((dateField) => { + return ( + + {dayjs(daysAndDatesHeaders[dateField].date).format( + dayOfTheWeekFormat, + )} + + ); + })} + + + {Object.keys(daysAndDatesHeaders).map((dateField) => { + return ( + + {daysAndDatesHeaders[dateField].text} + + ); + })} + + + ); + }; + + const getAbsenceDayColor = (absenceType: string) => { + switch (absenceType) { + case 'paid absence': + return COLORS.paidAbsence; + case 'shift request': + return COLORS.shiftRequest; + case 'unpaid absence': + return COLORS.unpaidAbsence; + + default: + return COLORS.blank; + } + }; + + const renderEditForm = () => { + return ( + + ); + }; + + const renderUserReportRow = (userReport: IUserReport) => { + const { user, index, timeclocks, requests, schedules } = userReport; + + const timeclocksInfo: any = {}; + const requestsInfo: any = {}; + const scheduleShiftsInfo: any = {}; + const timeclocksObj: any = {}; + + type TimeclocksInfo = { + shiftStart: string; + shiftEnd?: string; + }; + type ShiftString = { + absent?: boolean; + shiftRequest?: boolean; + paidAbsence?: boolean; + unpaidAbsence?: boolean; + shiftNotClosed?: boolean; + + timeclockInfo?: [TimeclocksInfo]; + + backgroundColor?: string; + + requestStartTime?: string; + requestEndTime?: string; + + scheduled?: boolean; + timeclockExists?: boolean; + }; + + const renderUserInfo = ( + <> + {index} + {user.employeeId || '-'} + +
+ {`${ + user.details?.lastName ? user.details?.lastName.charAt(0) : '' + }.${user.details?.firstName ? user.details?.firstName : ''}` || '-'} +
+
+ {user.details?.position} +
+ + + ); + + const scheduleShifts: IShift[] = []; + + // scheduled: true + if (schedules?.length) { + for (const schedule of schedules) { + if (schedule.shifts?.length) { + scheduleShifts.push(...schedule.shifts); + } + } + + for (const scheduleShift of scheduleShifts) { + const dateField = dayjs(scheduleShift.shiftStart).format( + dateOfTheMonthFormat, + ); + + scheduleShiftsInfo[dateField] = { scheduled: true }; + } + } + + if (timeclocks?.length) { + for (const timeclock of timeclocks) { + // prevent showing duplicate timeclocks created by shift request + if ( + timeclock.deviceType && + timeclock.deviceType.toLocaleLowerCase() === 'shift request' + ) { + continue; + } + + timeclocksObj[timeclock._id] = timeclock; + + const dateField = dayjs(timeclock.shiftStart).format( + dateOfTheMonthFormat, + ); + + const shiftStart = dayjs(timeclock.shiftStart).format(timeFormat); + const shiftEnd = timeclock.shiftEnd + ? dayjs(timeclock.shiftEnd).format(timeFormat) + : ''; + // if multiple shifts on single day + if (dateField in timeclocksInfo) { + const prevTimeclock = timeclocksInfo[dateField]; + timeclocksInfo[dateField] = [ + { + _id: timeclock._id, + shiftStart, + shiftEnd, + shiftNotClosed: timeclock.shiftNotClosed, + shiftActive: timeclock.shiftActive || !timeclock.shiftEnd, + }, + ...prevTimeclock, + ]; + + continue; + } + + timeclocksInfo[dateField] = [ + { + _id: timeclock._id, + shiftStart, + shiftEnd, + shiftNotClosed: timeclock.shiftNotClosed, + deviceType: timeclock.deviceType, + shiftActive: timeclock.shiftActive || !timeclock.shiftEnd, + }, + ]; + } + } + + if (requests?.length) { + for (const request of requests) { + const { absenceTimeType, reason } = request; + + const lowerCasedReason = reason.toLocaleLowerCase(); + // dont show check in | check out request + if ( + lowerCasedReason.includes('check in') || + lowerCasedReason.includes('check out') + ) { + continue; + } + + if (absenceTimeType === 'by day') { + const abseneDurationPerDay: string = + ( + parseFloat(request.totalHoursOfAbsence) / + request.requestDates.length + ).toFixed(1) + ' hours'; + + for (const requestDate of request.requestDates) { + const date = dayjs(new Date(requestDate)).format( + dateOfTheMonthFormat, + ); + + // if multiple requests per day + if (date in requestsInfo) { + requestsInfo[date].push({ + reason: request.reason, + backgroundColor: getAbsenceDayColor(request.absenceType || ''), + absenceDuration: abseneDurationPerDay, + }); + + continue; + } + + requestsInfo[date] = [ + { + reason: request.reason, + backgroundColor: getAbsenceDayColor(request.absenceType || ''), + absenceDuration: abseneDurationPerDay, + }, + ]; + } + + continue; + } + + const absenceDuration: string = + dayjs(request.startTime).format(timeFormat) + + '~' + + dayjs(request.endTime).format(timeFormat); + + // by hour + const dateField = dayjs(request.startTime).format(dateOfTheMonthFormat); + + if (dateField in requestsInfo) { + requestsInfo[dateField].push({ + reason: request.reason, + backgroundColor: getAbsenceDayColor(request.absenceType || ''), + absenceDuration, + }); + continue; + } + + requestsInfo[dateField] = [ + { + reason: request.reason, + backgroundColor: getAbsenceDayColor(request.absenceType || ''), + absenceDuration, + }, + ]; + } + } + + const listRowOnColumnOrder: any = []; + + for (const dateField of Object.keys(daysAndDatesHeaders)) { + let emptyCell = true; + const shiftCell = ( + + ); + + const contentInsideCell: any = []; + + const getDate = new Date( + new Date(dateField).setFullYear(new Date().getFullYear()), + ); + // absent day + if ( + !timeclocksInfo[dateField] && + !requestsInfo[dateField] && + scheduleShiftsInfo[dateField] && + getDate.getTime() < new Date().getTime() + ) { + contentInsideCell.push( + + Absent + , + ); + + emptyCell = false; + } + + // add timeclock content + if (dateField in timeclocksInfo) { + contentInsideCell.push( + timeclocksInfo[dateField].map((timeclock) => { + return ( + + { + setShowModal(true); + setEditTimeclock(timeclocksObj[timeclock._id]); + }} + > + {timeclock.shiftStart} ~ {timeclock.shiftEnd} + + + ); + }), + ); + emptyCell = false; + } + // add request content + if (requestsInfo[dateField]) { + requestsInfo[dateField].map((request) => { + contentInsideCell.push( + + + {request.reason} + + , + ); + }); + emptyCell = false; + } + + listRowOnColumnOrder.push( + emptyCell ? shiftCell : {contentInsideCell}, + ); + } + + return ( + + {renderUserInfo} + {listRowOnColumnOrder} + + ); + }; + + const actionBarLeft = ( + + + + + + ); + + const modalContent = (contenProps) => ( + + ); + + const trigger = ( + + ); + + const actionBarRight = ( + + + + + ); + + const actionBar = ( + + ); + + getActionBar(actionBar); + showSideBar(isSideBarOpen); + getPagination(); + + return ( + + + {renderTableHeaders()} + {showModal && renderEditForm()} + + {reportByUsers.map((r, i) => + renderUserReportRow({ ...r, index: i + 1 }), + )} + +
+
+ ); +}; + +export default TimeclockList; diff --git a/packages/plugin-timeclock-ui/src/constants.ts b/packages/plugin-timeclock-ui/src/constants.ts index 380adebcc68..55d0f61c0aa 100644 --- a/packages/plugin-timeclock-ui/src/constants.ts +++ b/packages/plugin-timeclock-ui/src/constants.ts @@ -1,28 +1,42 @@ import { __ } from '@erxes/ui/src/utils'; import { isEnabled } from '@erxes/ui/src/utils/core'; +const COLORS = { + absent: 'rgba(255,88,87,0.5)', + absentBorder: 'rgba(255,88,87,0.1)', + shiftRequest: '#85C7F2', + paidAbsence: 'rgba(72,191,132, 0.5)', + unpaidAbsence: '#E7C8DD', + shiftNotClosed: 'rgba(175,66,174,0.5)', + regularTimeclock: ' rgba(0, 177, 78, 0.1)', + activeTimeclock: 'rgba(255,88,87,0.2)', + weekend: 'rgba(244,193,189,1.0)', + white: '#ffffff', + blank: '#ffffff', +}; + const menuTimeClock = (searchFilter: string, isCurrentUserAdmin: boolean) => { const navigationMenu = [ - { title: __('Timeclocks'), link: `/timeclocks${searchFilter}` } + { title: __('Timeclocks'), link: `/timeclocks${searchFilter}` }, ]; if (!isEnabled('bichil')) { navigationMenu.push({ title: __('Time logs'), - link: `/timeclocks/logs${searchFilter}` + link: `/timeclocks/logs${searchFilter}`, }); } navigationMenu.push( { title: __('Requests'), link: `/timeclocks/requests${searchFilter}` }, { title: __('Schedule'), link: `/timeclocks/schedule${searchFilter}` }, - { title: __('Report'), link: `/timeclocks/report${searchFilter}` } + { title: __('Report'), link: `/timeclocks/report${searchFilter}` }, ); if (isCurrentUserAdmin) { navigationMenu.push({ title: __('Configuration'), - link: `/timeclocks/config${searchFilter}` + link: `/timeclocks/config${searchFilter}`, }); } @@ -43,5 +57,6 @@ export { dateAndTimeFormat, dateDayFormat, dayOfTheWeekFormat, - dateOfTheMonthFormat + dateOfTheMonthFormat, + COLORS, }; diff --git a/packages/plugin-timeclock-ui/src/containers/timeclock/TimeclockList2.tsx b/packages/plugin-timeclock-ui/src/containers/timeclock/TimeclockList2.tsx new file mode 100644 index 00000000000..64a4ad1fa98 --- /dev/null +++ b/packages/plugin-timeclock-ui/src/containers/timeclock/TimeclockList2.tsx @@ -0,0 +1,94 @@ +import React from 'react'; + +import { gql } from '@apollo/client'; +import { graphql } from '@apollo/client/react/hoc'; +import Spinner from '@erxes/ui/src/components/Spinner'; +import { withProps } from '@erxes/ui/src/utils/core'; +import * as compose from 'lodash.flowright'; +import TimeclockList from '../../components/timeclock/TimeclockList2'; +import { queries, mutations } from '../../graphql'; +import { + ReportByUsersQueryResponse, + TimeClockMutationResponse, +} from '../../types'; +import { generateParams } from '../../utils'; +import { Alert } from '@erxes/ui/src/utils'; +import { IUser } from '@erxes/ui/src/auth/types'; +type Props = { + currentUser: IUser; + queryParams: any; + history?: any; + isCurrentUserAdmin: boolean; + isCurrentUserSupervisor: boolean; + + getActionBar: (actionBar: any) => void; + showSideBar: (sideBar: boolean) => void; + getPagination: (pagination: any) => void; +}; + +type FinalProps = { + listReportByUsersQuery: ReportByUsersQueryResponse; +} & Props & + TimeClockMutationResponse; + +const TimeclockContainer = (props: FinalProps) => { + const { + listReportByUsersQuery, + timeclockEditMutation, + isCurrentUserAdmin, + isCurrentUserSupervisor, + getPagination, + getActionBar, + showSideBar, + queryParams, + currentUser, + } = props; + + if (listReportByUsersQuery.loading) { + return ; + } + + const timeclockEdit = (variables: any) => { + timeclockEditMutation({ variables }) + .then(() => Alert.success('Successfully edited timeclock')) + .catch((err) => Alert.error(err.message)); + }; + + const { list = [], totalCount } = + listReportByUsersQuery?.timeclockReportByUsers; + + return ( + + ); +}; + +export default withProps( + compose( + graphql(gql(queries.timeclockReportByUsers), { + name: 'listReportByUsersQuery', + options: ({ queryParams, isCurrentUserAdmin }) => ({ + variables: { + ...generateParams(queryParams), + isCurrentUserAdmin, + }, + }), + }), + graphql(gql(mutations.timeclockEdit), { + name: 'timeclockEditMutation', + options: { + refetchQueries: ['timeclocksMain', 'timeclockReportByUsers'], + }, + }), + )(TimeclockContainer), +); diff --git a/packages/plugin-timeclock-ui/src/graphql/queries.ts b/packages/plugin-timeclock-ui/src/graphql/queries.ts index 9a62d3258ab..5c91cc1d225 100644 --- a/packages/plugin-timeclock-ui/src/graphql/queries.ts +++ b/packages/plugin-timeclock-ui/src/graphql/queries.ts @@ -383,6 +383,45 @@ query scheduleConfigOrder($userId: String){ } }`; +const timeclockReportByUsers = ` +query timeclockReportByUsers(${listParamsDef}){ + timeclockReportByUsers(${listParamsValue}){ + list { + user{ + ${userFields} + } + + + schedules{ + shifts{ + shiftStart + shiftEnd + } + } + timeclocks{ + _id + shiftStart + shiftEnd + shiftActive + user { + ${userFields} + } + employeeUserName + branchName + employeeId + deviceName + deviceType + inDevice + inDeviceType + outDevice + outDeviceType + } + requests + } + totalCount + } +}`; + export default { timeclockReports, branches, @@ -406,5 +445,6 @@ export default { scheduleConfigOrder, timeclockBranches, - timeclockDepartments + timeclockDepartments, + timeclockReportByUsers, }; diff --git a/packages/plugin-timeclock-ui/src/styles.ts b/packages/plugin-timeclock-ui/src/styles.ts index 3f59397420b..fcd6ffef7d6 100644 --- a/packages/plugin-timeclock-ui/src/styles.ts +++ b/packages/plugin-timeclock-ui/src/styles.ts @@ -18,7 +18,7 @@ const FilterWrapper = styled.div` `; export const Trigger = styledTS<{ type: string; isHoverActionBar?: boolean }>( - styled.div + styled.div, )` cursor: pointer; display: flex; @@ -80,8 +80,8 @@ const SidebarHeader = styledTS<{ bold?: boolean; }>(styled.div)` height: ${dimensions.headerSpacing}px; - text-transform: ${props => props.uppercase && 'uppercase'}; - font-weight: ${props => (props.bold ? 'bold' : '500')}; + text-transform: ${(props) => props.uppercase && 'uppercase'}; + font-weight: ${(props) => (props.bold ? 'bold' : '500')}; display: flex; font-size: ${typography.fontSizeHeading8}px; flex-direction: column; @@ -92,7 +92,7 @@ const CustomWidth = styledTS<{ widthPercent: number; }>(styled.div)` -width: ${props => props.widthPercent}%; +width: ${(props) => props.widthPercent}%; margin-top: 10px; margin-bottom: 10px; display: flex; @@ -141,7 +141,7 @@ const CustomRangeContainer = styled.div` const CustomRow = styledTS<{ marginNum: number; }>(styled.div)` - margin: ${props => props.marginNum}px 0 + margin: ${(props) => props.marginNum}px 0 `; const Input = styledTS<{ @@ -155,16 +155,16 @@ const Input = styledTS<{ padding: ${dimensions.unitSpacing}px 0; color: ${colors.textPrimary}; border-bottom: 1px solid; - border-color:${props => + border-color:${(props) => props.hasError ? colors.colorCoreRed : colors.colorShadowGray}; background: none; transition: all 0.3s ease; - type: ${props => { + type: ${(props) => { if (props.type) { return props.type; } }} - ${props => { + ${(props) => { if (props.round) { return ` font-size: 13px; @@ -177,7 +177,7 @@ const Input = styledTS<{ return ''; }}; - ${props => { + ${(props) => { if (props.align) { return ` text-align: ${props.align}; @@ -273,7 +273,7 @@ const InlineBlock = styled.div` `; const CustomLabel = styledTS<{ uppercase?: boolean }>(styled.label)` - text-transform: ${props => (props.uppercase ? 'uppercase' : 'none')}; + text-transform: ${(props) => (props.uppercase ? 'uppercase' : 'none')}; display: inline-block; margin: 10px 0; font-weight: ${typography.fontWeightRegular}; @@ -307,7 +307,7 @@ const FlexColumn = styledTS<{ }>(styled.div)` display: flex; flex-direction: column; - gap:${props => props.marginNum}px; + gap:${(props) => props.marginNum}px; `; const FlexColumnMargined = styledTS<{ @@ -315,8 +315,8 @@ const FlexColumnMargined = styledTS<{ }>(styled.div)` display: flex; flex-direction: column; - gap: ${props => props.marginNum}px - margin-top:${props => props.marginNum * 2}px; + gap: ${(props) => props.marginNum}px + margin-top:${(props) => props.marginNum * 2}px; `; const FlexColumnCustom = styledTS<{ @@ -324,7 +324,7 @@ const FlexColumnCustom = styledTS<{ }>(styled.div)` display: flex; flex-direction: column; - gap: ${props => props.marginNum}px + gap: ${(props) => props.marginNum}px margin: 20px 20px div:first-child { @@ -336,7 +336,7 @@ const FlexColumnCustom = styledTS<{ const CustomWidthDiv = styledTS<{ width: number; }>(styled.div)` - width: ${props => props.width}px + width: ${(props) => props.width}px justify-content: right; `; @@ -351,7 +351,7 @@ const TextAlignRight = styled.div` const ToggleDisplay = styledTS<{ display: boolean; }>(styled.div)` - display: ${props => (props.display ? 'inline' : 'none')}; + display: ${(props) => (props.display ? 'inline' : 'none')}; `; const DateName = styled.div` @@ -361,11 +361,11 @@ const DateName = styled.div` `; const MarginX = styledTS<{ margin: number }>(styled.div)` - margin: 0 ${props => props.margin}px; + margin: 0 ${(props) => props.margin}px; `; const MarginY = styledTS<{ margin: number }>(styled.div)` - margin: ${props => props.margin}px 0; + margin: ${(props) => props.margin}px 0; `; const RowField = styled.div` @@ -389,7 +389,7 @@ const CustomCollapseRow = styledTS<{ isChild: boolean }>(styled.div)` overflow: hidden; justify-content: space-between; align-items: center; - padding: ${props => + padding: ${(props) => props.isChild ? dimensions.unitSpacing : dimensions.coreSpacing}px; margin: 0px; background: ${colors.colorWhite}; @@ -419,12 +419,12 @@ const SortItem = styledTS<{ display: flex; justify-content: space-between; border-left: 2px solid transparent; - border-top: ${props => + border-top: ${(props) => !props.isDragging ? `1px solid ${colors.borderPrimary}` : 'none'}; border-radius: 4px; - box-shadow: ${props => + box-shadow: ${(props) => props.isDragging ? `0 2px 8px ${colors.shadowPrimary}` : 'none'}; - left: ${props => + left: ${(props) => props.isDragging && props.isModal ? '40px!important' : 'auto'}; &:last-child { margin-bottom: 0; @@ -435,7 +435,7 @@ const SortItem = styledTS<{ border-color: ${colors.colorSecondary}; border-top: none; } - ${props => + ${(props) => props.column && css` width: ${100 / props.column}%; @@ -456,7 +456,7 @@ const CustomBoxWrapper = styled.div` `; const RoundBox = styledTS<{ pinned?: boolean }>(styled.div)` - background: ${props => (props.pinned ? colors.colorSecondary : '#f5f5f5')}; + background: ${(props) => (props.pinned ? colors.colorSecondary : '#f5f5f5')}; border-radius: 50%; border: 1px solid ${colors.borderPrimary}; width: ${dimensions.coreSpacing}px; @@ -469,7 +469,7 @@ const RoundBox = styledTS<{ pinned?: boolean }>(styled.div)` top: -5px; i { - filter: ${props => !props.pinned && 'brightness(30%)'}; + filter: ${(props) => !props.pinned && 'brightness(30%)'}; } &:hover { @@ -509,8 +509,8 @@ const SearchInput = styledTS<{ isInPopover: boolean }>(styled.div)` border: 1px solid ${colors.borderPrimary}; padding: 20px 20px 20px 30px; border-radius: 20px; - width: ${props => (props.isInPopover ? '250px' : '350px')}; - margin: ${props => props.isInPopover && '5px 5px 0'}; + width: ${(props) => (props.isInPopover ? '250px' : '350px')}; + margin: ${(props) => props.isInPopover && '5px 5px 0'}; background: ${colors.colorWhite}; @media (max-width: 1300px) { @@ -548,6 +548,72 @@ const SchedulesTableWrapper = styled.div` } `; +const BorderedTd = styled.td` + background-color: white; + border: 2px solid #eeeeee; + text-align: center; +`; + +const RequestInfo = styledTS<{ + backgroundColor: string; + borderColor?: string; + textColor?: string; + hoverContent?: string; +}>(styled.div)` + width: 92px; + overflow:hidden; + white-space:nowrap; + text-overflow: ellipsis; + border: 2px solid ${(props) => + props.borderColor ? props.borderColor : props.backgroundColor}; + border-radius: 20px; + padding: 6px; + margin: 5px auto; + background-color:${(props) => props.backgroundColor}; + color: white; +`; + +const TimeclockInfo = styledTS<{ + activeShift?: boolean; + color?: string; + disabled?: boolean; +}>(styled.div)` + width: ${(props) => (props.activeShift ? '92px' : 'max-content')}; + border: 2px solid ${(props) => + props.activeShift ? 'rgba(255,88,87,0.2)' : 'rgba(0, 177, 78, 0.1)'}; + border-radius: 20px; + padding: 6px; + margin: 5px auto; + background-color: ${(props) => + props.color + ? props.color + : props.activeShift + ? 'rgba(255,88,87,0.2)' + : 'rgba(0, 177, 78, 0.1)'}; + + pointer-events: ${(props) => (props.disabled ? 'none' : '')} + opacity: ${(props) => (props.disabled ? '0.7' : '1')}; + cursor: pointer; +`; + +const TimeclockTableWrapper = styled.div` + .fixed-column { + position: sticky; + left: 0; + background: #fff; + z-index: 99; + } +`; + +const ColoredSquare = styledTS<{ color: string }>(styled.div)` + width: 10px; /* Adjust the size of the colored corner */ + max-width:10px; + height: 10px; /* Adjust the size of the colored corner */ + max-height:10px; + background-color: ${(props) => props.color}; + } +`; + export { AlertContainer, ConfigFormWrapper, @@ -586,5 +652,10 @@ export { TextAlignCenter, TextAlignRight, ToggleButton, - ToggleDisplay + ToggleDisplay, + BorderedTd, + RequestInfo, + TimeclockInfo, + TimeclockTableWrapper, + ColoredSquare, }; diff --git a/packages/plugin-timeclock-ui/src/types.ts b/packages/plugin-timeclock-ui/src/types.ts index 1d8dc651996..679bbc7889e 100644 --- a/packages/plugin-timeclock-ui/src/types.ts +++ b/packages/plugin-timeclock-ui/src/types.ts @@ -99,6 +99,12 @@ export interface IUserReport { totalAbsenceMins?: number; totalMinsAbsenceThisMonth?: number; absenceInfo?: IUserAbsenceInfo; + + index?: number; + + schedules?: ISchedule[]; + timeclocks?: ITimeclock[]; + requests?: IAbsence[]; } export interface IUserAbsenceInfo { @@ -275,6 +281,10 @@ export type ReportsQueryResponse = { timeclockReports: { list: IReport[]; totalCount: number }; } & QueryResponse; +export type ReportByUsersQueryResponse = { + timeclockReportByUsers: { list: [IUserReport]; totalCount: number }; +} & QueryResponse; + export type MutationVariables = { _id?: string; userId?: string;