From 58dffe7af0930db22fb37faa976de3c371edeebc Mon Sep 17 00:00:00 2001 From: leoho2fi Date: Tue, 28 May 2024 16:45:10 +0800 Subject: [PATCH] report update --- .../modules/report/service/ReportService.kt | 309 ++++++++++++++---- .../modules/report/web/ReportController.kt | 90 ++--- .../modules/report/web/model/ReportRequest.kt | 4 +- 3 files changed, 301 insertions(+), 102 deletions(-) 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 bad39d3..ab4d097 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 @@ -20,6 +20,10 @@ import org.apache.poi.ss.util.CellAddress import org.apache.poi.ss.util.CellRangeAddress import org.apache.poi.ss.util.CellUtil import org.apache.poi.xssf.usermodel.XSSFWorkbook +import org.apache.poi.ss.usermodel.FormulaEvaluator +import org.apache.poi.ss.usermodel.CellStyle +import org.apache.poi.ss.usermodel.DateUtil +import org.apache.poi.ss.usermodel.CellType import org.hibernate.jdbc.Work import org.springframework.core.io.ClassPathResource import org.springframework.stereotype.Service @@ -30,6 +34,9 @@ import java.sql.Time import java.time.LocalDate import java.time.YearMonth import java.time.format.DateTimeFormatter +import java.time.ZoneId +import java.time.format.DateTimeParseException +import java.time.temporal.ChronoUnit import java.util.* @@ -215,18 +222,34 @@ open class ReportService( @Throws(IOException::class) fun generateLateStartReport( - team: Team, - customer: Customer, - project: List, + teamId: Long, + clientId: Long, + remainedDate:LocalDate, + remainedDateTo:LocalDate, + teams: List, + customers: List, ): ByteArray { + val projectDetails = getLateStartDetails(teamId, clientId, remainedDate, remainedDateTo) + + // Find team and customer names based on teamId and clientId for the Excel report headers + // val team = teamRepository.findById(teamId).orElse(null) + // val teams = if (team == null) listOf() else listOf(team) + // val customer = customerRepository.findById(clientId).orElse(null) + // val customers = if (customer == null) listOf() else listOf(customer) + val workbook: Workbook = createLateStartReport( - team, - customer, - project, - LATE_START_REPORT) + teams, + customers, + LATE_START_REPORT, // Ensure you provide the correct path to your template + teamId, + clientId, + projectDetails + ) + val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) workbook.close() + return outputStream.toByteArray() } @@ -1197,80 +1220,209 @@ open class ReportService( } //createLateStartReport - private fun createLateStartReport( - team: Team, - customer: Customer, - project: List, - templatePath: String - ):Workbook{ - - val resource = ClassPathResource(templatePath) - val templateInputStream = resource.inputStream - val workbook: Workbook = XSSFWorkbook(templateInputStream) - val sheet = workbook.getSheetAt(0) - - // Formatting the current date to "YYYY/MM/DD" and setting it to cell C2 - val formattedToday = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) - val dateCell = sheet.getRow(1)?.getCell(2) ?: sheet.getRow(1).createCell(2) - dateCell.setCellValue(formattedToday) - - // Start populating project data starting at row 7 - val startRow = 6 // 0-based index for row 7 - val projectDataRow = sheet.createRow(startRow) +private fun createLateStartReport( + team: List, + customer: List, + templatePath: String, + teamId: Long, + clientId: Long, + lateStartData: List> +): Workbook { + val resource = ClassPathResource(templatePath) + val templateInputStream = resource.inputStream + val workbook: Workbook = XSSFWorkbook(templateInputStream) + val sheet = workbook.getSheetAt(0) + val evaluator: FormulaEvaluator = workbook.creationHelper.createFormulaEvaluator() + + // Apply standard and custom fonts + applyStandardAndCustomFonts(workbook, sheet) + + // Formatting the date in cell C2 + setDateInCellC2(workbook, sheet) + + setTeamAndClientIds(sheet, team, teamId, customer, clientId) + + // Set data and conditional formatting in columns + setDataAndConditionalFormatting(workbook, sheet, lateStartData, evaluator) + + // Auto-size columns to fit content + autoSizeColumns(sheet) + + return workbook +} - // Styling for cell A1: Font size 16 and bold - val headerFont = workbook.createFont().apply { + private fun applyStandardAndCustomFonts(workbook: Workbook, sheet: Sheet) { + val timesNewRoman = workbook.createFont().apply { + fontName = "Times New Roman" + fontHeightInPoints = 12 bold = true + } + val timesNewRomanLarge = workbook.createFont().apply { + fontName = "Times New Roman" fontHeightInPoints = 16 + bold = true } - val headerCellStyle = workbook.createCellStyle().apply { - setFont(headerFont) + // Apply custom font size for A1 + val rowA1 = sheet.getRow(0) ?: sheet.createRow(0) + val cellA1 = rowA1.getCell(0) ?: rowA1.createCell(0) + val cellStyleA1 = workbook.createCellStyle().apply { + cloneStyleFrom(cellA1.cellStyle) + setFont(timesNewRomanLarge) } - val headerCell = sheet.getRow(0)?.getCell(0) ?: sheet.getRow(0).createCell(0) - headerCell.cellStyle = headerCellStyle - //headerCell.setCellValue("Report Title") - + cellA1.cellStyle = cellStyleA1 // Apply styles from A2 to A4 (bold) - val boldFont = workbook.createFont().apply { bold = true } - val boldCellStyle = workbook.createCellStyle().apply { setFont(boldFont) } + val boldCellStyle = workbook.createCellStyle().apply { setFont(timesNewRoman) } listOf(1, 2, 3).forEach { rowIndex -> - val row = sheet.getRow(rowIndex) - val cell = row?.getCell(0) ?: row.createCell(0) + val row = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex) + val cell = row.getCell(0) ?: row.createCell(0) cell.cellStyle = boldCellStyle } // Apply styles from A6 to J6 (bold, bottom border, center alignment) val styleA6ToJ6 = workbook.createCellStyle().apply { - setFont(boldFont) + setFont(timesNewRoman) // Use the standard bold font for consistency alignment = HorizontalAlignment.CENTER borderBottom = BorderStyle.THIN } - for (colIndex in 0..9) { - val cellAddress = CellAddress(5, colIndex) // Row 6 (0-based index), Columns A to J - val row = sheet.getRow(cellAddress.row) - val cell = row?.getCell(cellAddress.column) ?: row.createCell(cellAddress.column) + // Apply styleA6ToJ6 to cells A6 to J6 + val rowA6 = sheet.getRow(5) ?: sheet.createRow(5) + (0..9).forEach { colIndex -> + val cell = rowA6.getCell(colIndex) ?: rowA6.createCell(colIndex) cell.cellStyle = styleA6ToJ6 } + } + private fun setDateInCellC2(workbook: Workbook, sheet: Sheet) { + val formattedToday = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) + val dateCell = sheet.getRow(1)?.getCell(2) ?: sheet.getRow(1).createCell(2) + dateCell.setCellValue(formattedToday) + } - // Setting column widths dynamically based on content length (example logic) - val maxContentWidths = IntArray(10) { 8 } // Initial widths for A to J - for (rowIndex in 0..sheet.lastRowNum) { - val row = sheet.getRow(rowIndex) - for (colIndex in 0..9) { - val cell = row.getCell(colIndex) - if (cell != null) { - val length = cell.toString().length - if (length > maxContentWidths[colIndex]) { - maxContentWidths[colIndex] = length - } - } - } + private fun setDataAndConditionalFormatting( + workbook: Workbook, + sheet: Sheet, + lateStartData: List>, + evaluator: FormulaEvaluator + ) { + var dataRowIndex = 6 // Starting from row 7 + lateStartData.forEachIndexed { index, data -> + val row = sheet.getRow(dataRowIndex) ?: sheet.createRow(dataRowIndex) + populateDataRow(workbook, sheet, row, data, index) + dataRowIndex++ } - for (colIndex in 0..9) { - sheet.setColumnWidth(colIndex, (maxContentWidths[colIndex] + 2) * 256) // Set the width for each column + + // Update conditional formatting to apply to the new range of column J + val sheetCF = sheet.sheetConditionalFormatting + val rule1 = sheetCF.createConditionalFormattingRule("J7 < 60") + val pattern1 = rule1.createPatternFormatting().apply { + fillBackgroundColor = IndexedColors.RED.index + fillPattern = PatternFormatting.SOLID_FOREGROUND + } + val rule2 = sheetCF.createConditionalFormattingRule("J7 >= 60") + val pattern2 = rule2.createPatternFormatting().apply { + fillBackgroundColor = IndexedColors.YELLOW.index + fillPattern = PatternFormatting.SOLID_FOREGROUND + } + val region = CellRangeAddress(dataRowIndex - lateStartData.size, dataRowIndex - 1, 9, 9) // Column J + sheetCF.addConditionalFormatting(arrayOf(region), arrayOf(rule1, rule2)) + } + private fun setTeamAndClientIds(sheet: Sheet, team: List, teamId: Long, customer: List, clientId: Long) { + // Set the 'teamId' value in cell C3 + val teamIdRow = sheet.getRow(2) ?: sheet.createRow(2) + val teamIdCell = teamIdRow.getCell(2) ?: teamIdRow.createCell(2) + teamIdCell.setCellValue(if (teamId == 0L) "All" else team.getOrNull(0)?.name ?: "Unknown") + + // Set the 'clientId' value in cell C4 + val clientIdRow = sheet.getRow(3) ?: sheet.createRow(3) + val clientIdCell = clientIdRow.getCell(2) ?: clientIdRow.createCell(2) + clientIdCell.setCellValue(if (clientId == 0L) "All" else customer.getOrNull(0)?.name ?: "Unknown") + } + + + private fun populateDataRow(workbook: Workbook, sheet: Sheet, row: Row, data: Map, index: Int) { + val dateFormat = workbook.creationHelper.createDataFormat().getFormat("yyyy/MM/dd") + // Define the font for general use (if not already defined elsewhere) + val generalFont = workbook.createFont().apply { + fontName = "Times New Roman" + fontHeightInPoints = 12 // Standard size + } + val dateCellStyle = workbook.createCellStyle().apply { + dataFormat = dateFormat + setFont(generalFont) // Apply the font here along with the date format + } + // Create a style for center alignment for column G + val centerAlignedStyle = workbook.createCellStyle().apply { + alignment = HorizontalAlignment.CENTER + setFont(generalFont) // Maintain font consistency } - return workbook + // Column A: Sequential ID + val idCell = row.createCell(0) + idCell.setCellValue((index + 1).toDouble()) // Set as double + + // Column B: Project Code + val projectCodeCell = row.createCell(1) + projectCodeCell.setCellValue(data["project_code"] as String) + + // Column C: Project Name + val projectNameCell = row.createCell(2) + projectNameCell.setCellValue(data["project_name"] as String) + + // Column D: Team Name + val teamNameCell = row.createCell(3) + teamNameCell.setCellValue(data["team_name"] as String) + + // Column E: Customer Name + val customerNameCell = row.createCell(4) + customerNameCell.setCellValue(data["customer_name"] as String) + +// // Column F: Project Plan Start Date +// val projectPlanStartCell = row.createCell(5) +// projectPlanStartCell.setCellValue(data["project_plan_start"].toString()) + + // Parse the project start date and apply the date format + val projectPlanStartCell = row.createCell(5) + try { + val date = LocalDate.parse(data["project_plan_start"].toString(), DateTimeFormatter.ISO_LOCAL_DATE) + projectPlanStartCell.setCellValue(Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant())) + } catch (e: DateTimeParseException) { + projectPlanStartCell.setCellValue("Invalid date") + } + projectPlanStartCell.cellStyle = dateCellStyle + + // Column G: Calculated Date Difference (days since project start from today) + val daysDifferenceCell = row.createCell(6) + daysDifferenceCell.setCellFormula("C$2-F${row.rowNum + 1}") + daysDifferenceCell.cellStyle = centerAlignedStyle + + // Column H: Task Group Name + val taskGroupNameCell = row.createCell(7) + taskGroupNameCell.setCellValue(data["task_group_name"] as String) + +// // Column I: Milestone End Date + val dateCell = row.createCell(8) // Milestone end date in column I + try { + val date = LocalDate.parse(data["milestone_end_date"].toString()) + dateCell.setCellValue(Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant())) + } catch (e: DateTimeParseException) { + dateCell.setCellValue("Invalid date") + } + dateCell.cellStyle = dateCellStyle + + // Get the current date and parse the milestone end date + val today = LocalDate.now() + val endDate = LocalDate.parse(data["milestone_end_date"].toString(), DateTimeFormatter.ISO_LOCAL_DATE) + val daysBetween = ChronoUnit.DAYS.between(today, endDate).toInt() // Calculate the difference in days + + val daysDifferenceCellJ = row.createCell(9) + daysDifferenceCellJ.setCellValue(daysBetween.toDouble()) // Set as double for consistency + daysDifferenceCellJ.cellStyle = centerAlignedStyle + + } + + private fun autoSizeColumns(sheet: Sheet) { + for (colIndex in 0..9) { + sheet.autoSizeColumn(colIndex) + } } open fun getFinancialStatus(teamLeadId: Long?): List> { @@ -1989,4 +2141,41 @@ open class ReportService( return costAndExpenseList } + open fun getLateStartDetails(teamId: Long?, clientId: Long?, remainedDate: LocalDate, remainedDateTo: LocalDate): List> { + val sql = StringBuilder(""" + SELECT + p.code AS project_code, + p.name AS project_name, + t.name AS team_name, + c.name AS customer_name, + p.planStart AS project_plan_start, + tg.name AS task_group_name, + m.endDate AS milestone_end_date + FROM + project p + LEFT JOIN team t ON p.teamLead = t.teamLead + LEFT JOIN customer c ON p.customerId = c.id + LEFT JOIN task_group tg ON tg.id = (SELECT taskGroupId FROM milestone WHERE projectId = p.id LIMIT 1) + LEFT JOIN milestone m ON m.taskGroupId = tg.id AND m.projectId = p.id + WHERE + p.status = 'Pending to Start' + AND p.planStart < CURRENT_DATE + AND m.endDate BETWEEN :remainedDate AND :remainedDateTo + """.trimIndent()) + + if (teamId != null && teamId > 0) { + sql.append(" AND t.id = :teamId") + } + if (clientId != null && clientId > 0) { + sql.append(" AND c.id = :clientId") + } + + val args = mutableMapOf( + "teamId" to teamId, + "clientId" to clientId, + "remainedDate" to remainedDate, + "remainedDateTo" to remainedDateTo + ) + return jdbcDao.queryForList(sql.toString(), args) + } } 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 b6be334..758228f 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 @@ -173,51 +173,30 @@ class ReportController( @PostMapping("/downloadLateStartReport") @Throws(ServletRequestBindingException::class, IOException::class) -// fun downloadLateStartReport(@RequestBody @Valid request: LateStartReportRequest): ResponseEntity { -// //val reportDate = projectRepository.findAllByPlanStartLessThanEqualAndPlanEndGreaterThanEqual().orElseThrow()//to do -// val team = teamRepository.findById(request.teamId).orElseThrow() -// //val customer = customerRepository.findById(request.clientId).orElseThrow() +//ReportController.kt + fun downloadLateStartReport(@RequestBody @Valid request: LateStartReportRequest): ResponseEntity { + val team = teamRepository.findById(request.teamId).orElse(null) + val teams = if (team == null) teamRepository.findAll() else listOf(team) + val customer = customerRepository.findById(request.clientId).orElse(null) + val customers = if (customer == null) customerRepository.findAll() else listOf(customer) + val projects = projectRepository.findAllByPlanStartLessThanEqualAndPlanEndGreaterThanEqual(request.remainedDate, request.remainedDateTo) -// val reportResult: ByteArray = excelReportService.generateLateStartReport(team,customer,reportDate) -// val headers = HttpHeaders() -// val filename = "Late_Start_Report_${LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))}.xlsx" -// headers.add("filename", "${URLEncoder.encode(filename, "UTF-8")}") -// // Logging the filename to ensure it's correctly formatted -// println("Generated filename for download: $filename") - -// return ResponseEntity.ok() -// .headers(headers) -// .contentLength(reportResult.size.toLong()) -// .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) -// .body(ByteArrayResource(reportResult)) -// } - -fun downloadLateStartReport(@RequestBody @Valid request: LateStartReportRequest): ResponseEntity { - val team = teamRepository.findById(request.teamId).orElseThrow { - Exception("Team not found with ID: ${request.teamId}") - } - - val customer = customerRepository.findByName(request.customer).orElseThrow() - // ?: throw Exception("Customer not found with name: ${request.customer}") - - val projects = projectRepository.findAllByPlanStartLessThanEqualAndPlanEndGreaterThanEqual(request.remainedDateFrom, request.remainedDateTo) - if (projects.isEmpty()) { - throw Exception("No projects found for the given date: ${request.reportDate}") + val reportResult: ByteArray = try { + excelReportService.generateLateStartReport(request.teamId, request.clientId, + request.remainedDate, request.remainedDateTo,teams,customers) + } catch (e: Exception) { + throw Exception("Error generating report", e) } - val reportResult: ByteArray = try { - excelReportService.generateLateStartReport(team, customer, projects) - } catch (e: Exception) { - throw Exception("Error generating report", e) - } - val headers = HttpHeaders() - val formattedDate = request.reportDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + val currentDate = LocalDate.now() // Get the current date + val formattedDate = currentDate.format(DateTimeFormatter.ofPattern("yyyy_MM_dd")) // Format the current date val filename = "Late_Start_Report_$formattedDate.xlsx" - headers.add("Content-Disposition", "attachment; filename=${URLEncoder.encode(filename, "UTF-8")}") - + //headers.add("Content-Disposition", "attachment; filename=filename}") + headers.add("filename", "${URLEncoder.encode(filename, "UTF-8")}") + println("Generated filename for download: $filename") - + return ResponseEntity.ok() .headers(headers) .contentLength(reportResult.size.toLong()) @@ -225,6 +204,39 @@ fun downloadLateStartReport(@RequestBody @Valid request: LateStartReportRequest) .body(ByteArrayResource(reportResult)) } +//fun downloadLateStartReport(@RequestBody @Valid request: LateStartReportRequest): ResponseEntity { +// val team = teamRepository.findById(request.teamId).orElseThrow { +// Exception("Team not found with ID: ${request.teamId}") +// } +// +// val customer = customerRepository.findByName(request.customer).orElseThrow() +// // ?: throw Exception("Customer not found with name: ${request.customer}") +// +// val projects = projectRepository.findAllByPlanStartLessThanEqualAndPlanEndGreaterThanEqual(request.remainedDateFrom, request.remainedDateTo) +// if (projects.isEmpty()) { +// throw Exception("No projects found for the given date: ${request.reportDate}") +// } +// +// val reportResult: ByteArray = try { +// excelReportService.generateLateStartReport(team, customer, projects) +// } catch (e: Exception) { +// throw Exception("Error generating report", e) +// } +// +// val headers = HttpHeaders() +// val formattedDate = request.reportDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) +// val filename = "Late_Start_Report_$formattedDate.xlsx" +// headers.add("Content-Disposition", "attachment; filename=${URLEncoder.encode(filename, "UTF-8")}") +// +// println("Generated filename for download: $filename") +// +// return ResponseEntity.ok() +// .headers(headers) +// .contentLength(reportResult.size.toLong()) +// .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) +// .body(ByteArrayResource(reportResult)) +// } + // For API TESTING @GetMapping("/financialReport/{id}") fun getFinancialReport(@PathVariable id: Long): List> { 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 70671a7..acbb1a9 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 @@ -26,10 +26,8 @@ data class StaffMonthlyWorkHourAnalysisReportRequest ( data class LateStartReportRequest ( val teamId: Long, val clientId: Long, - val remainedDateFrom: LocalDate, + val remainedDate: LocalDate, val remainedDateTo: LocalDate, - val customer: String, - val reportDate: LocalDate ) data class ProjectResourceOverconsumptionReport ( val teamId: Long?,