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 0000000..7f5a623 Binary files /dev/null and b/src/main/resources/templates/report/AR09_Project Daily Work Hours Analysis Report.xlsx differ