From 83c634d0b647c80b08ac7bff70fff45c936565df Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Wed, 30 Apr 2025 15:46:49 +0800 Subject: [PATCH] 7th, 15th mail reminder Dashboard filter by team Man hour daily report --- .../mail/service/MailReminderService.kt | 9 +- .../modules/data/service/DashboardService.kt | 31 ++- .../modules/report/service/ReportService.kt | 233 +++++++++++++++++- .../modules/report/web/ReportController.kt | 17 ++ .../modules/report/web/model/ReportRequest.kt | 6 + .../timesheet/entity/TimesheetRepository.kt | 3 + ...ject Daily Work Hours Analysis Report.xlsx | Bin 0 -> 14538 bytes 7 files changed, 290 insertions(+), 9 deletions(-) create mode 100644 src/main/resources/templates/report/AR09_Project Daily Work Hours Analysis Report.xlsx 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 e7fccf1..ff38e1a 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 @@ -105,6 +105,8 @@ open class MailReminderService( @Scheduled(cron = "0 0 6 7 * ?") // (SS/MM/HH/DD/MM/YY) open fun sendTimesheetToTeamLead7TH() { + logger.info("-----------------------") + logger.info("Scheduled Start: 7th email reminder") if (!isSettingsConfigValid()) return val today = LocalDate.now() val holidayList = holidayService.commonHolidayList().map { it.date } @@ -189,6 +191,7 @@ open class MailReminderService( .append(table) val receiver = listOf(teamLead.email) + createEmailRequest(emailContent.toString(), receiver) } } @@ -263,6 +266,8 @@ open class MailReminderService( // @Scheduled(cron = "0 51 14 * * ?") // (SS/MM/HH/DD/MM/YY) @Scheduled(cron = "0 0 6 15 * ?") // (SS/MM/HH/DD/MM/YY) open fun sendTimesheetToTeamLead15TH() { + logger.info("-----------------------") + logger.info("Scheduled Start: 15th email reminder") if (!isSettingsConfigValid()) return val today = LocalDate.now().minusDays(1) // should always be 14, exclude 15th because the email is sent at 0600, suppose no one input timesheet in advance val firstDay = today.withDayOfMonth(1) @@ -508,9 +513,11 @@ open class MailReminderService( message.append("\n [$key] ") } } - println(message) +// println(message) if (!isNaughty) return@forEach val emailAddress = staffRepository.findById(id).get().email +// val staffId = staffRepository.findById(id).get().staffId + println(emailAddress) val receiver = listOf(emailAddress) createEmailRequest(message.toString(), receiver) } diff --git a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt index 41fa66a..da4559c 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt @@ -466,9 +466,18 @@ open class DashboardService( + " LEFT JOIN milestone_payment mp ON m.id = mp.milestoneId" ) - if (args["teamLeadId"] != null && args["teamLeadId"].toString().toInt() > 0) { - sql.append(" WHERE p.teamLead = :teamLeadId") + if (viewDashboardAuthority() == "self") { + val teamId = staffsService.currentStaff()?.team?.id + if (teamId != null) { + sql.append(" WHERE p.teamLead = $teamId") + } + }else{ + if (args["teamLeadId"] != null && args["teamLeadId"].toString().toInt() > 0) { +// println("if 1 ${args["teamLeadId"]}") + sql.append(" WHERE p.teamLead = :teamLeadId") + } } + sql.append(" AND p.status NOT IN ('Pending to Start', 'Completed', 'Deleted')" + " AND mp.date >= CURDATE()" + " ) AS subquery" @@ -478,9 +487,20 @@ open class DashboardService( + " WHERE p.deleted = false " ) - if (args["teamLeadId"] != null && args["teamLeadId"].toString().toInt() > 0) { - sql.append(" and p.teamLead = :teamLeadId") + + if (viewDashboardAuthority() == "self") { + val teamId = staffsService.currentStaff()?.team?.id + if (teamId != null) { + sql.append(" and p.teamLead = $teamId") + } + }else{ + if (args["teamLeadId"] != null && args["teamLeadId"].toString().toInt() > 0) { +// println("if 2 ${args["teamLeadId"]}") + sql.append(" and p.teamLead = :teamLeadId") + } } + + sql.append(" and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" // + " and (tg.name != '5. Miscellaneous' or tg.name is null)" + " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone, taskGroup.expectedStage" @@ -1410,7 +1430,6 @@ open class DashboardService( + " left join subsidiary s2 on p.customerSubsidiaryId = s2.id" + " where p.deleted = 0" + " and p.status = 'On-going'" - + " order by p.code" ) if (viewDashboardAuthority() == "self") { @@ -1420,6 +1439,8 @@ open class DashboardService( } } + sql.append(" order by p.code") + return jdbcDao.queryForList(sql.toString(), args) } fun CashFlowMonthlyIncomeByMonth(args: Map): List> { 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 99ef0c1..9ffa836 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 @@ -33,10 +33,8 @@ import java.util.* import java.awt.Color import java.lang.IllegalArgumentException import java.math.RoundingMode -import java.time.Year -import javax.swing.plaf.synth.Region +import java.time.DayOfWeek import kotlin.collections.ArrayList -import kotlin.collections.HashMap data class DayInfo(val date: String?, val weekday: String?) @@ -76,6 +74,7 @@ open class ReportService( private val COMPLETION_PROJECT = "templates/report/AR05_Project Completion Report.xlsx" private val CROSS_TEAM_CHARGE_REPORT = "templates/report/Cross Team Charge Report.xlsx" private val PROJECT_MANHOUR_SUMMARY = "templates/report/Project Manhour Summary.xlsx" + private val PROJECT_MONTHLY_REPORT = "templates/report/AR09_Project Daily Work Hours Analysis Report.xlsx" private fun cellBorderArgs(top: Int, bottom: Int, left: Int, right: Int): MutableMap { var cellBorderArgs = mutableMapOf() @@ -5425,4 +5424,232 @@ open class ReportService( ProjectSummary(staffData, projectCumulativeExpenditure) } } + + data class DateInfo( + val date: LocalDate?, + val isHoliday: Boolean + ) + + fun convertStringToLocalDate(dateStrings: List, pattern: String = "dd/MM/yyyy"): List { + // Define the formatter for the date string (dd/MM/yyyy format) + val formatter = DateTimeFormatter.ofPattern(pattern) + + // Convert each string in the list to LocalDate + return dateStrings.mapNotNull { dateString -> + try { + LocalDate.parse(dateString, formatter) + } catch (e: DateTimeParseException) { + println("Invalid date format: $dateString") // Log the invalid date + null // Exclude invalid dates from the result + } + } + } + // Function to generate all dates in a month + fun generateDatesForMonthWithHolidays(yearMonth: YearMonth, holidays: List): List { + + return (1..yearMonth.lengthOfMonth()).map { day -> + val date = yearMonth.atDay(day) + val isHoliday = date in holidays || date.dayOfWeek == DayOfWeek.SATURDAY || date.dayOfWeek == DayOfWeek.SUNDAY + DateInfo(date, isHoliday) + } + } + + fun columnIndexToLetter(columnIndex: Int): String { + val letters = StringBuilder() + var index = columnIndex + + // Convert to Excel column letters (1-based index) + while (index > 0) { + val letterIndex = (index - 1) % 26 + letters.insert(0, ('A' + letterIndex).toChar()) + index = (index - letterIndex) / 26 + } + + return letters.toString() + } + + @Throws(IOException::class) + private fun createProjectMonthlyWorkbook( + yearMonth: YearMonth, + project: Project, + timesheets: List, + monthList: List, + templatePath: String, + ): Workbook { + val resource = ClassPathResource(templatePath) + val templateInputStream = resource.inputStream + val workbook: Workbook = XSSFWorkbook(templateInputStream) + + val monthStyle = workbook.createDataFormat().getFormat("MMM YYYY") + val formatter = DateTimeFormatter.ofPattern("MMM yyyy") + + // Style Related + val redFont = workbook.createFont().apply { + color = IndexedColors.RED.index + fontHeightInPoints = 12 + } + + val blackFont = workbook.createFont().apply { + color = IndexedColors.BLACK.index + fontHeightInPoints = 12 + } + + val redFontStyle = workbook.createCellStyle().apply { + setFont(redFont) + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + } + + val blackFontStyle = workbook.createCellStyle().apply { + setFont(blackFont) + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + } + + val yellowBackgroundStyle = workbook.createCellStyle().apply { + setFont(blackFont) + borderTop = BorderStyle.THIN + borderBottom = BorderStyle.THIN + borderLeft = BorderStyle.THIN + borderRight = BorderStyle.THIN + + fillForegroundColor = IndexedColors.YELLOW.index + fillPattern = FillPatternType.SOLID_FOREGROUND + } + + var sheet: Sheet = workbook.getSheetAt(0) + + val staffs : List = timesheets.mapNotNull { it.staff }.distinct() + val timesheetRecordsByStaff = timesheets.groupBy ({it.staff}, {it}) + .mapValues { (_, records) -> + records.groupBy( {it.recordDate}, {(it.normalConsumed ?: 0.0) + (it.otConsumed ?: 0.0)}) + .mapValues { it.value.sum() } + } + + + var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field + var columnIndex = 1 + + sheet.getRow(rowIndex).createCell(columnIndex+1).apply { + setCellValue(FORMATTED_TODAY) + } + + val startRowIndex = 6 + val startColumnIndex = 2 + + rowIndex = 2 + sheet.getRow(rowIndex).getCell(columnIndex+1).apply { + setCellValue(yearMonth.format(formatter)) + cellStyle.dataFormat = monthStyle + } + + rowIndex = 3 + sheet.getRow(rowIndex).getCell(columnIndex+1).apply { + setCellValue( "${project.code} - ${project.name}" ) + } + + // date header row + rowIndex = 5 + columnIndex = 2 + for (date in monthList){ + val row = sheet.getRow(rowIndex) + val dateCell = row.createCell(columnIndex) + dateCell.apply { + setCellValue(date.date?.format(DateTimeFormatter.ofPattern("dd"))) + cellStyle = blackFontStyle + if(date.isHoliday){ + cellStyle.apply { cellStyle = redFontStyle } + } + } + sheet.autoSizeColumn(columnIndex) + columnIndex++ + } + + // + rowIndex = 6 + timesheetRecordsByStaff.forEach{(staff, dates) -> + val row = sheet.getRow(rowIndex) + val staffIdCell = row.createCell(0) + val staffNameCell = row.createCell(1) + staffIdCell.apply { + setCellValue(staff?.staffId ?: "") + cellStyle =blackFontStyle + } + staffNameCell.apply { + setCellValue(staff?.name ?: "") + cellStyle =blackFontStyle + } + + + var recordDateCol = 2 + monthList.forEachIndexed { index, dateInfo -> + val recordDateByStaffCell = row.createCell(2+index) + recordDateByStaffCell.cellStyle = blackFontStyle + if(dates.keys.contains(dateInfo.date)){ + recordDateByStaffCell.setCellValue(dates.getValue(dateInfo.date)) + }else{ + recordDateByStaffCell.setCellValue("-") + } + recordDateCol++ + } + rowIndex++ + } + + val lastRowIndex = rowIndex + val totalRow = sheet.getRow(lastRowIndex) + + // calculate the sum of work hours spent per day + val numberOfColumnns = monthList.size + val lastColumnIndex = startColumnIndex + numberOfColumnns + for (colIndex in startColumnIndex until lastColumnIndex){ + val columnLetter = columnIndexToLetter(colIndex + 1) + val totalSumFormula = "SUM($columnLetter${6 + 1}:$columnLetter${lastRowIndex})" // + val totalSumCell = totalRow.createCell(colIndex) + totalSumCell.apply { + cellFormula = totalSumFormula + cellStyle = yellowBackgroundStyle + } + } + + // calculate the sum of work hour per staff + val startColumnLetter = columnIndexToLetter(startColumnIndex) + val lastColumnLetter = columnIndexToLetter(lastColumnIndex) + for (index in startRowIndex .. lastRowIndex){ + val staffTotalSumFormula = "SUM($startColumnLetter${index+1}:$lastColumnLetter${index+1})" // excel start from 1, index start from 0 + val staffTotalRow = sheet.getRow(index) + val staffTotalRowCell = staffTotalRow.createCell(lastColumnIndex) + staffTotalRowCell.apply { + cellFormula = staffTotalSumFormula + cellStyle = yellowBackgroundStyle + } + } + + return workbook + } + + fun genProjectMonthlyReport( + yearMonth: YearMonth, + projectId: Long, + holidays: List, + ): ByteArray{ + + val project = projectRepository.findById(projectId).get(); + + val year = yearMonth.year + val month = yearMonth.monthValue + val timesheetRecords : List = timesheetRepository.findByYearAndMonthAndProjectId(year, month, projectId) + val monthList = generateDatesForMonthWithHolidays(yearMonth, convertStringToLocalDate(holidays)) + + val workbook: Workbook = createProjectMonthlyWorkbook(yearMonth, project, timesheetRecords, monthList, PROJECT_MONTHLY_REPORT) + + val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() + workbook.write(outputStream) + workbook.close() + + return outputStream.toByteArray() + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt index d402bef..6131df8 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt @@ -35,6 +35,7 @@ import java.time.format.DateTimeFormatter import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.project.service.SubsidiaryService import com.ffii.tsms.modules.report.web.model.* +import com.ffii.tsms.modules.timesheet.entity.Timesheet import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory import org.springframework.data.domain.Example @@ -370,6 +371,22 @@ class ReportController( // return ResponseEntity.noContent().build() // } } + @PostMapping("/project-monthly-report") + fun getMonthDateList(@RequestBody @Valid request: ProjectMonthlyRequest){ + println("-------getMonthDateList----------") + println(request) + +// println(excelReportService.generateDatesForMonthWithHolidays(request.yearMonth, excelReportService.convertStringToLocalDate(request.holidays))) + } + + @PostMapping("/gen-project-monthly-report") + fun genProjectMonthlyReport(@RequestBody @Valid request: ProjectMonthlyRequest): ResponseEntity{ + val reportResult: ByteArray = excelReportService.genProjectMonthlyReport(request.yearMonth, request.projectId, request.holidays) + + return ResponseEntity.ok() + .header("filename", "Project Monthly Report - " + LocalDate.now() + ".xlsx") + .body(ByteArrayResource(reportResult)) + } // API for testing data of total cumulative expenditure // @GetMapping("/test") diff --git a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt index 1dae96a..9df7f6b 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt @@ -68,4 +68,10 @@ data class ProjectManhourSummaryRequest ( val startDate: LocalDate, val endDate: LocalDate, val teamId: Any +) + +data class ProjectMonthlyRequest ( + val projectId: Long, + val yearMonth: YearMonth, + val holidays: List ) \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt b/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt index 0e0caef..83c55f7 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt @@ -35,4 +35,7 @@ interface TimesheetRepository : AbstractRepository { fun findMinRecordDateByProjectId(projectId: Long): LocalDate? fun findAllByDeletedFalseAndRecordDate(recordDate: LocalDate): List + + @Query("SELECT t FROM Timesheet t WHERE YEAR(t.recordDate) = ?1 AND MONTH(t.recordDate) = ?2 and t.project.id = ?3 order by t.recordDate, t.staff.id") + fun findByYearAndMonthAndProjectId(year: Int, month: Int, projectId: Long): List } \ No newline at end of file diff --git a/src/main/resources/templates/report/AR09_Project Daily Work Hours Analysis Report.xlsx b/src/main/resources/templates/report/AR09_Project Daily Work Hours Analysis Report.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7f5a623f7c788b83acf45518b14f4c5bd138038d GIT binary patch literal 14538 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 EKQjJlr2qf` literal 0 HcmV?d00001