MSI\2Fi 11 months ago
parent
commit
2da20b697f
6 changed files with 185 additions and 43 deletions
  1. +113
    -37
      src/main/java/com/ffii/tsms/modules/common/mail/service/MailReminderService.kt
  2. +9
    -0
      src/main/java/com/ffii/tsms/modules/common/mail/web/models/WorkHourRecords.kt
  3. +13
    -4
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  4. +2
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/entity/LeaveRepository.kt
  5. +48
    -2
      src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt
  6. BIN
      src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx

+ 113
- 37
src/main/java/com/ffii/tsms/modules/common/mail/service/MailReminderService.kt View File

@@ -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.SettingNames
import com.ffii.tsms.modules.common.holiday.service.HolidayService 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.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.Staff
import com.ffii.tsms.modules.data.entity.StaffRepository import com.ffii.tsms.modules.data.entity.StaffRepository
import com.ffii.tsms.modules.data.entity.TeamRepository 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.data.service.StaffsService
import com.ffii.tsms.modules.settings.service.SettingsService import com.ffii.tsms.modules.settings.service.SettingsService
import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository 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 com.ffii.tsms.modules.user.service.UserService
import jakarta.mail.internet.InternetAddress import jakarta.mail.internet.InternetAddress
import org.apache.commons.logging.Log import org.apache.commons.logging.Log
@@ -26,12 +28,14 @@ data class TableRow(
val name: String, val name: String,
val missingDates: MutableList<LocalDate>, val missingDates: MutableList<LocalDate>,
) )

