diff --git a/src/main/java/com/ffii/tsms/modules/common/mail/service/MailReminderService.kt b/src/main/java/com/ffii/tsms/modules/common/mail/service/MailReminderService.kt index 5778895..e80d352 100644 --- a/src/main/java/com/ffii/tsms/modules/common/mail/service/MailReminderService.kt +++ b/src/main/java/com/ffii/tsms/modules/common/mail/service/MailReminderService.kt @@ -3,6 +3,7 @@ package com.ffii.tsms.modules.common.mail.service import com.ffii.tsms.modules.common.SettingNames import com.ffii.tsms.modules.common.holiday.service.HolidayService import com.ffii.tsms.modules.common.mail.pojo.MailRequest +import com.ffii.tsms.modules.common.mail.web.models.WorkHourRecords import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.StaffRepository import com.ffii.tsms.modules.data.entity.TeamRepository @@ -10,6 +11,7 @@ import com.ffii.tsms.modules.data.service.CompanyHolidayService import com.ffii.tsms.modules.data.service.StaffsService import com.ffii.tsms.modules.settings.service.SettingsService import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository +import com.ffii.tsms.modules.timesheet.service.TimesheetsService import com.ffii.tsms.modules.user.service.UserService import jakarta.mail.internet.InternetAddress import org.apache.commons.logging.Log @@ -26,12 +28,14 @@ data class TableRow( val name: String, val missingDates: MutableList, ) + @Service open class MailReminderService( val mailService: MailService, val userService: UserService, val settingsService: SettingsService, val holidayService: HolidayService, + val timesheetsService: TimesheetsService, val timesheetRepository: TimesheetRepository, val staffsService: StaffsService, val staffRepository: StaffRepository, @@ -117,8 +121,19 @@ open class MailReminderService( val filteredLastMonthDays = lastMonthDays.filter { it !in allHolidaysList && it.dayOfWeek != DayOfWeek.SATURDAY && it.dayOfWeek != DayOfWeek.SUNDAY } - - val timesheet = timesheetRepository.findByDeletedFalseAndRecordDateBetweenOrderByRecordDate(filteredLastMonthDays.first(),filteredLastMonthDays.last()) + val args = mutableMapOf( + "from" to filteredLastMonthDays.first(), + "to" to filteredLastMonthDays.last(), + ) + val ts = timesheetsService.workHourRecordsWithinRange(args) + val timesheet: List = ts.map { + WorkHourRecords( + staffId = it["staffId"].toString().toLong(), + recordDate = LocalDate.parse(it["recordDate"].toString()), + hours = it["hours"].toString().toDouble() + ) + } +// val timesheet = timesheetRepository.findByDeletedFalseAndRecordDateBetweenOrderByRecordDate(filteredLastMonthDays.first(),filteredLastMonthDays.last()) val staffs = staffRepository.findAllByEmployTypeAndDeletedFalseAndDepartDateIsNull(FULLTIME) val teams = teamRepository.findAll().filter { team -> team.deleted == false } @@ -128,17 +143,36 @@ open class MailReminderService( val teamMembers: List = staffs.filter { it.team != null && it.team.id == team.id } if (teamMembers.isEmpty()) continue val teamMembersIds: List = teamMembers.map { it.id }.sorted() - val filteredTimesheet = timesheet.filter { teamMembersIds.contains(it.staff?.id) } - val timesheetByIdAndRecord = filteredTimesheet.groupBy { it.staff?.id to it.recordDate } - .map { (key, _) -> - val (staffId, recordDate) = key - recordDate to mutableListOf(staffId ?: 0) +// val filteredTimesheet = timesheet.filter { teamMembersIds.contains(it.staff?.id) } + val filteredTimesheet = timesheet.filter { teamMembersIds.contains(it.staffId) } +// val timesheetByIdAndRecord = filteredTimesheet.groupBy { it.staff?.id to it.recordDate } +// .map { (key, _) -> +// val (staffId, recordDate) = key +// recordDate to mutableListOf(staffId ?: 0) +// } + println(filteredTimesheet.filter { it.staffId == 4.toLong() }) + val timesheetByIdAndRecord = filteredTimesheet.groupBy { it.staffId to it.recordDate } + .mapNotNull { (key, records) -> + Triple( + key.second, + key.first, + records.sumOf { it.hours } + ) } - val goodStaffsList = filteredLastMonthDays.map { day -> - timesheetByIdAndRecord.find { - it.first == day - } ?: Pair(day, mutableListOf()) - }.sortedBy { it.first } +// val goodStaffsList = filteredLastMonthDays.map { day -> +// timesheetByIdAndRecord.find { +// it.first == day +// } ?: Pair(day, mutableListOf()) +// }.sortedBy { it.first } + + val goodStaffsList = filteredLastMonthDays.map { date -> + val matchedStaffIds = timesheetByIdAndRecord + .filter { it.first == date && it.third >= 8 } + .map { it.second } // Extracting the second element (staffId) + // Returning a Pair of the date and the list of matched staff IDs + Pair(date, matchedStaffIds) + }.sortedBy { it.first } // Sort by date +// return // creating the email val intro = StringBuilder("${teamLead.name}, Staffs Missing Timesheet in the Table Below: \n") val tableData = mutableListOf() @@ -148,7 +182,7 @@ open class MailReminderService( goodStaffsList.forEach { (key, value) -> if (!value.contains(it.id!!)) { isNaughty = true - missingDates.add(key!!) + missingDates.add(key) } } if (!isNaughty) return@forEach @@ -176,7 +210,19 @@ open class MailReminderService( val allHolidaysList: List = (holidayList + companyHolidayList).toSet().toList() //get data - val timesheet = timesheetRepository.findByDeletedFalseAndRecordDateBetweenOrderByRecordDate(firstDay, today) + val args = mutableMapOf( + "from" to firstDay, + "to" to today, + ) + val ts = timesheetsService.workHourRecordsWithinRange(args) + val timesheet: List = ts.map { + WorkHourRecords( + staffId = it["staffId"].toString().toLong(), + recordDate = LocalDate.parse(it["recordDate"].toString()), + hours = it["hours"].toString().toDouble() + ) + } +// val timesheet = timesheetRepository.findByDeletedFalseAndRecordDateBetweenOrderByRecordDate(firstDay, today) val staffs = staffRepository.findAllByEmployTypeAndDeletedFalseAndDepartDateIsNull(FULLTIME) val teams = teamRepository.findAll().filter { team -> team.deleted == false } @@ -195,18 +241,24 @@ open class MailReminderService( if (teamMembers.isEmpty()) continue val teamMembersIds: List = teamMembers.map { it.id }.sorted() // getting the naughty list - val filteredTimesheet = timesheet.filter { teamMembersIds.contains(it.staff?.id) } // filter team members' timesheet - val timesheetByIdAndRecord = filteredTimesheet.groupBy { it.staff?.id to it.recordDate } - .map { (key, _) -> - val (staffId, recordDate) = key - recordDate to mutableListOf(staffId ?: 0) + val filteredTimesheet = timesheet.filter { teamMembersIds.contains(it.staffId) } // filter team members' timesheet + val timesheetByIdAndRecord = filteredTimesheet.groupBy { it.staffId to it.recordDate } + .mapNotNull { (key, records) -> + Triple( + key.second, + key.first, + records.sumOf { it.hours } + ) } // change the date list with desired time range - val goodStaffsList = filteredDatesList.map { day -> - timesheetByIdAndRecord.find { - it.first == day - } ?: Pair(day, mutableListOf()) + val goodStaffsList = filteredDatesList.map { date -> + val matchedStaffIds = timesheetByIdAndRecord + .filter { it.first == date && it.third >= 8 } + .map { it.second } // Extracting the second element (staffId) + // Returning a Pair of the date and the list of matched staff IDs + Pair(date, matchedStaffIds) }.sortedBy { it.first } + // creating the email content val intro = StringBuilder("${teamLead.name}, Staffs Missing Timesheet in the Table Below: ($firstDay-$today) \n") val tableData = mutableListOf() @@ -216,7 +268,7 @@ open class MailReminderService( goodStaffsList.forEach { (key, value) -> if (!value.contains(it.id!!)) { isNaughty = true - missingDates.add(key!!) + missingDates.add(key) } } if (!isNaughty) return@forEach @@ -267,22 +319,46 @@ open class MailReminderService( daysBefore = daysBefore.minusDays(1) } - val timesheet = timesheetRepository.findByDeletedFalseAndRecordDateBetweenOrderByRecordDate(sevenDaysBefore, fourDaysBefore) + val args = mutableMapOf( + "from" to sevenDaysBefore, + "to" to fourDaysBefore, + ) + + val ts = timesheetsService.workHourRecordsWithinRange(args) + val timesheet: List = ts.map { + WorkHourRecords( + staffId = it["staffId"].toString().toLong(), + recordDate = LocalDate.parse(it["recordDate"].toString()), + hours = it["hours"].toString().toDouble() + ) + } +// val timesheet = timesheetRepository.findByDeletedFalseAndRecordDateBetweenOrderByRecordDate(sevenDaysBefore, fourDaysBefore) val staffs = staffRepository.findAllByEmployTypeAndDeletedFalseAndDepartDateIsNull(FULLTIME) // FT? FT? etc val staffIds: List = staffs.map { it.id as Long } - val timesheetByIdAndRecord = timesheet.groupBy { it.staff?.id to it.recordDate } - .map { (key, _) -> - val (staffId, recordDate) = key - "$recordDate" to mutableListOf(staffId ?: 0) - } - val goodStaffsList = workingDaysList.map { it -> - val key = it.toString() - timesheetByIdAndRecord.find { - it.first == key - }?: Pair(key, mutableListOf()) - }.sortedBy { it.first } - +// val timesheetByIdAndRecord = timesheet.groupBy { it.staff?.id to it.recordDate } +// .map { (key, _) -> +// val (staffId, recordDate) = key +// "$recordDate" to mutableListOf(staffId ?: 0) +// } + val timesheetByIdAndRecord = timesheet.groupBy { + it.staffId to it.recordDate + }.mapNotNull { (key, records) -> + Triple( + key.second, + key.first, + records.sumOf { it.hours } + ) + } + val goodStaffsList = workingDaysList.map { date -> + val matchedStaffIds = timesheetByIdAndRecord + .filter { it.first == date && it.third >= 8 } + .map { it.second } // Extracting the second element (staffId) + // Returning a Pair of the date and the list of matched staff IDs + Pair(date, matchedStaffIds) + }.sortedBy { it.first } // Sort by date + println("goodStaffsList") + println(goodStaffsList) // change this list with the staffs that need checking staffIds.forEach { id -> var isNaughty: Boolean = false diff --git a/src/main/java/com/ffii/tsms/modules/common/mail/web/models/WorkHourRecords.kt b/src/main/java/com/ffii/tsms/modules/common/mail/web/models/WorkHourRecords.kt new file mode 100644 index 0000000..7b41728 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/common/mail/web/models/WorkHourRecords.kt @@ -0,0 +1,9 @@ +package com.ffii.tsms.modules.common.mail.web.models + +import java.time.LocalDate + +data class WorkHourRecords( + val staffId: Long, + val recordDate: LocalDate, + val hours: Double, +) diff --git a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt index 33fd49a..528ddb8 100644 --- a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -2167,10 +2167,18 @@ open class ReportService( // } open fun getProjectResourceOverconsumptionReport(args: Map): List> { - val sql = StringBuilder( - " SELECT " + val sql = StringBuilder("with pe_cte as (" + + " select" + + " pe.projectId," + + " sum(coalesce(amount)) as expense" + + " from project_expense pe" + + " where deleted = false" + + " group by pe.projectId" + + " )" + + " SELECT " + " p.code, p.name, tm.code as team, concat(c.code, ' -',c.name) as client, COALESCE(concat(ss.code, ' -', ss.name), 'N/A') as subsidiary " - + " , (p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8 as plannedBudget, sum(t.consumedBudget) as actualConsumedBudget " + + " , (p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8 as plannedBudget, sum(t.consumedBudget) + coalesce(pc.expense, 0) as actualConsumedBudget " + + " , coalesce(pc.expense, 0) as projectExpense " + " , COALESCE(p.totalManhour, 0) as plannedManhour, sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as actualConsumedManhour " + " , sum(t.consumedBudget) / ((p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8) as budgetConsumptionRate " + " , sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0) as manhourConsumptionRate " @@ -2189,6 +2197,7 @@ open class ReportService( + " LEFT JOIN salary sal on se.salaryId = sal.salaryPoint " + " ) t " + " left join project p on p.id = t.projectId " + + " left join pe_cte pc on pc.projectId = t.projectId " + " left join team tm on p.teamLead = tm.teamLead " + " left join customer c on c.id = p.customerId " + " left join subsidiary ss on p.customerSubsidiaryId = ss.id " @@ -2217,7 +2226,7 @@ open class ReportService( else -> "" } } - sql.append(" group by p.code, p.name, tm.code, c.code, c.name, ss.code, ss.name, p.expectedTotalFee, p.subContractFee, p.totalManhour ") + sql.append(" group by p.code, p.name, tm.code, c.code, c.name, ss.code, ss.name, p.expectedTotalFee, p.subContractFee, p.totalManhour, pc.expense ") sql.append(statusFilter) return jdbcDao.queryForList(sql.toString(), args) } diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/entity/LeaveRepository.kt b/src/main/java/com/ffii/tsms/modules/timesheet/entity/LeaveRepository.kt index 1911771..71fcbf3 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/entity/LeaveRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/entity/LeaveRepository.kt @@ -10,4 +10,6 @@ interface LeaveRepository : AbstractRepository { fun deleteAllByStaffAndRecordDate(staff: Staff, recordDate: LocalDate) fun findByStaffAndRecordDateBetweenOrderByRecordDate(staff: Staff, start: LocalDate, end: LocalDate): List + fun findByDeletedFalseAndRecordDateBetweenOrderByRecordDate(start: LocalDate, end: LocalDate): List + } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt index b862ab1..012acfc 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt @@ -1,6 +1,9 @@ package com.ffii.tsms.modules.timesheet.service import com.ffii.core.exception.BadRequestException +import com.ffii.core.support.AbstractBaseEntityService +import com.ffii.core.support.JdbcDao +import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.StaffRepository import com.ffii.tsms.modules.data.service.StaffsService import com.ffii.tsms.modules.data.service.TeamService @@ -28,8 +31,10 @@ open class TimesheetsService( private val projectRepository: ProjectRepository, private val taskRepository: TaskRepository, private val staffsService: StaffsService, - private val teamService: TeamService, private val staffRepository: StaffRepository, private val leaveRepository: LeaveRepository -) { + private val teamService: TeamService, + private val staffRepository: StaffRepository, private val leaveRepository: LeaveRepository, + private val jdbcDao: JdbcDao, + ) : AbstractBaseEntityService(jdbcDao, timesheetRepository) { @Transactional open fun saveTimesheet(recordTimeEntry: Map>): Map> { // Need to be associated with a staff @@ -378,4 +383,45 @@ open class TimesheetsService( return "Rearrange success" } + open fun workHourRecordsWithinRange(args: Map): List> { + val sql = StringBuilder("WITH ts_cte AS (" + + " SELECT " + + " t.recordDate, " + + " t.staffId, " + + " SUM(coalesce(t.normalConsumed, 0)) + SUM(COALESCE(t.otConsumed, 0)) AS hours " + + " , 'normal' as tp" + + " FROM timesheet t " + + " where t.deleted = false " + + " GROUP BY t.staffId, t.recordDate " + + " ), " + + " l_cte AS ( " + + " SELECT " + + " l.recordDate, " + + " l.staffId, " + + " SUM(COALESCE(l.leaveHours, 0)) AS hours " + + " ,'ot' as tp " + + " FROM `leave` l " + + " where l.deleted = false " + + " GROUP BY l.staffId, l.recordDate " + + " ) " + + " select " + + " recordDate, " + + " staffId, " + + " hours " + + " ,tp " + + " from ( " + + " SELECT " + + " * " + + " FROM ts_cte tc " + + " union " + + " SELECT " + + " * " + + " FROM l_cte lc " + + " ) ut " + ) + if (args.containsKey("from") && args.containsKey("to")) + sql.append(" where recordDate BETWEEN :from AND :to "); + return jdbcDao.queryForList(sql.toString(), args) + } + } \ No newline at end of file diff --git a/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx b/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx index 4700608..6318513 100644 Binary files a/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx and b/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx differ