@Service @Service
open class MailReminderService( open class MailReminderService(
val mailService: MailService, val mailService: MailService,
val userService: UserService, val userService: UserService,
val settingsService: SettingsService, val settingsService: SettingsService,
val holidayService: HolidayService, val holidayService: HolidayService,
val timesheetsService: TimesheetsService,
val timesheetRepository: TimesheetRepository, val timesheetRepository: TimesheetRepository,
val staffsService: StaffsService, val staffsService: StaffsService,
val staffRepository: StaffRepository, val staffRepository: StaffRepository,
@@ -117,8 +121,19 @@ open class MailReminderService(
val filteredLastMonthDays = lastMonthDays.filter { val filteredLastMonthDays = lastMonthDays.filter {
it !in allHolidaysList && it.dayOfWeek != DayOfWeek.SATURDAY && it.dayOfWeek != DayOfWeek.SUNDAY 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<WorkHourRecords> = 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 staffs = staffRepository.findAllByEmployTypeAndDeletedFalseAndDepartDateIsNull(FULLTIME)
val teams = teamRepository.findAll().filter { team -> team.deleted == false } val teams = teamRepository.findAll().filter { team -> team.deleted == false }


@@ -128,17 +143,36 @@ open class MailReminderService(
val teamMembers: List<Staff> = staffs.filter { it.team != null && it.team.id == team.id } val teamMembers: List<Staff> = staffs.filter { it.team != null && it.team.id == team.id }
if (teamMembers.isEmpty()) continue if (teamMembers.isEmpty()) continue
val teamMembersIds: List<Long?> = teamMembers.map { it.id }.sorted() val teamMembersIds: List<Long?> = 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<Long>(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<Long>(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 // creating the email
val intro = StringBuilder("${teamLead.name}, Staffs Missing Timesheet in the Table Below: \n") val intro = StringBuilder("${teamLead.name}, Staffs Missing Timesheet in the Table Below: \n")
val tableData = mutableListOf<TableRow>() val tableData = mutableListOf<TableRow>()
@@ -148,7 +182,7 @@ open class MailReminderService(
goodStaffsList.forEach { (key, value) -> goodStaffsList.forEach { (key, value) ->
if (!value.contains(it.id!!)) { if (!value.contains(it.id!!)) {
isNaughty = true isNaughty = true
missingDates.add(key!!)
missingDates.add(key)
} }
} }
if (!isNaughty) return@forEach if (!isNaughty) return@forEach
@@ -176,7 +210,19 @@ open class MailReminderService(
val allHolidaysList: List<LocalDate> = (holidayList + companyHolidayList).toSet().toList() val allHolidaysList: List<LocalDate> = (holidayList + companyHolidayList).toSet().toList()


//get data //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<WorkHourRecords> = 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 staffs = staffRepository.findAllByEmployTypeAndDeletedFalseAndDepartDateIsNull(FULLTIME)
val teams = teamRepository.findAll().filter { team -> team.deleted == false } val teams = teamRepository.findAll().filter { team -> team.deleted == false }


@@ -195,18 +241,24 @@ open class MailReminderService(
if (teamMembers.isEmpty()) continue if (teamMembers.isEmpty()) continue
val teamMembersIds: List<Long?> = teamMembers.map { it.id }.sorted() val teamMembersIds: List<Long?> = teamMembers.map { it.id }.sorted()
// getting the naughty list // 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<Long>(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 // 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 } }.sortedBy { it.first }

// creating the email content // creating the email content
val intro = StringBuilder("${teamLead.name}, Staffs Missing Timesheet in the Table Below: ($firstDay-$today) \n") val intro = StringBuilder("${teamLead.name}, Staffs Missing Timesheet in the Table Below: ($firstDay-$today) \n")
val tableData = mutableListOf<TableRow>() val tableData = mutableListOf<TableRow>()
@@ -216,7 +268,7 @@ open class MailReminderService(
goodStaffsList.forEach { (key, value) -> goodStaffsList.forEach { (key, value) ->
if (!value.contains(it.id!!)) { if (!value.contains(it.id!!)) {
isNaughty = true isNaughty = true
missingDates.add(key!!)
missingDates.add(key)
} }
} }
if (!isNaughty) return@forEach if (!isNaughty) return@forEach
@@ -267,22 +319,46 @@ open class MailReminderService(
daysBefore = daysBefore.minusDays(1) 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<WorkHourRecords> = 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 staffs = staffRepository.findAllByEmployTypeAndDeletedFalseAndDepartDateIsNull(FULLTIME) // FT? FT? etc
val staffIds: List<Long> = staffs.map { it.id as Long } val staffIds: List<Long> = 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<Long>(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<Long>(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 // change this list with the staffs that need checking
staffIds.forEach { id -> staffIds.forEach { id ->
var isNaughty: Boolean = false var isNaughty: Boolean = false


+ 9
- 0
src/main/java/com/ffii/tsms/modules/common/mail/web/models/WorkHourRecords.kt View File

@@ -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,
)

+ 13
- 4
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt View File

@@ -2167,10 +2167,18 @@ open class ReportService(
// } // }


open fun getProjectResourceOverconsumptionReport(args: Map<String, Any>): List<Map<String, Any>> { open fun getProjectResourceOverconsumptionReport(args: Map<String, Any>): List<Map<String, Any>> {
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.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 " + " , 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.consumedBudget) / ((p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8) as budgetConsumptionRate "
+ " , sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0) as manhourConsumptionRate " + " , 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 " + " LEFT JOIN salary sal on se.salaryId = sal.salaryPoint "
+ " ) t " + " ) t "
+ " left join project p on p.id = t.projectId " + " 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 team tm on p.teamLead = tm.teamLead "
+ " left join customer c on c.id = p.customerId " + " left join customer c on c.id = p.customerId "
+ " left join subsidiary ss on p.customerSubsidiaryId = ss.id " + " left join subsidiary ss on p.customerSubsidiaryId = ss.id "
@@ -2217,7 +2226,7 @@ open class ReportService(
else -> "" 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) sql.append(statusFilter)
return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
} }


+ 2
- 0
src/main/java/com/ffii/tsms/modules/timesheet/entity/LeaveRepository.kt View File

@@ -10,4 +10,6 @@ interface LeaveRepository : AbstractRepository<Leave, Long> {
fun deleteAllByStaffAndRecordDate(staff: Staff, recordDate: LocalDate) fun deleteAllByStaffAndRecordDate(staff: Staff, recordDate: LocalDate)


fun findByStaffAndRecordDateBetweenOrderByRecordDate(staff: Staff, start: LocalDate, end: LocalDate): List<Leave> fun findByStaffAndRecordDateBetweenOrderByRecordDate(staff: Staff, start: LocalDate, end: LocalDate): List<Leave>
fun findByDeletedFalseAndRecordDateBetweenOrderByRecordDate(start: LocalDate, end: LocalDate): List<Leave>

} }

+ 48
- 2
src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt View File

@@ -1,6 +1,9 @@
package com.ffii.tsms.modules.timesheet.service package com.ffii.tsms.modules.timesheet.service


import com.ffii.core.exception.BadRequestException 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.entity.StaffRepository
import com.ffii.tsms.modules.data.service.StaffsService import com.ffii.tsms.modules.data.service.StaffsService
import com.ffii.tsms.modules.data.service.TeamService import com.ffii.tsms.modules.data.service.TeamService
@@ -28,8 +31,10 @@ open class TimesheetsService(
private val projectRepository: ProjectRepository, private val projectRepository: ProjectRepository,
private val taskRepository: TaskRepository, private val taskRepository: TaskRepository,
private val staffsService: StaffsService, 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<Timesheet, Long, TimesheetRepository>(jdbcDao, timesheetRepository) {
@Transactional @Transactional
open fun saveTimesheet(recordTimeEntry: Map<LocalDate, List<TimeEntry>>): Map<String, List<TimeEntry>> { open fun saveTimesheet(recordTimeEntry: Map<LocalDate, List<TimeEntry>>): Map<String, List<TimeEntry>> {
// Need to be associated with a staff // Need to be associated with a staff
@@ -378,4 +383,45 @@ open class TimesheetsService(


return "Rearrange success" return "Rearrange success"
} }
open fun workHourRecordsWithinRange(args: Map<String, Any>): List<Map<String, Any>> {
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)
}

} }

BIN
src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx View File


Loading…
Cancel
Save