From 03fc7e8c2f1e93d9efb5b6d6076862b48f45210e Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Fri, 24 May 2024 12:27:40 +0800 Subject: [PATCH 01/21] update report & fix project --- .../project/entity/ProjectRepository.kt | 7 +- .../project/entity/ProjectTaskRepository.kt | 1 + .../modules/report/service/ReportService.kt | 681 +++++++++++------- .../modules/report/web/ReportController.kt | 37 +- .../modules/report/web/model/ReportRequest.kt | 5 + .../report/AR02_Delay Report v02.xlsx | Bin 15065 -> 12399 bytes 6 files changed, 477 insertions(+), 254 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt index 57d1720..8f367a8 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt @@ -1,10 +1,13 @@ package com.ffii.tsms.modules.project.entity; import com.ffii.core.support.AbstractRepository +import com.ffii.tsms.modules.data.entity.Customer +import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.project.entity.projections.InvoiceInfoSearchInfo import com.ffii.tsms.modules.project.entity.projections.InvoiceSearchInfo import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo import org.springframework.data.jpa.repository.Query +import org.springframework.lang.Nullable import java.io.Serializable import java.time.LocalDate @@ -25,8 +28,10 @@ interface ProjectRepository : AbstractRepository { "") fun getLatestCodeNumberByMainProject(isClpProject: Boolean, id: Serializable?): Long? - @Query("SELECT max(case when length(p.code) - length(replace(p.code, '-', '')) > 1 then cast(substring_index(p.code, '-', -1) as long) end) FROM Project p WHERE p.code like ?1 and p.id != ?2") + @Query("SELECT max(case when length(p.code) - length(replace(p.code, '-', '')) > 1 then cast(substring_index(p.code, '-', -1) as long) end) FROM Project p WHERE p.code like %?1% and p.id != ?2") fun getLatestCodeNumberBySubProject(code: String, id: Serializable?): Long? fun findAllByStatusIsNotAndMainProjectIsNull(status: String): List + + fun findAllByTeamLeadAndCustomer(teamLead: Staff, customer: Customer): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectTaskRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectTaskRepository.kt index b1b57e0..085ed7e 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectTaskRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectTaskRepository.kt @@ -5,5 +5,6 @@ import com.ffii.core.support.AbstractRepository interface ProjectTaskRepository : AbstractRepository { fun findAllByProject(project: Project): List + fun findAllByProjectIn(projects: List): List fun findByProjectAndTask(project: Project, task: Task): ProjectTask? } \ No newline at end of file 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..9ea3ebd 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 @@ -6,6 +6,7 @@ import com.ffii.tsms.modules.data.entity.Salary import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.Team import com.ffii.tsms.modules.project.entity.Invoice +import com.ffii.tsms.modules.project.entity.Milestone import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.timesheet.entity.Leave import com.ffii.tsms.modules.timesheet.entity.Timesheet @@ -47,6 +48,7 @@ open class ReportService( private val PandL_REPORT = "templates/report/AR07_Project P&L Report v02.xlsx" private val FINANCIAL_STATUS_REPORT = "templates/report/EX01_Financial Status Report.xlsx" private val PROJECT_CASH_FLOW_REPORT = "templates/report/EX02_Project Cash Flow Report.xlsx" + private val PROJECT_POTENTIAL_DELAY_REPORT = "templates/report/AR02_Delay Report v02.xlsx" private val MONTHLY_WORK_HOURS_ANALYSIS_REPORT = "templates/report/AR08_Monthly Work Hours Analysis Report.xlsx" private val SALART_LIST_TEMPLATE = "templates/report/Salary Template.xlsx" private val LATE_START_REPORT = "templates/report/AR01_Late Start Report v01.xlsx" @@ -64,33 +66,35 @@ open class ReportService( val tempList = mutableListOf>() - for (item in financialStatus){ - val normalConsumed = item.getValue("normalConsumed") as Double - val hourlyRate = item.getValue("hourlyRate") as BigDecimal + for (item in financialStatus) { + val normalConsumed = item.getValue("normalConsumed") as Double + val hourlyRate = item.getValue("hourlyRate") as BigDecimal // println("normalConsumed------------- $normalConsumed") // println("hourlyRate------------- $hourlyRate") val manHourRate = normalConsumed.toBigDecimal().multiply(hourlyRate) // println("manHourRate------------ $manHourRate") - val otConsumed = item.getValue("otConsumed") as Double + val otConsumed = item.getValue("otConsumed") as Double val manOtHourRate = otConsumed.toBigDecimal().multiply(hourlyRate).multiply(otFactor) - if(!tempList.any{ it.containsValue(item.getValue("code"))}){ - - tempList.add(mapOf( - "code" to item.getValue("code"), - "description" to item.getValue("description"), - "client" to item.getValue("client"), - "teamLead" to item.getValue("teamLead"), - "planStart" to item.getValue("planStart"), - "planEnd" to item.getValue("planEnd"), - "expectedTotalFee" to item.getValue("expectedTotalFee"), - "normalConsumed" to manHourRate, - "otConsumed" to manOtHourRate, - "issuedAmount" to item.getValue("sumIssuedAmount"), - "paidAmount" to item.getValue("sumPaidAmount"), - )) - }else{ + if (!tempList.any { it.containsValue(item.getValue("code")) }) { + + tempList.add( + mapOf( + "code" to item.getValue("code"), + "description" to item.getValue("description"), + "client" to item.getValue("client"), + "teamLead" to item.getValue("teamLead"), + "planStart" to item.getValue("planStart"), + "planEnd" to item.getValue("planEnd"), + "expectedTotalFee" to item.getValue("expectedTotalFee"), + "normalConsumed" to manHourRate, + "otConsumed" to manOtHourRate, + "issuedAmount" to item.getValue("sumIssuedAmount"), + "paidAmount" to item.getValue("sumPaidAmount"), + ) + ) + } else { // Find the existing Map in the tempList that has the same "code" value val existingMap = tempList.find { it.containsValue(item.getValue("code")) }!! @@ -121,7 +125,32 @@ open class ReportService( dateType: String ): ByteArray { // Generate the Excel report with query results - val workbook: Workbook = createProjectCashFlowReport(project, invoices, timesheets, dateType, PROJECT_CASH_FLOW_REPORT) + val workbook: Workbook = + createProjectCashFlowReport(project, invoices, timesheets, dateType, PROJECT_CASH_FLOW_REPORT) + + // Write the workbook to a ByteArrayOutputStream + val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() + workbook.write(outputStream) + workbook.close() + + return outputStream.toByteArray() + } + + @Throws(IOException::class) + fun generateProjectPotentialDelayReport( + searchedTeam: String, + searchedClient: String, + projects: List, + timesheets: List, + ): ByteArray { + // Generate the Excel report with query results + val workbook: Workbook = createProjectPotentialDelayReport( + searchedTeam, + searchedClient, + projects, + timesheets, + PROJECT_POTENTIAL_DELAY_REPORT + ) // Write the workbook to a ByteArrayOutputStream val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() @@ -223,7 +252,8 @@ open class ReportService( team, customer, project, - LATE_START_REPORT) + LATE_START_REPORT + ) val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) workbook.close() @@ -237,20 +267,20 @@ open class ReportService( templatePath: String, projects: List>, teamLeadId: Long - ) : Workbook { + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) - val accountingStyle = workbook.createDataFormat() .getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") + val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") val sheet = workbook.getSheetAt(0) //Set Column 2, 3, 4 to auto width - sheet.setColumnWidth(2, 20*256) - sheet.setColumnWidth(3, 45*256) - sheet.setColumnWidth(4, 15*256) + sheet.setColumnWidth(2, 20 * 256) + sheet.setColumnWidth(3, 45 * 256) + sheet.setColumnWidth(4, 15 * 256) val boldFont = sheet.workbook.createFont() boldFont.bold = true @@ -259,7 +289,7 @@ open class ReportService( boldFontCellStyle.setFont(boldFont) var rowNum = 14 - if (projects.isEmpty()){ + if (projects.isEmpty()) { // Fill the cell in Row 2-12 with thr calculated sum rowNum = 1 val row1: Row = sheet.getRow(rowNum) @@ -269,12 +299,13 @@ open class ReportService( rowNum = 2 val row2: Row = sheet.getRow(rowNum) val row2Cell = row2.createCell(2) - val sql = StringBuilder("select" - + " t.teamLead as id," - + " t.name, t.code " - + " from team t" - + " where t.deleted = false " - + " and t.teamLead = :teamLead " + val sql = StringBuilder( + "select" + + " t.teamLead as id," + + " t.name, t.code " + + " from team t" + + " where t.deleted = false " + + " and t.teamLead = :teamLead " ) val args = mapOf("teamLead" to teamLeadId) val team = jdbcDao.queryForMap(sql.toString(), args).get() @@ -292,14 +323,16 @@ open class ReportService( return workbook } - for(item in projects){ + for (item in projects) { val row: Row = sheet.createRow(rowNum++) val codeCell = row.createCell(0) codeCell.setCellValue(if (item["code"] != null) item.getValue("code").toString() else "N/A") val descriptionCell = row.createCell(1) - descriptionCell.setCellValue(if (item["description"] != null) item.getValue("description").toString() else "N/A") + descriptionCell.setCellValue( + if (item["description"] != null) item.getValue("description").toString() else "N/A" + ) val clientCell = row.createCell(2) clientCell.apply { @@ -333,7 +366,7 @@ open class ReportService( val normalConsumed = item["normalConsumed"]?.let { it as BigDecimal } ?: BigDecimal(0) val otConsumed = item["otConsumed"]?.let { it as BigDecimal } ?: BigDecimal(0) val cumExpenditure = normalConsumed.add(otConsumed) - cumExpenditureCell.apply{ + cumExpenditureCell.apply { setCellValue(cumExpenditure.toDouble()) cellStyle.dataFormat = accountingStyle } @@ -354,7 +387,8 @@ open class ReportService( val uninvoiceCell = row.createCell(11) uninvoiceCell.apply { - cellFormula = " IF(H${rowNum}<=I${rowNum}, H${rowNum}-K${rowNum}, IF(AND(H${rowNum}>I${rowNum}, I${rowNum}I${rowNum}, I${rowNum}>=K${rowNum}), I${rowNum}-K${rowNum}, 0))) " + cellFormula = + " IF(H${rowNum}<=I${rowNum}, H${rowNum}-K${rowNum}, IF(AND(H${rowNum}>I${rowNum}, I${rowNum}I${rowNum}, I${rowNum}>=K${rowNum}), I${rowNum}-K${rowNum}, 0))) " cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } @@ -366,7 +400,7 @@ open class ReportService( val receivedAmountCell = row.createCell(13) val paidAmount = item["paidAmount"]?.let { it as BigDecimal } ?: BigDecimal(0) - receivedAmountCell.apply{ + receivedAmountCell.apply { setCellValue(paidAmount.toDouble()) cellStyle.dataFormat = accountingStyle } @@ -382,18 +416,18 @@ open class ReportService( val lastRowNum = rowNum + 1 val row: Row = sheet.createRow(rowNum) - for(i in 0..4){ + for (i in 0..4) { val cell = row.createCell(i) - CellUtil.setCellStyleProperty(cell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(cell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(cell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(cell, "borderBottom", BorderStyle.DOUBLE) } val subTotalCell = row.createCell(5) subTotalCell.apply { setCellValue("Sub-total:") } - CellUtil.setCellStyleProperty(subTotalCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(subTotalCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(subTotalCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(subTotalCell, "borderBottom", BorderStyle.DOUBLE) val sumTotalFeeCell = row.createCell(6) @@ -401,24 +435,24 @@ open class ReportService( cellFormula = "SUM(G15:G${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumTotalFeeCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumTotalFeeCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumTotalFeeCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumTotalFeeCell, "borderBottom", BorderStyle.DOUBLE) val sumBudgetCell = row.createCell(7) sumBudgetCell.apply { cellFormula = "SUM(H15:H${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumBudgetCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumBudgetCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumBudgetCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumBudgetCell, "borderBottom", BorderStyle.DOUBLE) val sumCumExpenditureCell = row.createCell(8) sumCumExpenditureCell.apply { cellFormula = "SUM(I15:I${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumCumExpenditureCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumCumExpenditureCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumCumExpenditureCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumCumExpenditureCell, "borderBottom", BorderStyle.DOUBLE) val sumBudgetVCell = row.createCell(9) sumBudgetVCell.apply { @@ -426,16 +460,16 @@ open class ReportService( cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumBudgetVCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumBudgetVCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumBudgetVCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumBudgetVCell, "borderBottom", BorderStyle.DOUBLE) val sumIInvoiceCell = row.createCell(10) sumIInvoiceCell.apply { cellFormula = "SUM(K15:K${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumIInvoiceCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumIInvoiceCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumIInvoiceCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumIInvoiceCell, "borderBottom", BorderStyle.DOUBLE) val sumUInvoiceCell = row.createCell(11) sumUInvoiceCell.apply { @@ -443,20 +477,20 @@ open class ReportService( cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumUInvoiceCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumUInvoiceCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumUInvoiceCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumUInvoiceCell, "borderBottom", BorderStyle.DOUBLE) val lastCpiCell = row.createCell(12) - CellUtil.setCellStyleProperty(lastCpiCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(lastCpiCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(lastCpiCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(lastCpiCell, "borderBottom", BorderStyle.DOUBLE) val sumRAmountCell = row.createCell(13) sumRAmountCell.apply { cellFormula = "SUM(N15:N${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumRAmountCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumRAmountCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumRAmountCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumRAmountCell, "borderBottom", BorderStyle.DOUBLE) val sumUnSettleCell = row.createCell(14) sumUnSettleCell.apply { @@ -464,8 +498,8 @@ open class ReportService( cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumUnSettleCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumUnSettleCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumUnSettleCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumUnSettleCell, "borderBottom", BorderStyle.DOUBLE) // Fill the cell in Row 2-12 with thr calculated sum rowNum = 1 @@ -478,7 +512,7 @@ open class ReportService( val row2Cell = row2.createCell(2) if (teamLeadId < 0) { row2Cell.setCellValue("All") - }else{ + } else { row2Cell.apply { cellFormula = "D15" } @@ -639,7 +673,8 @@ open class ReportService( rowIndex = 15 - val dateFormatter = if (dateType == "Date") DateTimeFormatter.ofPattern("yyyy/MM/dd") else DateTimeFormatter.ofPattern("MMM YYYY") + val dateFormatter = + if (dateType == "Date") DateTimeFormatter.ofPattern("yyyy/MM/dd") else DateTimeFormatter.ofPattern("MMM YYYY") val combinedResults = (invoices.map { it.receiptDate } + timesheets.map { it.recordDate }).filterNotNull().sortedBy { it } .map { it.format(dateFormatter) }.distinct() @@ -648,9 +683,11 @@ open class ReportService( .mapValues { (_, timesheetEntries) -> timesheetEntries.map { timesheet -> if (timesheet.normalConsumed != null) { - timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0).times(timesheet.staff!!.salary.hourlyRate.toDouble()) + timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) + .times(timesheet.staff!!.salary.hourlyRate.toDouble()) } else if (timesheet.otConsumed != null) { - timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0).times(timesheet.staff!!.salary.hourlyRate.toDouble()) + timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) + .times(timesheet.staff!!.salary.hourlyRate.toDouble()) } else { 0.0 } @@ -765,6 +802,136 @@ open class ReportService( return workbook } + @Throws(IOException::class) + private fun createProjectPotentialDelayReport( + searchedTeam: String, + searchedClient: String, + projects: List, + timesheets: List, + templatePath: String, + ): Workbook { + // please create a new function for each report template + val resource = ClassPathResource(templatePath) + val templateInputStream = resource.inputStream + val workbook: Workbook = XSSFWorkbook(templateInputStream) + + val sheet: Sheet = workbook.getSheetAt(0) + + var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field + var columnIndex = 2 + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue(FORMATTED_TODAY) + } + + rowIndex = 2 + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue(searchedTeam) + } + + rowIndex = 3 + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue(searchedClient) + } + + val groupedTimesheets = timesheets + .groupBy { timesheetEntry -> + Pair( + timesheetEntry.projectTask?.project?.id, + timesheetEntry.projectTask?.milestone?.id + ) + } + .mapValues { (_, timesheetEntries) -> + timesheetEntries.map { timesheet -> + if (timesheet.normalConsumed != null) { + timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) + } else if (timesheet.otConsumed != null) { + timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) + } else { + 0.0 + } + } + } + +// groupedTimesheets.entries.forEach { (key, value) -> +// logger.info("key: $key") +// logger.info(key == Pair(1L, 1L)) +// logger.info("value: " + value.sumOf { it }) +// } + + rowIndex = 6 + projects.forEach { project: Project -> + + sheet.createRow(rowIndex).apply { + createCell(0).apply { + setCellValue((rowIndex - 5).toString()) + } + + createCell(1).apply { + setCellValue(project.code) + } + + createCell(2).apply { + setCellValue(project.name) + } + + createCell(3).apply { + val currentTeam = project.teamLead?.team + setCellValue(currentTeam?.code + " - " + currentTeam?.name) + } + + createCell(4).apply { + val currentClient = project.customer + setCellValue(currentClient?.code + " - " + currentClient?.name) + } + + createCell(5).apply { + setCellValue(project.actualStart?.format(DATE_FORMATTER)) + } + + createCell(6).apply { + setCellValue(project.planEnd?.format(DATE_FORMATTER)) + } + } + + project.milestones.forEach { milestone: Milestone -> + logger.info(milestone.id) + + val tempRow = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex) + rowIndex++ + + tempRow.apply { + createCell(7).apply { + setCellValue(milestone.taskGroup?.name ?: "N/A") + } + + createCell(8).apply { + setCellValue(milestone.endDate?.format(DATE_FORMATTER) ?: "N/A") + } + + createCell(9).apply { + cellStyle.dataFormat = workbook.createDataFormat().getFormat("0.00%") + + if (groupedTimesheets.containsKey(Pair(project.id, milestone.id))) { + val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]!!.sum() + + logger.info("manHoursSpent: $manHoursSpent") + logger.info("milestone.stagePercentAllocation: " + milestone.stagePercentAllocation) + logger.info("project.totalManhour: " + project.totalManhour) + + val resourceUtilization = + manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) + setCellValue(resourceUtilization ?: 0.0) + } else { + setCellValue(0.0) + } + } + } + } + } + + return workbook + } + fun getColumnAlphabet(colIndex: Int): String { val alphabet = StringBuilder() @@ -796,14 +963,16 @@ open class ReportService( // result = timesheet record mapped var result: Map = mapOf() if (timesheets.isNotEmpty()) { - projectList = timesheets.map{ "${it["code"]}\n ${it["name"]}"}.toList().distinct() + projectList = timesheets.map { "${it["code"]}\n ${it["name"]}" }.toList().distinct() result = timesheets.groupBy( { it["id"].toString() }, - { mapOf( - "date" to it["recordDate"], - "normalConsumed" to it["normalConsumed"], - "otConsumed" to it["otConsumed"], - ) } + { + mapOf( + "date" to it["recordDate"], + "normalConsumed" to it["normalConsumed"], + "otConsumed" to it["otConsumed"], + ) + } ) } println("---result---") @@ -887,7 +1056,7 @@ open class ReportService( rowIndex += 1 tempCell = sheet.getRow(rowIndex).createCell(0) - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) tempCell.setCellValue("Sub-total") tempCell.cellStyle = boldStyle CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) @@ -915,7 +1084,7 @@ open class ReportService( tempCell.setCellValue(normalConsumed) tempCell.cellStyle.dataFormat = accountingStyle - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) // rowIndex += 1 tempCell = sheet.getRow(rowIndex).createCell(0) @@ -935,7 +1104,7 @@ open class ReportService( tempCell.setCellValue("Total Leave Hours") tempCell.cellStyle = boldStyle CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) // cal total leave hour if (leaves.isNotEmpty()) { leaves.forEach { l -> @@ -958,11 +1127,11 @@ open class ReportService( CellUtil.setCellStyleProperties(tempCell, DoubleBorderBottom) tempCell = sheet.getRow(rowIndex).createCell(2) - tempCell.cellFormula = "C${rowIndex-2}+C${rowIndex-1}" + tempCell.cellFormula = "C${rowIndex - 2}+C${rowIndex - 1}" tempCell.cellStyle.dataFormat = accountingStyle CellUtil.setCellStyleProperties(tempCell, DoubleBorderBottom) - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// rowIndex = 7 columnIndex = 2 @@ -986,13 +1155,13 @@ open class ReportService( tempCell.cellStyle.dataFormat = accountingStyle } } - result.forEach{ (id, list) -> + result.forEach { (id, list) -> val temp: List> = list as List> - temp.forEachIndexed { index, _ -> - dayInt = temp[index]["date"].toString().toInt() - tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) - tempCell.setCellValue((temp[index]["normalConsumed"] as Double) + (temp[index]["otConsumed"] as Double)) - } + temp.forEachIndexed { index, _ -> + dayInt = temp[index]["date"].toString().toInt() + tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) + tempCell.setCellValue((temp[index]["normalConsumed"] as Double) + (temp[index]["otConsumed"] as Double)) + } columnIndex++ } } @@ -1009,34 +1178,35 @@ open class ReportService( tempCell.setCellValue(leave["leaveHours"] as Double) } } - ///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// - tempCell = sheet.getRow(rowIndex).createCell(columnIndex) - tempCell.setCellValue("Leave Hours") - tempCell.cellStyle = boldStyle - CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) - CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) - CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) + ///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// + tempCell = sheet.getRow(rowIndex).createCell(columnIndex) + tempCell.setCellValue("Leave Hours") + tempCell.cellStyle = boldStyle + CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) + CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) + CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) columnIndex += 1 - tempCell = sheet.getRow(rowIndex).createCell(columnIndex) - tempCell.setCellValue("Daily Manhour Spent\n(Excluding Leave Hours)") - tempCell.cellStyle = boldStyle - CellUtil.setAlignment(tempCell, HorizontalAlignment.LEFT) - CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) + tempCell = sheet.getRow(rowIndex).createCell(columnIndex) + tempCell.setCellValue("Daily Manhour Spent\n(Excluding Leave Hours)") + tempCell.cellStyle = boldStyle + CellUtil.setAlignment(tempCell, HorizontalAlignment.LEFT) + CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) - sheet.addMergedRegion(CellRangeAddress(6,6 , 2, columnIndex)) + sheet.addMergedRegion(CellRangeAddress(6, 6, 2, columnIndex)) ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// rowIndex = 8 if (sheet.getRow(rowIndex - 1).getCell(2).stringCellValue != "Leave Hours") { // cal daily spent manhour for (i in 0 until rowSize) { tempCell = sheet.getRow(rowIndex).createCell(columnIndex) - tempCell.cellFormula = "SUM(${getColumnAlphabet(2)}${rowIndex+1}:${getColumnAlphabet(columnIndex - 2)}${rowIndex+1})" // should columnIndex - 2 + tempCell.cellFormula = + "SUM(${getColumnAlphabet(2)}${rowIndex + 1}:${getColumnAlphabet(columnIndex - 2)}${rowIndex + 1})" // should columnIndex - 2 rowIndex++ } // cal subtotal println(rowIndex) - for (i in 0 ..columnSize ) { // minus last col + for (i in 0..columnSize) { // minus last col println(sheet.getRow(rowIndex).getCell(2 + i)) tempCell = sheet.getRow(rowIndex).getCell(2 + i) ?: sheet.getRow(rowIndex).createCell(2 + i) tempCell.cellFormula = "SUM(${getColumnAlphabet(2 + i)}9:${getColumnAlphabet(2 + i)}${rowIndex})" @@ -1136,7 +1306,7 @@ open class ReportService( result: List>, lowerLimit: Double, templatePath: String - ):Workbook { + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -1169,10 +1339,11 @@ open class ReportService( tempCell.setCellValue((index + 1).toDouble()) val keys = obj.keys.toList() keys.forEachIndexed { keyIndex, key -> - tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex).createCell(columnIndex + keyIndex + 1) + tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex) + .createCell(columnIndex + keyIndex + 1) when (obj[key]) { is Double -> tempCell.setCellValue(obj[key] as Double) - else -> tempCell.setCellValue(obj[key] as String ) + else -> tempCell.setCellValue(obj[key] as String) } } rowIndex++ @@ -1190,20 +1361,20 @@ open class ReportService( fillRed.setFillPattern(PatternFormatting.SOLID_FOREGROUND) val cfRules = arrayOf(rule1, rule2) - val regions = arrayOf(CellRangeAddress.valueOf("\$J7:\$K${rowIndex+1}")) + val regions = arrayOf(CellRangeAddress.valueOf("\$J7:\$K${rowIndex + 1}")) sheetCF.addConditionalFormatting(regions, cfRules); return workbook } -//createLateStartReport + //createLateStartReport private fun createLateStartReport( team: Team, customer: Customer, project: List, templatePath: String - ):Workbook{ - + ): Workbook { + val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -1214,9 +1385,9 @@ open class ReportService( 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) + // Start populating project data starting at row 7 + val startRow = 6 // 0-based index for row 7 + val projectDataRow = sheet.createRow(startRow) // Styling for cell A1: Font size 16 and bold val headerFont = workbook.createFont().apply { @@ -1276,29 +1447,29 @@ open class ReportService( open fun getFinancialStatus(teamLeadId: Long?): List> { val sql = StringBuilder( " with cte_timesheet as (" - + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.hourlyRate" - + " from timesheet t" - + " left join project_task pt on pt.id = t.projectTaskId" - + " left join project p ON p.id = pt.project_id" - + " left join staff s on s.id = t.staffId" - + " left join salary s2 on s.salaryId = s2.salaryPoint" - + " left join team t2 on t2.id = s.teamId" - + " )," - + " cte_invoice as (" - + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" - + " from invoice i" - + " left join project p on p.code = i.projectCode" - + " group by p.code" - + " )" - + " select p.code, p.description, c.name as client, concat(t.code, \' - \', t.name) as teamLead, p.planStart , p.planEnd , p.expectedTotalFee," - + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed," - + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount" - + " from project p" - + " left join cte_timesheet cte_ts on p.code = cte_ts.code" - + " left join customer c on c.id = p.customerId" - + " left join tsmsdb.team t on t.teamLead = p.teamLead" - + " left join cte_invoice cte_i on cte_i.code = p.code" - + " where p.status = \'On-going\'" + + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.hourlyRate" + + " from timesheet t" + + " left join project_task pt on pt.id = t.projectTaskId" + + " left join project p ON p.id = pt.project_id" + + " left join staff s on s.id = t.staffId" + + " left join salary s2 on s.salaryId = s2.salaryPoint" + + " left join team t2 on t2.id = s.teamId" + + " )," + + " cte_invoice as (" + + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" + + " from invoice i" + + " left join project p on p.code = i.projectCode" + + " group by p.code" + + " )" + + " select p.code, p.description, c.name as client, concat(t.code, \' - \', t.name) as teamLead, p.planStart , p.planEnd , p.expectedTotalFee," + + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed," + + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount" + + " from project p" + + " left join cte_timesheet cte_ts on p.code = cte_ts.code" + + " left join customer c on c.id = p.customerId" + + " left join tsmsdb.team t on t.teamLead = p.teamLead" + + " left join cte_invoice cte_i on cte_i.code = p.code" + + " where p.status = \'On-going\'" ) if (teamLeadId!! > 0) { sql.append(" and p.teamLead = :teamLeadId ") @@ -1312,32 +1483,33 @@ open class ReportService( open fun getTimesheet(args: Map): List> { val sql = StringBuilder( "SELECT" - + " p.id," - + " p.name," - + " p.code," - + " CAST(DATE_FORMAT(t.recordDate, '%d') AS SIGNED) AS recordDate," - + " sum(t.normalConsumed) as normalConsumed," - + " IFNULL(sum(t.otConsumed), 0.0) as otConsumed" - + " from timesheet t" - + " left join project_task pt on t.projectTaskId = pt.id" - + " left join project p on p.id = pt.project_id" - + " where t.staffId = :staffId" - + " group by p.id, t.recordDate" - + " order by p.id, t.recordDate" - + " and t.recordDate BETWEEN :startDate and :endDate" + + " p.id," + + " p.name," + + " p.code," + + " CAST(DATE_FORMAT(t.recordDate, '%d') AS SIGNED) AS recordDate," + + " sum(t.normalConsumed) as normalConsumed," + + " IFNULL(sum(t.otConsumed), 0.0) as otConsumed" + + " from timesheet t" + + " left join project_task pt on t.projectTaskId = pt.id" + + " left join project p on p.id = pt.project_id" + + " where t.staffId = :staffId" + + " group by p.id, t.recordDate" + + " order by p.id, t.recordDate" + + " and t.recordDate BETWEEN :startDate and :endDate" ) return jdbcDao.queryForList(sql.toString(), args) } + open fun getLeaves(args: Map): List> { val sql = StringBuilder( " SELECT " - + " sum(leaveHours) as leaveHours, " - + " CAST(DATE_FORMAT(recordDate, '%d') AS SIGNED) AS recordDate " - + " from `leave` " - + " where staffId = :staffId " - + " and recordDate BETWEEN :startDate and :endDate " - + " group by recordDate " - + " order by recordDate " + + " sum(leaveHours) as leaveHours, " + + " CAST(DATE_FORMAT(recordDate, '%d') AS SIGNED) AS recordDate " + + " from `leave` " + + " where staffId = :staffId " + + " and recordDate BETWEEN :startDate and :endDate " + + " group by recordDate " + + " order by recordDate " ) return jdbcDao.queryForList(sql.toString(), args) } @@ -1449,60 +1621,64 @@ open class ReportService( if (args.containsKey("custId")) sql.append("and c.id = :custId") if (args.containsKey("status")) - statusFilter = when (args.get("status")) { - "Potential Overconsumption" -> "and " + - " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 0.9 " + - " and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + - " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 0.9 " + - " and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1)" - "Overconsumption" -> "and " + - " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + - " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1) " - "Within Budget" -> "and " + - " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) < 0.9 " + - " and " + - " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 0.9 " - else -> "" - } - sql.append(statusFilter) + statusFilter = when (args.get("status")) { + "Potential Overconsumption" -> "and " + + " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 0.9 " + + " and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 0.9 " + + " and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1)" + + "Overconsumption" -> "and " + + " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1) " + + "Within Budget" -> "and " + + " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) < 0.9 " + + " and " + + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 0.9 " + + else -> "" + } + sql.append(statusFilter) } return jdbcDao.queryForList(sql.toString(), args) } - open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List>{ + + open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List> { val sql = StringBuilder( - " with cte_timesheet as (" - + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId," - + " t.recordDate" - + " from timesheet t" - + " left join project_task pt on pt.id = t.projectTaskId" - + " left join project p ON p.id = pt.project_id" - + " left join staff s on s.id = t.staffId" - + " left join salary s2 on s.salaryId = s2.salaryPoint" - + " left join team t2 on t2.id = s.teamId" - + " )," - + " cte_invoice as (" - + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" - + " from invoice i" - + " left join project p on p.code = i.projectCode" - + " group by p.code" - + " )" - + " select p.code, p.description, c.name as client, concat(t.code, \" - \", t.name) as teamLead," - + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed, DATE_FORMAT(cte_ts.recordDate, '%Y-%m') as recordDate, " - + " IFNULL(cte_ts.salaryPoint, 0) as salaryPoint, " - + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount," - + " s.name, s.staffId, g.code as gradeCode, g.name as gradeName, t2.code as teamCode, t2.name as teamName" - + " from project p" - + " left join cte_timesheet cte_ts on p.code = cte_ts.code" - + " left join customer c on c.id = p.customerId" - + " left join tsmsdb.team t on t.teamLead = p.teamLead" - + " left join cte_invoice cte_i on cte_i.code = p.code" - + " left join staff s on s.id = cte_ts.staffId" - + " left join grade g on g.id = s.gradeId" - + " left join team t2 on t2.id = s.teamId" - + " where p.deleted = false" - + " and (DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') >= :startMonth and DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') <= :endMonth)" - + " and p.id = :projectId" - + " order by g.code, s.name, cte_ts.recordDate" + " with cte_timesheet as (" + + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId," + + " t.recordDate" + + " from timesheet t" + + " left join project_task pt on pt.id = t.projectTaskId" + + " left join project p ON p.id = pt.project_id" + + " left join staff s on s.id = t.staffId" + + " left join salary s2 on s.salaryId = s2.salaryPoint" + + " left join team t2 on t2.id = s.teamId" + + " )," + + " cte_invoice as (" + + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" + + " from invoice i" + + " left join project p on p.code = i.projectCode" + + " group by p.code" + + " )" + + " select p.code, p.description, c.name as client, concat(t.code, \" - \", t.name) as teamLead," + + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed, DATE_FORMAT(cte_ts.recordDate, '%Y-%m') as recordDate, " + + " IFNULL(cte_ts.salaryPoint, 0) as salaryPoint, " + + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount," + + " s.name, s.staffId, g.code as gradeCode, g.name as gradeName, t2.code as teamCode, t2.name as teamName" + + " from project p" + + " left join cte_timesheet cte_ts on p.code = cte_ts.code" + + " left join customer c on c.id = p.customerId" + + " left join tsmsdb.team t on t.teamLead = p.teamLead" + + " left join cte_invoice cte_i on cte_i.code = p.code" + + " left join staff s on s.id = cte_ts.staffId" + + " left join grade g on g.id = s.gradeId" + + " left join team t2 on t2.id = s.teamId" + + " where p.deleted = false" + + " and (DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') >= :startMonth and DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') <= :endMonth)" + + " and p.id = :projectId" + + " order by g.code, s.name, cte_ts.recordDate" ) val args = mapOf( @@ -1537,24 +1713,24 @@ open class ReportService( val staffInfoList = mutableListOf>() println("manHoursSpent------- ${manHoursSpent}") - for(item in manHoursSpent){ - if(info["teamLead"] != item.getValue("teamLead")){ + for (item in manHoursSpent) { + if (info["teamLead"] != item.getValue("teamLead")) { info["teamLead"] = item.getValue("teamLead") } - if(info["client"] != item.getValue("client")){ + if (info["client"] != item.getValue("client")) { info["client"] = item.getValue("client") } - if(info["code"] != item.getValue("code")){ + if (info["code"] != item.getValue("code")) { info["code"] = item.getValue("code") } - if(info["code"] == item["code"] && "paidAmount" !in info){ + if (info["code"] == item["code"] && "paidAmount" !in info) { info["paidAmount"] = item.getValue("sumPaidAmount") } - if(info["description"] != item.getValue("description")){ + if (info["description"] != item.getValue("description")) { info["description"] = item.getValue("description") } val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() - if(!staffInfoList.any { it["staffId"] == item["staffId"] && it["name"] == item["name"]}){ + if (!staffInfoList.any { it["staffId"] == item["staffId"] && it["name"] == item["name"] }) { staffInfoList.add( mapOf( "staffId" to item.getValue("staffId"), @@ -1569,13 +1745,13 @@ open class ReportService( "normalConsumed" to item.getValue("normalConsumed"), "otConsumed" to item.getValue("otConsumed"), "totalManHours" to item.getValue("normalConsumed") as Double + item.getValue("otConsumed") as Double, - "manhourExpenditure" to (hourlyRate * item.getValue("normalConsumed") as Double ) - + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) + "manhourExpenditure" to (hourlyRate * item.getValue("normalConsumed") as Double) + + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) ) ) ) ) - }else{ + } else { val existingMap = staffInfoList.find { it.containsValue(item.getValue("staffId")) }!! val updatedMap = existingMap.toMutableMap() val hourlySpentList = updatedMap["hourlySpent"] as MutableList> @@ -1583,10 +1759,18 @@ open class ReportService( if (existingRecord != null) { val existingIndex = hourlySpentList.indexOf(existingRecord) val updatedRecord = existingRecord.toMutableMap() - updatedRecord["normalConsumed"] = (updatedRecord["normalConsumed"] as Double) + item.getValue("normalConsumed") as Double - updatedRecord["otConsumed"] = (updatedRecord["otConsumed"] as Double) + item.getValue("otConsumed") as Double - updatedRecord["totalManHours"] = (updatedRecord["totalManHours"] as Double) + item.getValue("normalConsumed") as Double + item.getValue("otConsumed") as Double - updatedRecord["manhourExpenditure"] = (updatedRecord["manhourExpenditure"] as Double) + (hourlyRate * item.getValue("normalConsumed") as Double) + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) + updatedRecord["normalConsumed"] = + (updatedRecord["normalConsumed"] as Double) + item.getValue("normalConsumed") as Double + updatedRecord["otConsumed"] = + (updatedRecord["otConsumed"] as Double) + item.getValue("otConsumed") as Double + updatedRecord["totalManHours"] = + (updatedRecord["totalManHours"] as Double) + item.getValue("normalConsumed") as Double + item.getValue( + "otConsumed" + ) as Double + updatedRecord["manhourExpenditure"] = + (updatedRecord["manhourExpenditure"] as Double) + (hourlyRate * item.getValue("normalConsumed") as Double) + (hourlyRate * item.getValue( + "otConsumed" + ) as Double * otFactor) hourlySpentList[existingIndex] = updatedRecord } else { hourlySpentList.add( @@ -1596,7 +1780,7 @@ open class ReportService( "otConsumed" to item.getValue("otConsumed"), "totalManHours" to item.getValue("normalConsumed") as Double + item.getValue("otConsumed") as Double, "manhourExpenditure" to (hourlyRate * item.getValue("normalConsumed") as Double) - + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) + + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) ) ) } @@ -1611,7 +1795,7 @@ open class ReportService( tempList.add(mapOf("info" to info)) tempList.add(mapOf("staffInfoList" to staffInfoList)) - println("Only Staff Info List --------------------- ${ tempList.first() { it.containsKey("staffInfoList") }["staffInfoList"]}") + println("Only Staff Info List --------------------- ${tempList.first() { it.containsKey("staffInfoList") }["staffInfoList"]}") println("tempList----------------- $tempList") return tempList @@ -1634,7 +1818,7 @@ open class ReportService( manhoursSpent: List>, startMonth: String, endMonth: String - ): Workbook{ + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -1660,10 +1844,11 @@ open class ReportService( val monthFormat = DateTimeFormatter.ofPattern("MMM yyyy", Locale.ENGLISH) - val info:Map = manhoursSpent.first() { it.containsKey("info") }["info"] as Map - val staffInfoList: List> = manhoursSpent.first() { it.containsKey("staffInfoList") }["staffInfoList"] as List> + val info: Map = manhoursSpent.first() { it.containsKey("info") }["info"] as Map + val staffInfoList: List> = + manhoursSpent.first() { it.containsKey("staffInfoList") }["staffInfoList"] as List> - if (staffInfoList.isEmpty()){ + if (staffInfoList.isEmpty()) { val sheet = workbook.getSheetAt(0) var rowNum = 0 rowNum = 1 @@ -1737,7 +1922,7 @@ open class ReportService( row9Cell.setCellValue("${convertStartMonth.format(monthFormat)} - ${convertEndMonth.format(monthFormat)}") rowNum = 10 - for(staff in staffInfoList){ + for (staff in staffInfoList) { // val row: Row = sheet.getRow(rowNum++) ?: sheet.createRow(rowNum++) val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) val staffCell = row.getCell(0) ?: row.createCell(0) @@ -1769,7 +1954,7 @@ open class ReportService( } rowNum += 2 val titleRow = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) - sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, 0, monthRange.size+3)) + sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, 0, monthRange.size + 3)) val titleCell = titleRow.getCell(0) ?: titleRow.createCell(0) titleCell.apply { setCellValue("Manhours Spent per Month") @@ -1786,7 +1971,7 @@ open class ReportService( } CellUtil.setAlignment(staffCell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(staffCell, VerticalAlignment.CENTER); - CellUtil.setCellStyleProperty(staffCell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(staffCell, "borderBottom", BorderStyle.THIN) val teamCell = manhoursSpentRow.getCell(1) ?: manhoursSpentRow.createCell(1) teamCell.apply { @@ -1794,44 +1979,44 @@ open class ReportService( } CellUtil.setAlignment(teamCell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(teamCell, VerticalAlignment.CENTER); - CellUtil.setCellStyleProperty(teamCell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(teamCell, "borderBottom", BorderStyle.THIN) - for ((column, month) in monthRange.withIndex()){ + for ((column, month) in monthRange.withIndex()) { val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) - val monthCell = row.getCell(2+column) ?: row.createCell(2+column) + val monthCell = row.getCell(2 + column) ?: row.createCell(2 + column) monthCell.apply { setCellValue(month.getValue("display").toString()) } CellUtil.setAlignment(monthCell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(monthCell, VerticalAlignment.CENTER); - CellUtil.setCellStyleProperty(monthCell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(monthCell, "borderBottom", BorderStyle.THIN) } val monthColumnEnd = monthRange.size val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) - val manhourCell = row.getCell(2+monthColumnEnd) ?: row.createCell(2+monthColumnEnd) + val manhourCell = row.getCell(2 + monthColumnEnd) ?: row.createCell(2 + monthColumnEnd) manhourCell.apply { setCellValue("Manhour Sub-total") } CellUtil.setAlignment(manhourCell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(manhourCell, VerticalAlignment.CENTER); CellUtil.setCellStyleProperty(manhourCell, CellUtil.WRAP_TEXT, true) - CellUtil.setCellStyleProperty(manhourCell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(manhourCell, "borderBottom", BorderStyle.THIN) - val manhourECell = row.getCell(2+monthColumnEnd+1) ?: row.createCell(2+monthColumnEnd+1) + val manhourECell = row.getCell(2 + monthColumnEnd + 1) ?: row.createCell(2 + monthColumnEnd + 1) manhourECell.apply { setCellValue("Manhour Expenditure (HKD)") } CellUtil.setAlignment(manhourECell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(manhourECell, VerticalAlignment.CENTER); CellUtil.setCellStyleProperty(manhourECell, CellUtil.WRAP_TEXT, true) - CellUtil.setCellStyleProperty(manhourECell,"borderBottom", BorderStyle.THIN) - sheet.setColumnWidth(2+monthColumnEnd+1, 20*256) + CellUtil.setCellStyleProperty(manhourECell, "borderBottom", BorderStyle.THIN) + sheet.setColumnWidth(2 + monthColumnEnd + 1, 20 * 256) // Manhour Spent LOOP println("monthRange--------------- ${monthRange}\n") rowNum += 1 - for(staff in staffInfoList){ + for (staff in staffInfoList) { val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) val staffCell = row.getCell(0) ?: row.createCell(0) var manhourExpenditure = 0.0 @@ -1845,10 +2030,10 @@ open class ReportService( } CellUtil.setCellStyleProperty(teamCell, CellUtil.WRAP_TEXT, true) - for(i in 0 until monthRange.size){ - val cell = row.getCell(i+2) ?: row.createCell(i+2) + for (i in 0 until monthRange.size) { + val cell = row.getCell(i + 2) ?: row.createCell(i + 2) cell.setCellValue(0.0) - for(hourlySpent in staff.getValue("hourlySpent") as List>){ + for (hourlySpent in staff.getValue("hourlySpent") as List>) { cell.apply { if (hourlySpent.getValue("recordDate") == monthRange[i].getValue("date").toString()) { setCellValue(hourlySpent.getValue("totalManHours") as Double) @@ -1861,13 +2046,13 @@ open class ReportService( CellUtil.setVerticalAlignment(cell, VerticalAlignment.CENTER); } - val manhourCell = row.getCell(2+monthRange.size) ?: row.createCell(2+monthRange.size) + val manhourCell = row.getCell(2 + monthRange.size) ?: row.createCell(2 + monthRange.size) manhourCell.apply { - cellFormula="SUM(C${rowNum+1}:${getColumnAlphabet(1+monthRange.size)}${rowNum+1})" + cellFormula = "SUM(C${rowNum + 1}:${getColumnAlphabet(1 + monthRange.size)}${rowNum + 1})" cellStyle.dataFormat = accountingStyle } - val manhourECell = row.getCell(3+monthRange.size) ?: row.createCell(3+monthRange.size) + val manhourECell = row.getCell(3 + monthRange.size) ?: row.createCell(3 + monthRange.size) manhourECell.apply { setCellValue(manhourExpenditure) cellStyle.dataFormat = accountingStyle @@ -1889,19 +2074,19 @@ open class ReportService( rowNum += 1 val totalManhourERow: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) - val startRow = 15+staffInfoList.size - val lastColumnIndex = getColumnAlphabet(3+monthRange.size) + val startRow = 15 + staffInfoList.size + val lastColumnIndex = getColumnAlphabet(3 + monthRange.size) val totalManhourETitleCell = totalManhourERow.getCell(0) ?: totalManhourERow.createCell(0) totalManhourETitleCell.apply { setCellValue("Total Manhour Expenditure (HKD)") } val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1) totalManhourECell.apply { - cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow+staffInfoList.size})" + cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow + staffInfoList.size})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(totalManhourETitleCell,"borderBottom", BorderStyle.THIN) - CellUtil.setCellStyleProperty(totalManhourECell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(totalManhourETitleCell, "borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(totalManhourECell, "borderBottom", BorderStyle.THIN) rowNum += 1 val pandlRow: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) @@ -1911,11 +2096,11 @@ open class ReportService( } val panlCell = pandlRow.getCell(1) ?: pandlRow.createCell(1) panlCell.apply { - cellFormula = "B${rowNum-1}-B${rowNum}" + cellFormula = "B${rowNum - 1}-B${rowNum}" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(panlCellTitle,"borderBottom", BorderStyle.DOUBLE) - CellUtil.setCellStyleProperty(panlCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(panlCellTitle, "borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(panlCell, "borderBottom", BorderStyle.DOUBLE) return workbook } 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..8221e96 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 @@ -2,11 +2,9 @@ package com.ffii.tsms.modules.report.web import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.ffii.tsms.modules.data.entity.Customer -import com.ffii.tsms.modules.data.entity.StaffRepository +import com.ffii.tsms.modules.data.entity.* //import com.ffii.tsms.modules.data.entity.projections.FinancialStatusReportInfo import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo -import com.ffii.tsms.modules.data.entity.Team import com.ffii.tsms.modules.data.service.CustomerService import com.ffii.tsms.modules.data.service.TeamService import com.ffii.tsms.modules.project.entity.* @@ -35,11 +33,12 @@ import java.time.LocalDate import java.net.URLEncoder import java.time.format.DateTimeFormatter import org.springframework.stereotype.Controller -import com.ffii.tsms.modules.data.entity.TeamRepository -import com.ffii.tsms.modules.data.entity.CustomerRepository import org.springframework.data.jpa.repository.JpaRepository import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.report.web.model.* +import com.ffii.tsms.modules.timesheet.entity.Timesheet +import org.springframework.data.domain.Example +import org.springframework.data.domain.ExampleMatcher @RestController @RequestMapping("/reports") @@ -87,6 +86,34 @@ class ReportController( .body(ByteArrayResource(reportResult)) } + @PostMapping("/ProjectPotentialDelayReport") + @Throws(ServletRequestBindingException::class, IOException::class) + fun getProjectPotentialDelayReport(@RequestBody @Valid request: ProjectPotentialDelayReportRequest): ResponseEntity { + + val team = if (request.teamId.lowercase() == "all") null else teamRepository.findById(request.teamId.toLong()).orElse(null) + val searchedTeam = if (team == null) "All" else team.code + " - " +team.name + val client = if (request.clientId.lowercase() == "all") null else customerRepository.findById(request.clientId.toLong()).orElse(null) + val searchedClient = if (client == null) "All" else client.code + " - " +client.name + + val matcher = ExampleMatcher.matching().withIgnoreNullValues() + val exampleQuery = Example.of(Project().apply { + // org.springframework.dao.InvalidDataAccessApiUsageException: Path 'teamLead.team.staff' from root Project must not span a cyclic property reference + // [{ com.ffii.tsms.modules.project.entity.Project@6847e037 }] -teamLead-> [{ com.ffii.tsms.modules.data.entity.Staff@2a4c488b }] -team-> [{ com.ffii.tsms.modules.data.entity.Team@a09acb5 }] -staff-> [{ com.ffii.tsms.modules.data.entity.Staff@2a4c488b }] + // teamLead = team?.staff + customer = client + status = "On-going" + }, matcher) + + val projects = if (team == null) projectRepository.findAll(exampleQuery) else projectRepository.findAll(exampleQuery).filter { it.teamLead == team.staff } + + val projectTasks = projectTaskRepository.findAllByProjectIn(projects) + val timesheets = timesheetRepository.findAllByProjectTaskIn(projectTasks) + val reportResult: ByteArray = excelReportService.generateProjectPotentialDelayReport(searchedTeam, searchedClient, projects, timesheets) + return ResponseEntity.ok() + .header("filename", "Project Potential Delay Report - " + LocalDate.now() + ".xlsx") + .body(ByteArrayResource(reportResult)) + } + @PostMapping("/StaffMonthlyWorkHourAnalysisReport") @Throws(ServletRequestBindingException::class, IOException::class) fun StaffMonthlyWorkHourAnalysisReport(@RequestBody @Valid request: StaffMonthlyWorkHourAnalysisReportRequest): ResponseEntity { 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..f908bf3 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 @@ -18,6 +18,11 @@ data class ProjectCashFlowReportRequest ( val dateType: String, // "Date", "Month" ) +data class ProjectPotentialDelayReportRequest ( + val teamId: String, + val clientId: String, +) + data class StaffMonthlyWorkHourAnalysisReportRequest ( val id: Long, val yearMonth: YearMonth diff --git a/src/main/resources/templates/report/AR02_Delay Report v02.xlsx b/src/main/resources/templates/report/AR02_Delay Report v02.xlsx index fe0d49339c350e5bcd8b6d0daa9d46d9b5e7b3f8..1d844349d56aafd889458b6a688a8caec9490764 100644 GIT binary patch delta 5173 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

Z7c4IVO z^(KvZ(p+E~)(O1lTq0iDMNeF_H_Kz=j-;dADSHn_B3yI24=_ zdunHD@fcIj6`sZu&*)OcM^A%XHoFyyx9QH{D%hB@Yj@zac=gTUL)noGOTryxhr^P4 z{QlF8yqLByHjIq&jbIew>M(E+B}(K(k~HsMSAP&3UseaJ4=ec-=G)yz+w0kt zCi5l9j=@ik>DeMLS6AuQ{38D)=C`SvI0t3urfm4?&>Hdb8vI0>(>bVzyhVRxx5?}! zTIm_wIj+PxU5R*qnrPjCHGl7Vu1YH^B^dMslzt?M^o!1}B4x{xv5q!V9a!zn!oT~E zj{*tIby+doTUdP{T9*}u3Qd#O8HAH6U#P8E+s)S~#*kK~J?7%y*P~pGF#0Gf#uB@Dg?0R*4Up?GWxy*&nnk9K> zn5VltzT1(&ag4Lr!Q+*|Rk2FR%}R?}|F3tGTo5xO^>xX|Wp!jrC{b@R<&+sNZ6F9k zhW{ffJo%?l+>)}IZQZA=Pt`t<_>DZnnEEZzOq7bJ2ZWz0k=-rtH<))(kI;3vWT27J zfKC@_mjuaTwu*ZXZuXha5jK)|&9Az)3a?NVc$38{G%_EX)5}uyh_2i(c&K7uB83Xx z*<_oPB&mS%e?B%1!%R{Vgv=-1?kcuYQ(U#qy^+@vHH{T~`M<6+oD^~g8%8PohM2cj-2OF ze5#}eNLWy)RE&L+;NGSX;ipB2oCgWdMk?>!oO5hvpDg#G--=Ol_T%N3@@-2hs(uq4 zUOZuaJfYH46xvp&J)CQqjXj@;dWZcvI+eY)PYBgdH%JekWhJvnjZ9Gs6SgNIrBfuN z3F{T@HJ^K zR?uPCZ}t+MDh<(n8vq~$mqz8_uE>`>F=R0e8 zw7;WvxouGcn_4y5;X+Ew^sIBha?(~v;FXg4TCVAcq}mdm<;iyOXK)u`#@R@kEM1R9 z*alz8d4_+#;#kP&jY`-JQvkP;niI1~!cqGcv5h&=OKt3$Nuo4qZa=je6_-!?(Ws6V zyq5llt)KO)6TX=C5%wGDSq%Fy6+Bp=zx8yYoalSSND{_o#qMeAJMUC#R>?SDnyQ$z zzc&_+wh_=M#zY0_P=y$(33+a>IpNcSkj?%yrGfOLQXdCr{FlqC?c(+z;Mk#t--@ng zPtVO9sl7DTcI}!~2VicGMCJt?v{2uT)O6di`uK2Z$~TNByHA&&A;+iXhRdE3WXY+u zDLpjyO-0R19v5ez{d*8|KNlmpms9`51R4jyLi>*`1OQ-08sx6uR_Nol!01gXn80ldK*DT`?t&i0EqvUNlJ|-lVGI#htL4` zm;UcCr$cK=$N{VA(a93Jz*+|Mx`YJp-_3mgTC)Gn27KiN0PH+%^}Id3d<1Phz3tH( i2nxEt|LgtOiS%Fjt~}^^Nf-nS00TJgK@tAH(EkBp`Cyp< delta 7918 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 From c0cc24d010b173b73e4b15a28881ef3d4c06570f Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Fri, 24 May 2024 12:39:55 +0800 Subject: [PATCH 02/21] cost and expense report --- .../modules/report/service/ReportService.kt | 84 ++++++++++++++++++ .../modules/report/web/ReportController.kt | 11 +++ .../modules/report/web/model/ReportRequest.kt | 6 ++ .../AR04_Cost and Expense Report v02.xlsx | Bin 14374 -> 12774 bytes 4 files changed, 101 insertions(+) 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 42c9754..1c23451 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 @@ -44,6 +44,7 @@ open class ReportService( private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd") private val FORMATTED_TODAY = LocalDate.now().format(DATE_FORMATTER) + private val COSTANDEXPENSE_REPORT = "templates/report/AR04_Cost and Expense Report v02.xlsx" private val PandL_REPORT = "templates/report/AR07_Project P&L Report v02.xlsx" private val FINANCIAL_STATUS_REPORT = "templates/report/EX01_Financial Status Report.xlsx" private val PROJECT_CASH_FLOW_REPORT = "templates/report/EX02_Project Cash Flow Report.xlsx" @@ -1986,4 +1987,87 @@ open class ReportService( return costAndExpenseList } + + fun createCostAndExpenseWorkbook( + templatePath: String, + costAndExpenseList: List>, + ): Workbook{ + val resource = ClassPathResource(templatePath) + val templateInputStream = resource.inputStream + val workbook: Workbook = XSSFWorkbook(templateInputStream) + + val sheet = workbook.getSheetAt(0) + var rowNum = 0 + rowNum = 1 + val row1: Row = sheet.getRow(rowNum) + val row1Cell = row1.getCell(1) + row1Cell.setCellValue(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")).toString()) + + rowNum = 6 + for(item in costAndExpenseList){ + val index = costAndExpenseList.indexOf(item) + val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) + val cell = row.getCell(0) ?: row.createCell(0) + cell.apply { + setCellValue(index.toDouble()) + } + + val cell1 = row.getCell(1) ?: row.createCell(1) + cell1.apply { + setCellValue(item["code"].toString()) + } + + val cell2 = row.getCell(2) ?: row.createCell(2) + cell2.apply { + setCellValue(item["description"].toString()) + } + + val cell3 = row.getCell(3) ?: row.createCell(3) + cell3.apply { + setCellValue(item["teamLead"].toString()) + } + + val cell4 = row.getCell(4) ?: row.createCell(4) + cell4.apply { + setCellValue(item["client"].toString()) + } + + val cell5 = row.getCell(5) ?: row.createCell(5) + val budget = item["budget"] as Double * 0.8 + cell5.apply { + setCellValue(budget) + } + + val cell6 = row.getCell(6) ?: row.createCell(6) + val manHoutsSpentCost = item["manhourExpenditure"] as Double + cell6.apply { + setCellValue(manHoutsSpentCost) + } + + val cell7 = row.getCell(7) ?: row.createCell(7) + cell7.apply { + cellFormula = "F${rowNum+1}-G${rowNum+1}" + } + + val cell8 = row.getCell(8) ?: row.createCell(8) + cell8.apply { + cellFormula = "H${rowNum+1}/G${rowNum+1}" + } + sheet.setRowBreak(rowNum++); + } + + return workbook + } + + fun genCostAndExpenseReport(): ByteArray{ + val costAndExpenseList = getCostAndExpense("All") + + val workbook: Workbook = createCostAndExpenseWorkbook(COSTANDEXPENSE_REPORT, costAndExpenseList) + + val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() + workbook.write(outputStream) + workbook.close() + + return outputStream.toByteArray() + } } 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..a23e649 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 @@ -254,4 +254,15 @@ fun downloadLateStartReport(@RequestBody @Valid request: LateStartReportRequest) return excelReportService.getCostAndExpense("All") } + @PostMapping("/costandexpenseReport") + @Throws(ServletRequestBindingException::class, IOException::class) + fun getCostAndExpenseReport(@RequestBody @Valid request: costAndExpenseRequest): ResponseEntity { + println(request) + val reportResult: ByteArray = excelReportService.genCostAndExpenseReport() + + return ResponseEntity.ok() + .header("filename", "Cost and Expense Report - " + LocalDate.now() + ".xlsx") + .body(ByteArrayResource(reportResult)) + } + } \ No newline at end of file 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..287e301 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 @@ -13,6 +13,12 @@ data class PandLReportRequest ( val endMonth: String, ) +data class costAndExpenseRequest ( + val teamId: Long, + val clientId: String, + val budgetPercentage: String, +) + data class ProjectCashFlowReportRequest ( val projectId: Long, val dateType: String, // "Date", "Month" diff --git a/src/main/resources/templates/report/AR04_Cost and Expense Report v02.xlsx b/src/main/resources/templates/report/AR04_Cost and Expense Report v02.xlsx index cb9ba8ec1707870e0fbb5d4551b152ff5c5a836e..889948be48004e40b1f58aaa48f03fb2695595d4 100644 GIT binary patch delta 5126 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 R#(HksEuZPo3f{ji{SWX%T0Q^( From e34cc36087a7047c1c15d0d48dba099b85694e7b Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Fri, 24 May 2024 15:39:42 +0800 Subject: [PATCH 03/21] update --- .../ffii/tsms/modules/report/service/ReportService.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 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 169519b..744b5a1 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 @@ -1521,7 +1521,7 @@ open class ReportService( + " result.teamCode, " + " result.custCode, " ) if (args.get("outstanding") as Boolean) { - sql.append(" result.totalBudget - COALESCE(i.issueAmount , 0) + COALESCE(i.issueAmount, 0) - COALESCE(i.paidAmount, 0) as `Receivable Remained`, ") + sql.append(" result.projectFee - (result.totalBudget - COALESCE(i.issueAmount , 0) + COALESCE(i.issueAmount, 0) - COALESCE(i.paidAmount, 0)) as `Receivable Remained`, ") } sql.append( " DATE_FORMAT(result.actualEnd, '%d/%m/%Y') as actualEnd " @@ -1533,6 +1533,7 @@ open class ReportService( + " min(t.code) as teamCode, " + " min(c.code) as custCode, " + " min(p.actualEnd) as actualEnd, " + + " min(p.expectedTotalFee) as projectFee, " + " sum(COALESCE(tns.totalConsumed*sal.hourlyRate, 0)) as totalBudget " + " FROM ( " + " SELECT " @@ -1545,11 +1546,11 @@ open class ReportService( + " GROUP BY t.staffId, t.projectTaskId " + " order by t.staffId " + " ) AS tns " - + " inner join project_task pt ON tns.taskId = pt.id " - + " left JOIN staff s ON tns.staffId = s.id " + + " right join project_task pt ON tns.taskId = pt.id " + + " left join project p on p.id = pt.project_id " + + " left JOIN staff s ON p.teamLead = s.id " + " left join salary sal on s.salaryId = sal.salaryPoint " + " left JOIN team t ON s.teamId = t.id " - + " left join project p on p.id = pt.project_id " + " left join customer c on c.id = p.customerId " + " where p.deleted = false " + " and p.status = 'Completed' " @@ -1591,7 +1592,7 @@ open class ReportService( + " p.name, " + " t.code as team, " + " c.code as client, " - + " p.expectedTotalFee as plannedBudget, " + + " p.expectedTotalFee * 0.8 as plannedBudget, " + " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, " + " COALESCE(p.totalManhour, 0) as plannedManhour, " + " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, " From b5e9d286db14653d8b7c2cf9ebfb6a38ec17fdb8 Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Fri, 24 May 2024 16:26:50 +0800 Subject: [PATCH 04/21] With, client and team fiter --- .../modules/report/service/ReportService.kt | 59 +++++++++++++++---- .../modules/report/web/ReportController.kt | 6 +- .../modules/report/web/model/ReportRequest.kt | 4 +- 3 files changed, 54 insertions(+), 15 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 169519b..ff8b12c 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 @@ -8,6 +8,7 @@ import com.ffii.tsms.modules.data.entity.Team import com.ffii.tsms.modules.project.entity.Invoice import com.ffii.tsms.modules.project.entity.Milestone import com.ffii.tsms.modules.project.entity.Project +import com.ffii.tsms.modules.report.web.model.costAndExpenseRequest import com.ffii.tsms.modules.timesheet.entity.Leave import com.ffii.tsms.modules.timesheet.entity.Timesheet import com.ffii.tsms.modules.timesheet.entity.projections.MonthlyLeave @@ -32,6 +33,7 @@ import java.time.LocalDate import java.time.YearMonth import java.time.format.DateTimeFormatter import java.util.* +import kotlin.jvm.optionals.getOrElse data class DayInfo(val date: String?, val weekday: String?) @@ -1933,7 +1935,7 @@ open class ReportService( val gradeCell = row.getCell(1) ?: row.createCell(1) gradeCell.apply { - setCellValue("G${staff.getValue("grade")}") + setCellValue("${staff.getValue("grade")}") } CellUtil.setAlignment(gradeCell, HorizontalAlignment.CENTER); @@ -2106,7 +2108,7 @@ open class ReportService( return workbook } - fun getCostAndExpense(status: String): List>{ + fun getCostAndExpense(clientId: Long?, teamId: Long?): List>{ val sql = StringBuilder( " with cte_timesheet as ( " + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId," @@ -2131,17 +2133,25 @@ open class ReportService( + " left join team t2 on t2.id = s.teamId" + " where ISNULL(p.code) = False" ) - if(status != "All"){ + if(clientId != null){ sql.append( - " and p.status = :status" + " and c.id = :clientId " ) } + + if(teamId != null){ + sql.append( + " and p.teamLead = :teamId " + ) + } + sql.append( " group by p.code, p.description , c.name, teamLead, p.expectedTotalFee , hourlyRate" + " order by p.code" ) val args = mapOf( - "status" to status + "clientId" to clientId, + "teamId" to teamId ) val otFactor = BigDecimal(1).toDouble() @@ -2179,6 +2189,8 @@ open class ReportService( fun createCostAndExpenseWorkbook( templatePath: String, costAndExpenseList: List>, + teamId: Long?, + clientId: Long? ): Workbook{ val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream @@ -2188,9 +2200,35 @@ open class ReportService( var rowNum = 0 rowNum = 1 val row1: Row = sheet.getRow(rowNum) - val row1Cell = row1.getCell(1) + val row1Cell = row1.getCell(2) row1Cell.setCellValue(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")).toString()) + rowNum = 2 + val row2: Row = sheet.getRow(rowNum) + val row2Cell = row2.getCell(2) + if(teamId == null){ + row2Cell.setCellValue("All") + }else{ + val sql = StringBuilder( + " select t.id, t.code, t.name, concat(t.code, \" - \" ,t.name) as teamLead from team t where t.id = :teamId " + ) + val team = jdbcDao.queryForMap(sql.toString(), mapOf("teamId" to teamId)).get() + row2Cell.setCellValue(team["teamLead"] as String) + } + + rowNum = 3 + val row3: Row = sheet.getRow(rowNum) + val row3Cell = row3.getCell(2) + if(clientId == null){ + row3Cell.setCellValue("All") + }else{ + val sql = StringBuilder( + " select c.id, c.name from customer c where c.id = :clientId " + ) + val client = jdbcDao.queryForMap(sql.toString(), mapOf("clientId" to clientId)).get() + row3Cell.setCellValue(client["name"] as String) + } + rowNum = 6 for(item in costAndExpenseList){ val index = costAndExpenseList.indexOf(item) @@ -2239,7 +2277,7 @@ open class ReportService( val cell8 = row.getCell(8) ?: row.createCell(8) cell8.apply { - cellFormula = "H${rowNum+1}/G${rowNum+1}" + cellFormula = "H${rowNum+1}/F${rowNum+1}" } sheet.setRowBreak(rowNum++); } @@ -2247,10 +2285,11 @@ open class ReportService( return workbook } - fun genCostAndExpenseReport(): ByteArray{ - val costAndExpenseList = getCostAndExpense("All") + fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray{ + + val costAndExpenseList = getCostAndExpense(request.clientId, request.teamId) - val workbook: Workbook = createCostAndExpenseWorkbook(COSTANDEXPENSE_REPORT, costAndExpenseList) + val workbook: Workbook = createCostAndExpenseWorkbook(COSTANDEXPENSE_REPORT, costAndExpenseList, request.clientId, request.teamId) val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) 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 70db023..9dc9282 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 @@ -277,15 +277,15 @@ fun downloadLateStartReport(@RequestBody @Valid request: LateStartReportRequest) } @GetMapping("/costNExpenses/{status}") - fun getManhoursSpent(@PathVariable status: String): List> { - return excelReportService.getCostAndExpense("All") + fun getManhoursSpent(@RequestBody @Valid request: costAndExpenseRequest): List> { + return excelReportService.getCostAndExpense(request.clientId, request.teamId) } @PostMapping("/costandexpenseReport") @Throws(ServletRequestBindingException::class, IOException::class) fun getCostAndExpenseReport(@RequestBody @Valid request: costAndExpenseRequest): ResponseEntity { println(request) - val reportResult: ByteArray = excelReportService.genCostAndExpenseReport() + val reportResult: ByteArray = excelReportService.genCostAndExpenseReport(request) return ResponseEntity.ok() .header("filename", "Cost and Expense Report - " + LocalDate.now() + ".xlsx") 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 6747af7..4dbc566 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 @@ -14,8 +14,8 @@ data class PandLReportRequest ( ) data class costAndExpenseRequest ( - val teamId: Long, - val clientId: String, + val teamId: Long?, + val clientId: Long?, val budgetPercentage: String, ) From 10fbc901b6494b4c879ead317d6724f03b67f1af Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Fri, 24 May 2024 17:12:01 +0800 Subject: [PATCH 05/21] Any remaining bugetPercentage filter --- .../modules/report/service/ReportService.kt | 29 +++++++++++++++---- .../modules/report/web/model/ReportRequest.kt | 2 +- 2 files changed, 25 insertions(+), 6 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 b7954ba..81fc06a 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 @@ -2183,15 +2183,25 @@ open class ReportService( } } } + val result = costAndExpenseList.map { + item -> + val budget = (item["budget"] as? Double)?.times(0.8) ?: 0.0 + val budgetRemain = budget - (item["manhourExpenditure"] as? Double ?: 0.0) + val remainingPercent = (budgetRemain / budget) + item.toMutableMap().apply { + put("budgetPercentage", remainingPercent) + } + } - return costAndExpenseList + return result } fun createCostAndExpenseWorkbook( templatePath: String, costAndExpenseList: List>, teamId: Long?, - clientId: Long? + clientId: Long?, + budgetPercentage: Double? ): Workbook{ val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream @@ -2230,9 +2240,18 @@ open class ReportService( row3Cell.setCellValue(client["name"] as String) } + + val filterList: List> + if(budgetPercentage != null){ + filterList = costAndExpenseList.filter { ((it["budgetPercentage"] as? Double) ?: 0.0) > budgetPercentage } + }else{ + filterList = costAndExpenseList + } + + rowNum = 6 - for(item in costAndExpenseList){ - val index = costAndExpenseList.indexOf(item) + for(item in filterList){ + val index = filterList.indexOf(item) val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) val cell = row.getCell(0) ?: row.createCell(0) cell.apply { @@ -2290,7 +2309,7 @@ open class ReportService( val costAndExpenseList = getCostAndExpense(request.clientId, request.teamId) - val workbook: Workbook = createCostAndExpenseWorkbook(COSTANDEXPENSE_REPORT, costAndExpenseList, request.clientId, request.teamId) + val workbook: Workbook = createCostAndExpenseWorkbook(COSTANDEXPENSE_REPORT, costAndExpenseList, request.teamId, request.clientId, request.budgetPercentage) val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) 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 4dbc566..c36ca6a 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 @@ -16,7 +16,7 @@ data class PandLReportRequest ( data class costAndExpenseRequest ( val teamId: Long?, val clientId: Long?, - val budgetPercentage: String, + val budgetPercentage: Double?, ) data class ProjectCashFlowReportRequest ( From 4f8b669cb6d506670cfb714576fb415d16fd7de1 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Fri, 24 May 2024 17:30:51 +0800 Subject: [PATCH 06/21] update cost and expense report dataformat --- .../ffii/tsms/modules/report/service/ReportService.kt | 9 +++++++++ 1 file changed, 9 insertions(+) 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 81fc06a..126f1fe 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 @@ -2207,6 +2207,9 @@ open class ReportService( val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) + val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") + val percentStyle = workbook.createDataFormat().getFormat("0.00%") + val sheet = workbook.getSheetAt(0) var rowNum = 0 rowNum = 1 @@ -2282,23 +2285,29 @@ open class ReportService( val budget = item["budget"] as Double * 0.8 cell5.apply { setCellValue(budget) +// cellStyle.dataFormat = accountingStyle } + CellUtil.setCellStyleProperty(cell5, "dataFormat", accountingStyle) val cell6 = row.getCell(6) ?: row.createCell(6) val manHoutsSpentCost = item["manhourExpenditure"] as Double cell6.apply { setCellValue(manHoutsSpentCost) } + CellUtil.setCellStyleProperty(cell6, "dataFormat", accountingStyle) val cell7 = row.getCell(7) ?: row.createCell(7) cell7.apply { cellFormula = "F${rowNum+1}-G${rowNum+1}" } + CellUtil.setCellStyleProperty(cell7, "dataFormat", accountingStyle) val cell8 = row.getCell(8) ?: row.createCell(8) cell8.apply { cellFormula = "H${rowNum+1}/F${rowNum+1}" } + CellUtil.setCellStyleProperty(cell8, "dataFormat", percentStyle) + sheet.setRowBreak(rowNum++); } From 05e5e5e95a601896b74793ccd63d7ee4e983f65c Mon Sep 17 00:00:00 2001 From: Wayne Date: Sun, 26 May 2024 00:16:25 +0900 Subject: [PATCH 07/21] Add project field to timesheet --- .../com/ffii/tsms/modules/timesheet/entity/Timesheet.kt | 5 +++++ .../tsms/modules/timesheet/service/TimesheetsService.kt | 4 +++- .../changes/20240525_01_wayne/01_timesheet_project.sql | 6 ++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/changelog/changes/20240525_01_wayne/01_timesheet_project.sql diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/entity/Timesheet.kt b/src/main/java/com/ffii/tsms/modules/timesheet/entity/Timesheet.kt index 9128493..1566126 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/entity/Timesheet.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/entity/Timesheet.kt @@ -2,6 +2,7 @@ package com.ffii.tsms.modules.timesheet.entity import com.ffii.core.entity.BaseEntity import com.ffii.tsms.modules.data.entity.Staff +import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.project.entity.ProjectTask import jakarta.persistence.* import jakarta.validation.constraints.NotNull @@ -29,6 +30,10 @@ open class Timesheet : BaseEntity() { @JoinColumn(name = "projectTaskId") open var projectTask: ProjectTask? = null + @ManyToOne + @JoinColumn(name = "projectId") + open var project: Project? = null + @Column(name = "remark") open var remark: String? = null } \ 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 1324c4b..31350b1 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 @@ -46,6 +46,7 @@ open class TimesheetsService( this.normalConsumed = timeEntry.inputHours this.otConsumed = timeEntry.otHours this.projectTask = projectTask + this.project = project this.remark = timeEntry.remark } } @@ -72,6 +73,7 @@ open class TimesheetsService( this.normalConsumed = entry.inputHours this.otConsumed = entry.otHours this.projectTask = projectTask + this.project = project this.remark = entry.remark this.recordDate = this.recordDate ?: recordDate this.staff = this.staff ?: memberStaff @@ -112,7 +114,7 @@ open class TimesheetsService( .mapValues { (_, timesheets) -> timesheets.map { timesheet -> TimeEntry( id = timesheet.id!!, - projectId = timesheet.projectTask?.project?.id, + projectId = timesheet.projectTask?.project?.id ?: timesheet.project?.id, taskId = timesheet.projectTask?.task?.id, taskGroupId = timesheet.projectTask?.task?.taskGroup?.id, inputHours = timesheet.normalConsumed ?: 0.0, diff --git a/src/main/resources/db/changelog/changes/20240525_01_wayne/01_timesheet_project.sql b/src/main/resources/db/changelog/changes/20240525_01_wayne/01_timesheet_project.sql new file mode 100644 index 0000000..d4a42a5 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240525_01_wayne/01_timesheet_project.sql @@ -0,0 +1,6 @@ +-- liquibase formatted sql +-- changeset wayne:timesheet_project + +ALTER TABLE timesheet ADD projectId INT NULL; + +ALTER TABLE timesheet ADD CONSTRAINT FK_TIMESHEET_ON_PROJECTID FOREIGN KEY (projectId) REFERENCES project (id); From 953adbaaf0c6a511a591ca08d26464b50e92b5e1 Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Sun, 26 May 2024 19:03:07 +0800 Subject: [PATCH 08/21] add dashboard api --- .../modules/data/service/DashboardService.kt | 365 +++++++++++++++++- .../modules/data/web/DashboardController.kt | 20 + 2 files changed, 383 insertions(+), 2 deletions(-) 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 d3b2c09..279a397 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 @@ -509,8 +509,8 @@ open class DashboardService( + " p.name as projectName," + " t.code as team," + " s.name as teamLead," - + " p.actualStart as startDate," - + " p.planEnd as targetEndDate," + + " date_format(p.actualStart, '%Y-%m-%d') as startDate," + + " date_format(p.planEnd, '%Y-%m-%d') as targetEndDate," + " c.name as client," + " coalesce(s2.name,'N/A') as subsidiary" + " from project p" @@ -524,6 +524,367 @@ open class DashboardService( return jdbcDao.queryForList(sql.toString(), args) } + fun CashFlowMonthlyIncomeByMonth(args: Map): List> { + val sql = StringBuilder("select" + + " months.month as monthInvoice," + + " coalesce (invoice.invoiceMonth,'-') as invoiceMonth," + + " coalesce (invoice.income,0) as income," + + " SUM(COALESCE(invoice.income, 0)) OVER (ORDER BY months.month) AS cumulativeIncome" + + " FROM (" + + " SELECT 01 AS month" + + " UNION" + + " SELECT 02" + + " UNION" + + " SELECT 03" + + " UNION" + + " SELECT 04" + + " UNION" + + " SELECT 05" + + " UNION" + + " SELECT 06" + + " UNION" + + " SELECT 07" + + " UNION" + + " SELECT 08" + + " UNION" + + " SELECT 09" + + " UNION" + + " SELECT 10" + + " UNION" + + " SELECT 11" + + " union" + + " select 12" + + " ) AS months" + + " left join(" + + " select" + + " month(i.receiptDate) as invoiceMonth," + + " coalesce(sum(i.paidAmount),0) as income" + + " from project p" + + " left join invoice i on p.code = i.projectCode" + + " where p.status = 'On-going'" + + " and p.id in (:projectIds)" + + " and year(i.receiptDate) = :year" + + " and i.id is not null" + + " group by month(i.receiptDate)" + + " ) as invoice on months.month = invoice.invoiceMonth" + ) + + return jdbcDao.queryForList(sql.toString(), args) + } + + fun CashFlowMonthlyExpenditureByMonth(args: Map): List> { + val sql = StringBuilder("select" + + " months.month as monthExpenditure," + + " coalesce (expenditure.recordMonth,'-') as recordMonth," + + " coalesce (expenditure.expenditure,0) as expenditure," + + " SUM(COALESCE(expenditure.expenditure, 0)) OVER (ORDER BY months.month) AS cumulativeExpenditure" + + " FROM (" + + " SELECT 01 AS month" + + " UNION" + + " SELECT 02" + + " UNION" + + " SELECT 03" + + " UNION" + + " SELECT 04" + + " UNION" + + " SELECT 05" + + " UNION" + + " SELECT 06" + + " UNION" + + " SELECT 07" + + " UNION" + + " SELECT 08" + + " UNION" + + " SELECT 09" + + " UNION" + + " SELECT 10" + + " UNION" + + " SELECT 11" + + " union" + + " select 12" + + " ) AS months" + + " left join(" + + " SELECT" + + " r.recordMonth as recordMonth," + + " sum(r.cumulativeExpenditure) as expenditure" + + " from(" + + " select" + + " month(t.recordDate) as recordMonth," + + " (coalesce(sum(t.normalConsumed),0) * s2.hourlyRate) + (coalesce(sum(t.otConsumed),0) * s2.hourlyRate * 1.0) as cumulativeExpenditure" + + " from project p" + + " left join project_task pt on p.id = pt.project_id" + + " left join timesheet t on pt.id = t.projectTaskId" + + " left join staff s on t.staffId = s.id" + + " left join salary s2 on s.salaryId = s2.salaryPoint" + + " where t.id is not null" + + " and p.id in (:projectIds)" + + " and year(t.recordDate) = :year" + + " group by month(t.recordDate),s2.hourlyRate" + + " ) as r" + + " group by r.recordMonth" + + " ) as expenditure on months.month = expenditure.recordMonth" + ) + + return jdbcDao.queryForList(sql.toString(), args) + } + fun CashFlowReceivableAndExpenditure(args: Map): List> { + val sql = StringBuilder("select" + + "coalesce (round(sum(i.paidAmount)/sum(i.issueAmount)*100,0)) as receivedPercentage," + + "coalesce (round(expenditure.expenditure/(sum(p.expectedTotalFee)*0.8)*100,0)) as expenditurePercentage," + + " coalesce (sum(i.issueAmount),0) as totalInvoiced," + + " coalesce (sum(i.paidAmount),0) as totalReceived," + + " coalesce (sum(i.issueAmount) - sum(i.paidAmount),0) as receivable," + + " coalesce (round(sum(p.expectedTotalFee)*0.8,2),0) as totalBudget," + + " coalesce (expenditure.expenditure) as totalExpenditure" + + " from project p" + + " left join invoice i on p.code = i.projectCode" + + " left join(" + + " select" + + " sum(r.expenditure) as expenditure" + + " from(" + + " select" + + " (coalesce(sum(t.normalConsumed),0) * s2.hourlyRate) + (coalesce(sum(t.otConsumed),0) * s2.hourlyRate * 1.0) as expenditure" + + " from project p" + + " left join project_task pt on p.id = pt.project_id" + + " left join timesheet t on pt.id = t.projectTaskId" + + " left join staff s on t.staffId = s.id" + + " left join salary s2 on s.salaryId = s2.salaryPoint" + + " where t.id is not null" + + " and p.id in (:projectIds)" + + " group by s2.hourlyRate" + + " ) as r" + + " ) as expenditure on 1=1" + + " where p.id in (:projectIds)" + + " group by expenditure.expenditure" + ) + + return jdbcDao.queryForList(sql.toString(), args) + } + fun CashFlowAnticipateIncome(args: Map): List> { + val sql = StringBuilder("select" + + " months.month as monthanticipateIncome," + + " coalesce (anticipateIncome.anticipateIncomeDate,'-') as anticipateIncomeDate," + + " coalesce (anticipateIncome.anticipateIncome,0) as anticipateIncome" + + " FROM (" + + " SELECT 01 AS month" + + " UNION" + + " SELECT 02" + + " UNION" + + " SELECT 03" + + " UNION" + + " SELECT 04" + + " UNION" + + " SELECT 05" + + " UNION" + + " SELECT 06" + + " UNION" + + " SELECT 07" + + " UNION" + + " SELECT 08" + + " UNION" + + " SELECT 09" + + " UNION" + + " SELECT 10" + + " UNION" + + " SELECT 11" + + " union" + + " select 12" + + " ) AS months" + + " left join(" + + " select" + + " month(mp.date) as anticipateIncomeDate," + + " sum(mp.amount) as anticipateIncome" + + " from project p" + + " left join milestone m on p.id = m.projectId" + + " left join milestone_payment mp on m.id = mp.milestoneId" + + " where p.id in (:projecIds)" + + " group by month(mp.date)" + + " ) as anticipateIncome on months.month = anticipateIncome.anticipateIncomeDate" + ) + + return jdbcDao.queryForList(sql.toString(), args) + } + fun CashFlowAnticipateExpenditure(args: Map): List> { + val sql = StringBuilder("select" + + " p.id, p.name," + + " date_format(p.planStart, '%Y-%m') as planStart," + + " date_format(p.planEnd, '%Y-%m') as planEnd," + + " month(p.planStart) as startMonth," + + " PERIOD_DIFF(DATE_FORMAT(p.planEnd, '%Y%m'), DATE_FORMAT(p.planStart, '%Y%m'))+1 AS 'Duration'," + + " ROUND(p.totalManhour / (PERIOD_DIFF(DATE_FORMAT(p.planEnd, '%Y%m'), DATE_FORMAT(p.planStart, '%Y%m'))+1), 2) AS 'Average Manhours'," + + " p.teamLead, p.totalManhour" + + " FROM project p" + + " WHERE p.status = 'On-going'" + + " and p.id in (:productIds)" + + " order by teamLead, planStart" + ) + + return jdbcDao.queryForList(sql.toString(), args) + } + fun CashFlowLedger(args: Map): List> { + val sql = StringBuilder("select" + + " date," + + " COALESCE(ROUND(income, 2), 0) AS income," + + " COALESCE(ROUND(expenditure, 2), 0) AS expenditure," + + " ROUND(SUM(COALESCE(income, 0) - COALESCE(expenditure, 0)) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), 2) AS balance," + + " CASE" + + " WHEN income > 0 THEN paymentMilestone" + + " ELSE 'Monthly Manpower Expenditure'" + + " END AS remarks" + + " FROM" + + " (" + + " SELECT" + + " date_format(i.receiptDate, '%b %y') AS date," + + " sum(i.paidAmount) AS income," + + " NULL AS expenditure," + + " i.paymentMilestone AS paymentMilestone" + + " FROM" + + " project p" + + " LEFT JOIN invoice i ON p.code = i.projectCode" + + " WHERE" + + " p.id IN (:projectIds)" + + " AND i.paidAmount IS NOT NULL" + + " GROUP BY" + + " date_format(i.receiptDate, '%b %y')," + + " i.paymentMilestone" + + " UNION" + + " SELECT" + + " date_format(r.date, '%b %y') AS date," + + " NULL AS income," + + " sum(r.expenditure) AS expenditure," + + " NULL AS paymentMilestone" + + " FROM" + + " (" + + " SELECT" + + " t.recordDate AS date," + + " (COALESCE(SUM(t.normalConsumed), 0) * s2.hourlyRate) + (COALESCE(SUM(t.otConsumed), 0) * s2.hourlyRate * 1.0) AS expenditure" + + " FROM" + + " project p" + + " LEFT JOIN project_task pt ON p.id = pt.project_id" + + " LEFT JOIN timesheet t ON pt.id = t.projectTaskId" + + " LEFT JOIN staff s ON t.staffId = s.id" + + " LEFT JOIN salary s2 ON s.salaryId = s2.salaryPoint" + + " WHERE" + + " t.id IS NOT NULL" + + " AND p.id IN (:projectIds)" + + " GROUP BY" + + " s2.hourlyRate," + + " t.recordDate" + + " ) AS r" + + " GROUP BY" + + " date_format(r.date, '%b %y')" + + " ) AS combined_data" + + " ORDER BY" + + " date" + ) + + return jdbcDao.queryForList(sql.toString(), args) + } + fun TeamCashFlowIncome(args: Map): List> { + val sql = StringBuilder("select" + + " months.month as monthInvoice," + + " coalesce (invoice.invoiceMonth,'-') as invoiceMonth," + + " coalesce (invoice.income,0) as income," + + " SUM(COALESCE(invoice.income, 0)) OVER (ORDER BY months.month) AS cumulativeIncome" + + " FROM (" + + " SELECT 01 AS month" + + " UNION" + + " SELECT 02" + + " UNION" + + " SELECT 03" + + " UNION" + + " SELECT 04" + + " UNION" + + " SELECT 05" + + " UNION" + + " SELECT 06" + + " UNION" + + " SELECT 07" + + " UNION" + + " SELECT 08" + + " UNION" + + " SELECT 09" + + " UNION" + + " SELECT 10" + + " UNION" + + " SELECT 11" + + " union" + + " select 12" + + " ) AS months" + + " left join(" + + " select" + + " month(i.receiptDate) as invoiceMonth," + + " coalesce(sum(i.paidAmount),0) as income" + + " from project p" + + " left join team t on p.teamLead = t.teamLead" + + " left join invoice i on p.code = i.projectCode" + + " where p.status = 'On-going'" + + " and t.id = :teamId" + + " and year(i.receiptDate) = :year" + + " and i.id is not null" + + " group by month(i.receiptDate)" + + " ) as invoice on months.month = invoice.invoiceMonth" + ) + + return jdbcDao.queryForList(sql.toString(), args) + } + fun TeamCashFlowExpenditure(args: Map): List> { + val sql = StringBuilder("select" + + " months.month as monthExpenditure," + + " coalesce (expenditure.recordMonth,'-') as recordMonth," + + " coalesce (expenditure.expenditure,0) as expenditure," + + " SUM(COALESCE(expenditure.expenditure, 0)) OVER (ORDER BY months.month) AS cumulativeExpenditure" + + " FROM (" + + " SELECT 01 AS month" + + " UNION" + + " SELECT 02" + + " UNION" + + " SELECT 03" + + " UNION" + + " SELECT 04" + + " UNION" + + " SELECT 05" + + " UNION" + + " SELECT 06" + + " UNION" + + " SELECT 07" + + " UNION" + + " SELECT 08" + + " UNION" + + " SELECT 09" + + " UNION" + + " SELECT 10" + + " UNION" + + " SELECT 11" + + " union" + + " select 12" + + " ) AS months" + + " left join(" + + " SELECT" + + " r.recordMonth as recordMonth," + + " sum(r.cumulativeExpenditure) as expenditure" + + " from(" + + " select" + + " month(t.recordDate) as recordMonth," + + " (coalesce(sum(t.normalConsumed),0) * s2.hourlyRate) + (coalesce(sum(t.otConsumed),0) * s2.hourlyRate * 1.0) as cumulativeExpenditure" + + " from project p" + + " left join team t2 on p.teamLead = t2.teamLead" + + " left join project_task pt on p.id = pt.project_id" + + " left join timesheet t on pt.id = t.projectTaskId" + + " left join staff s on t.staffId = s.id" + + " left join salary s2 on s.salaryId = s2.salaryPoint" + + " where t.id is not null" + + " and t2.id = :teamId" + + " and year(t.recordDate) = :year" + + " group by month(t.recordDate),s2.hourlyRate" + + " ) as r" + + " group by r.recordMonth" + + " ) as expenditure on months.month = expenditure.recordMonth" + ) + + return jdbcDao.queryForList(sql.toString(), args) + } + } diff --git a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt index 7926dcc..dcd5b35 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt @@ -131,4 +131,24 @@ class DashboardController( val args = mutableMapOf() return dashboardService.CashFlowProject(args) } + @GetMapping("/searchCashFlowByMonth") + fun searchCashFlowByMonth(request: HttpServletRequest?): List> { + val args = mutableMapOf() + val projectIdList = request?.getParameter("projectIdList") + val year = request?.getParameter("year") + val projectIds = projectIdList?.split(",")?.map { it.toInt() }?.toList() + if (projectIds != null) { + args["projectIds"] = projectIds + } + if (year != null) { + args["year"] = year + } + val result = mutableMapOf() + val cashFlowMonthlyIncome = dashboardService.CashFlowMonthlyIncomeByMonth(args) + val cashFlowMonthlyExpenditure = dashboardService.CashFlowMonthlyExpenditureByMonth(args) + result["incomeList"] = cashFlowMonthlyIncome + result["expenditureList"] = cashFlowMonthlyExpenditure + return listOf(result) + } + } \ No newline at end of file From 912f4e8f8eff8a0b71a23b8e2ab41a6c624eea17 Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Mon, 27 May 2024 00:12:00 +0800 Subject: [PATCH 09/21] api improvement --- .../modules/data/service/DashboardService.kt | 41 +++++++++----- .../modules/data/web/DashboardController.kt | 55 ++++++++++++++++++- 2 files changed, 80 insertions(+), 16 deletions(-) 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 279a397..551df9e 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 @@ -629,13 +629,14 @@ open class DashboardService( } fun CashFlowReceivableAndExpenditure(args: Map): List> { val sql = StringBuilder("select" - + "coalesce (round(sum(i.paidAmount)/sum(i.issueAmount)*100,0)) as receivedPercentage," - + "coalesce (round(expenditure.expenditure/(sum(p.expectedTotalFee)*0.8)*100,0)) as expenditurePercentage," + + " coalesce (round(sum(i.paidAmount)/sum(i.issueAmount)*100,0),0) as receivedPercentage," + + " coalesce (round(expenditure.expenditure/(sum(p.expectedTotalFee)*0.8)*100,0),0) as expenditurePercentage," + " coalesce (sum(i.issueAmount),0) as totalInvoiced," + " coalesce (sum(i.paidAmount),0) as totalReceived," + " coalesce (sum(i.issueAmount) - sum(i.paidAmount),0) as receivable," + " coalesce (round(sum(p.expectedTotalFee)*0.8,2),0) as totalBudget," - + " coalesce (expenditure.expenditure) as totalExpenditure" + + " coalesce (expenditure.expenditure) as totalExpenditure," + + " coalesce (sum(p.expectedTotalFee)*0.8 - expenditure.expenditure,0) as expenditureReceivable" + " from project p" + " left join invoice i on p.code = i.projectCode" + " left join(" @@ -697,7 +698,8 @@ open class DashboardService( + " from project p" + " left join milestone m on p.id = m.projectId" + " left join milestone_payment mp on m.id = mp.milestoneId" - + " where p.id in (:projecIds)" + + " where p.id in (:projectIds)" + + " and year(mp.date) = :year" + " group by month(mp.date)" + " ) as anticipateIncome on months.month = anticipateIncome.anticipateIncomeDate" ) @@ -706,17 +708,26 @@ open class DashboardService( } fun CashFlowAnticipateExpenditure(args: Map): List> { val sql = StringBuilder("select" - + " p.id, p.name," - + " date_format(p.planStart, '%Y-%m') as planStart," - + " date_format(p.planEnd, '%Y-%m') as planEnd," - + " month(p.planStart) as startMonth," - + " PERIOD_DIFF(DATE_FORMAT(p.planEnd, '%Y%m'), DATE_FORMAT(p.planStart, '%Y%m'))+1 AS 'Duration'," - + " ROUND(p.totalManhour / (PERIOD_DIFF(DATE_FORMAT(p.planEnd, '%Y%m'), DATE_FORMAT(p.planStart, '%Y%m'))+1), 2) AS 'Average Manhours'," - + " p.teamLead, p.totalManhour" - + " FROM project p" - + " WHERE p.status = 'On-going'" - + " and p.id in (:productIds)" - + " order by teamLead, planStart" + + " p.id, p.name," + + " date_format(p.planStart, '%Y-%m') as planStart," + + " date_format(p.planEnd, '%Y-%m') as planEnd," + + " case" + + " when year(p.planStart) < :year then 1" + + " when year(p.planStart) = :year then month(p.planStart)" + + " end as startMonth," + + " case" + + " when year(p.planStart) < :year and year(p.planEnd) > :year then 12" + + " when year(p.planStart) < :year and year(p.planEnd) = :year then month(p.planEnd)" + + " when year(p.planStart) = :year and year(p.planEnd) > :year then 12 - month(p.planStart)" + + " else PERIOD_DIFF(DATE_FORMAT(p.planEnd, '%Y%m'), DATE_FORMAT(p.planStart, '%Y%m'))+1" + + " end AS 'Duration'," + + " ROUND(p.totalManhour / (PERIOD_DIFF(DATE_FORMAT(p.planEnd, '%Y%m'), DATE_FORMAT(p.planStart, '%Y%m'))+1), 2) AS 'AverageManhours'," + + " p.teamLead, p.totalManhour" + + " FROM project p" + + " WHERE p.status = 'On-going'" + + " and p.id in (1,2,3,4,5,6)" + + " and (year(p.planStart) <= :year and year(p.planEnd) >= :year)" + + " order by teamLead, planStart" ) return jdbcDao.queryForList(sql.toString(), args) diff --git a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt index dcd5b35..7e6f150 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt @@ -150,5 +150,58 @@ class DashboardController( result["expenditureList"] = cashFlowMonthlyExpenditure return listOf(result) } - + @GetMapping("/searchCashFlowReceivableAndExpenditure") + fun searchCashFlowReceivableAndExpenditure(request: HttpServletRequest?): List> { + val args = mutableMapOf() + val projectIdList = request?.getParameter("projectIdList") + val projectIds = projectIdList?.split(",")?.map { it.toInt() }?.toList() + if (projectIds != null) { + args["projectIds"] = projectIds + } + return dashboardService.CashFlowReceivableAndExpenditure(args) + } + @GetMapping("/searchCashFlowAnticipate") + fun searchCashFlowAnticipate(request: HttpServletRequest?): List> { + val args = mutableMapOf() + val projectIdList = request?.getParameter("projectIdList") + val year = request?.getParameter("year") + val projectIds = projectIdList?.split(",")?.map { it.toInt() }?.toList() + if (projectIds != null) { + args["projectIds"] = projectIds + } + if (year != null) { + args["year"] = year + } + val result = mutableMapOf() + val cashFlowAnticipateIncome = dashboardService.CashFlowAnticipateIncome(args) + val cashFlowAnticipateExpenditure = dashboardService.CashFlowAnticipateExpenditure(args) + result["anticipateIncomeList"] = cashFlowAnticipateIncome + result["anticipateExpenditureList"] = cashFlowAnticipateExpenditure + return listOf(result) + } + @GetMapping("/searchCashFlowLedger") + fun searchCashFlowLedger(request: HttpServletRequest?): List> { + val args = mutableMapOf() + val projectIdList = request?.getParameter("projectIdList") + val projectIds = projectIdList?.split(",")?.map { it.toInt() }?.toList() + if (projectIds != null) { + args["projectIds"] = projectIds + } + return dashboardService.CashFlowLedger(args) + } + @GetMapping("/searchTeamCashFlow") + fun searchTeamCashFlow(request: HttpServletRequest?): List> { + val args = mutableMapOf() + val teamIdList = request?.getParameter("teamIdList") + val teamIds = teamIdList?.split(",")?.map { it.toInt() }?.toList() + if (teamIds != null) { + args["teamIds"] = teamIds + } + val result = mutableMapOf() + val teamCashFlowIncome = dashboardService.TeamCashFlowIncome(args) + val teamCashFlowExpenditure = dashboardService.TeamCashFlowExpenditure(args) + result["teamCashFlowIncome"] = teamCashFlowIncome + result["teamCashFlowExpenditure"] = teamCashFlowExpenditure + return listOf(result) + } } \ No newline at end of file From 05bdd09e005fe52fc8b499e68b08071a544ae5bd Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 27 May 2024 15:10:41 +0800 Subject: [PATCH 10/21] [Remove] mock_team_leads & mock_staffs --- .../20240318_01_wayne/02_mock_team_leads.sql | 35 ------ .../20240414_01_wayne/01_mock_staffs.sql | 114 ------------------ 2 files changed, 149 deletions(-) delete mode 100644 src/main/resources/db/changelog/changes/20240318_01_wayne/02_mock_team_leads.sql delete mode 100644 src/main/resources/db/changelog/changes/20240414_01_wayne/01_mock_staffs.sql diff --git a/src/main/resources/db/changelog/changes/20240318_01_wayne/02_mock_team_leads.sql b/src/main/resources/db/changelog/changes/20240318_01_wayne/02_mock_team_leads.sql deleted file mode 100644 index ce29748..0000000 --- a/src/main/resources/db/changelog/changes/20240318_01_wayne/02_mock_team_leads.sql +++ /dev/null @@ -1,35 +0,0 @@ --- liquibase formatted sql - --- changeset wayne:mock_team_leads - -INSERT INTO `user` (name, username, password) VALUES - ('Wayne Lee','wlee','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'), - ('Ming Chan','mchan','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'); - -INSERT INTO company (companyCode, name) VALUES - ('ABC', 'Company ABC'); - -INSERT INTO team (name, code) VALUES - ('Team Wayne Lee', 'WL'), - ('Team Ming Chan', 'MC'); - -INSERT INTO salary_effective (date, salaryId) VALUES - (current_date, 1); - -INSERT INTO staff (userId, name, staffId, companyId, teamId, salaryEffId) VALUES - ( - (SELECT id from `user` where username = 'wlee'), - 'Wayne Lee', - '001', - (SELECT id from company where companyCode = 'ABC'), - (SELECT id from team where code = 'WL'), - (SELECT id from salary_effective where salaryId = 1) - ), - ( - (SELECT id from `user` where username = 'mchan'), - 'Ming Chan', - '002', - (SELECT id from company where companyCode = 'ABC'), - (SELECT id from team where code = 'MC'), - (SELECT id from salary_effective where salaryId = 1) - ); \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20240414_01_wayne/01_mock_staffs.sql b/src/main/resources/db/changelog/changes/20240414_01_wayne/01_mock_staffs.sql deleted file mode 100644 index aa90e7a..0000000 --- a/src/main/resources/db/changelog/changes/20240414_01_wayne/01_mock_staffs.sql +++ /dev/null @@ -1,114 +0,0 @@ --- liquibase formatted sql --- changeset wayne:mock_staffs - -INSERT INTO `user` (name, username, password) VALUES - ('Albert Lam','alam','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'), - ('Bernard Chou','bchou','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'), - ('Carl Junior','cjunior','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'), - ('Denis Lau','dlau','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'), - ('Edward Wong','ewong','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'), - ('Fred Cheung','fcheung','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'), - ('Gordon Ramsey','gramsey','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'), - ('Heather Stewards','hstewards','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'), - ('Ivan Foo','ifoo','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'), - ('Jack Son','json','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'), - ('Kurt Land','kland','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'), - ('Lawrence Arnold','larnold','$2a$10$65S7/AhKn8MldlYmvFN5JOfr1yaULwFNDIhTskLTuUCKgbbs8sFAi'); - -INSERT INTO staff (userId, name, staffId, companyId, teamId, salaryEffId) VALUES - ( - (SELECT id from `user` where username = 'alam'), - 'Albert Lam', - '003', - 1, - 1, - 1 - ), - ( - (SELECT id from `user` where username = 'bchou'), - 'Bernard Chou', - '004', - 1, - 2, - 1 - ), - ( - (SELECT id from `user` where username = 'cjunior'), - 'Carl Junior', - '005', - 1, - 1, - 1 - ), - ( - (SELECT id from `user` where username = 'dlau'), - 'Denis Lau', - '006', - 1, - 2, - 1 - ), - ( - (SELECT id from `user` where username = 'ewong'), - 'Edward Wong', - '007', - 1, - 1, - 1 - ), - ( - (SELECT id from `user` where username = 'fcheung'), - 'Fred Cheung', - '008', - 1, - 2, - 1 - ), - ( - (SELECT id from `user` where username = 'gramsey'), - 'Gordon Ramsey', - '009', - 1, - 1, - 1 - ), - ( - (SELECT id from `user` where username = 'hstewards'), - 'Heather Stewards', - '010', - 1, - 2, - 1 - ), - ( - (SELECT id from `user` where username = 'ifoo'), - 'Ivan Foo', - '011', - 1, - 1, - 1 - ), - ( - (SELECT id from `user` where username = 'json'), - 'Jack Son', - '012', - 1, - 2, - 1 - ), - ( - (SELECT id from `user` where username = 'kland'), - 'Kurt Land', - '013', - 1, - 1, - 1 - ), - ( - (SELECT id from `user` where username = 'larnold'), - 'Lawrence Arnold', - '014', - 1, - 2, - 1 - ); \ No newline at end of file From fe9d7037e54aa19beec0a6fec36be2e4d435fb4c Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 27 May 2024 15:10:54 +0800 Subject: [PATCH 11/21] Update task --- .../20240527_01_cyril/01_update_task.sql | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/main/resources/db/changelog/changes/20240527_01_cyril/01_update_task.sql diff --git a/src/main/resources/db/changelog/changes/20240527_01_cyril/01_update_task.sql b/src/main/resources/db/changelog/changes/20240527_01_cyril/01_update_task.sql new file mode 100644 index 0000000..ddfd700 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240527_01_cyril/01_update_task.sql @@ -0,0 +1,123 @@ +-- liquibase formatted sql +-- changeset cyril:task + +UPDATE task +SET name='1.1 Prepare / revise Cost Estimate / Cost Plan' +WHERE id=1; +UPDATE task +SET name='1.2 Cost estimation and cost studies' +WHERE id=2; +UPDATE task +SET name='1.3 Cash flow forecast' +WHERE id=3; +UPDATE task +SET name='1.4 Attend meetings' +WHERE id=4; +UPDATE task +SET name='2.1 Advise on tendering, contractual and procurement arrangement',taskGroupId=2 +WHERE id=5; +UPDATE task +SET name='2.2 Drafting / vetting front-part' +WHERE id=6; +UPDATE task +SET name='2.3 Carry out pre-qualification exercise / EOI' +WHERE id=7; +UPDATE task +SET name='2.4 Measurement & billing for 1st tender out' +WHERE id=8; +UPDATE task +SET name='2.5 Measurement & billing for tender addendum' +WHERE id=9; +UPDATE task +SET name='2.6 Bulk checking' +WHERE id=10; +UPDATE task +SET name='2.7 Edit tender documents (Incl. Bills of Quantities / SOR)' +WHERE id=11; +UPDATE task +SET name='2.8 Preparation of pre-tender estimates' +WHERE id=12; +UPDATE task +SET name='2.9 Update cash flow forecast' +WHERE id=13; +UPDATE task +SET name='2.10 Attend meetings' +WHERE id=14; +UPDATE task +SET name='3.1 Evaluation of tenders (incl. rate analysis)',taskGroupId=3 +WHERE id=15; +UPDATE task +SET name='3.2 Preparation of tender queries',taskGroupId=3 +WHERE id=16; +UPDATE task +SET name='3.3 Attend tender interviews' +WHERE id=17; +UPDATE task +SET name='3.4 Preparation of Report on Tenderers' +WHERE id=18; +UPDATE task +SET name='3.5 Draft Letter of Acceptance / Award' +WHERE id=19; +UPDATE task +SET name='3.6 Preparation of Contract Documents' +WHERE id=20; +UPDATE task +SET name='4.1 Check insurance policies, surety bond, guarantee, etc.',taskGroupId=4 +WHERE id=21; +UPDATE task +SET name='4.2 Valuation for interim/final payments (incl. site visits)',taskGroupId=4 +WHERE id=22; +UPDATE task +SET name='4.3 Preparation of financial statements (incl. cash flow forecasts)',taskGroupId=4 +WHERE id=23; +UPDATE task +SET name='4.4 Cost check / advice on alterative design solutions' +WHERE id=24; +UPDATE task +SET name='4.5 Cost estimation for draft AIs/EIs/SIs' +WHERE id=25; +UPDATE task +SET name='4.6 Advise on contractual issues & evaluate monetary claims' +WHERE id=26; +UPDATE task +SET name='4.7 Measurement & valuation of variations / prime cost & provisional sums' +WHERE id=27; +UPDATE task +SET name='4.8 Negotiation and settlement of final accounts (incl. meetings)' +WHERE id=28; +UPDATE task +SET name='4.9 Preparation of Statement of Final Account' +WHERE id=29; +UPDATE task +SET name='4.10 Preparation of Cost Analysis for the completed project' +WHERE id=30; +UPDATE task +SET name='4.11 Check / review draft final bills' +WHERE id=31; +UPDATE task +SET name='4.12 Carry out site check for draft final bills' +WHERE id=32; +UPDATE task +SET name='4.13 Attend meetings (other than final account meetings)' +WHERE id=33; +UPDATE task +SET name='5.1 Preparation of Fee Proposal / EOI',taskGroupId=5 +WHERE id=34; +UPDATE task +SET name='5.2 Attend Management Meeting / Management Workshop',taskGroupId=5 +WHERE id=35; +UPDATE task +SET name='5.3 Preparation of project budget i.e. manhours vs fee receivables',taskGroupId=5 +WHERE id=36; +UPDATE task +SET name='5.4 Attend Local / International Conference / Seminar / Webinar' +WHERE id=37; +UPDATE task +SET name='5.5 Preparing supplementary agreements (SA)' +WHERE id=38; +UPDATE task +SET name='5.6 Measurement / ad hoc tasks for Contractor' +WHERE id=39; +UPDATE task +SET name='5.7 Management Timesheet Allocation' +WHERE id=40; \ No newline at end of file From 194c69b125715648d7c067f270158a4659997642 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Mon, 27 May 2024 16:19:36 +0800 Subject: [PATCH 12/21] update report --- .../modules/report/service/ReportService.kt | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 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 126f1fe..27ccb5e 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 @@ -60,6 +60,30 @@ open class ReportService( private val COMPLETION_PROJECT = "templates/report/AR05_Project Completion Report.xlsx" // ==============================|| GENERATE REPORT ||============================== // + fun generalCreateReportIndexed( // just loop through query records one by one, return rowIndex + sheet: Sheet, + result: List>, + startRow: Int, + startColumn: Int + ): Int { + var rowIndex = startRow + var columnIndex = startColumn + result.forEachIndexed { index, obj -> + var tempCell = sheet.getRow(rowIndex).createCell(columnIndex) + tempCell.setCellValue((index + 1).toDouble()) + val keys = obj.keys.toList() + keys.forEachIndexed { keyIndex, key -> + tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex) + .createCell(columnIndex + keyIndex + 1) + when (obj[key]) { + is Double -> tempCell.setCellValue(obj[key] as Double) + else -> tempCell.setCellValue(obj[key] as String) + } + } + rowIndex++ + } + return rowIndex + } fun genFinancialStatusReport(teamLeadId: Long): ByteArray { @@ -1286,19 +1310,8 @@ open class ReportService( rowIndex = 5 columnIndex = 0 - result.forEachIndexed { index, obj -> - tempCell = sheet.getRow(rowIndex).createCell(columnIndex) - tempCell.setCellValue((index + 1).toDouble()) - val keys = obj.keys.toList() - keys.forEachIndexed { keyIndex, key -> - tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex).createCell(columnIndex + keyIndex + 1) - when (obj[key]) { - is Double -> tempCell.setCellValue(obj[key] as Double) - else -> tempCell.setCellValue(obj[key] as String ) - } - } - rowIndex++ - } + + generalCreateReportIndexed(sheet, result, rowIndex, columnIndex) return workbook } @@ -1337,20 +1350,9 @@ open class ReportService( rowIndex = 6 columnIndex = 0 - result.forEachIndexed { index, obj -> - tempCell = sheet.getRow(rowIndex).createCell(columnIndex) - tempCell.setCellValue((index + 1).toDouble()) - val keys = obj.keys.toList() - keys.forEachIndexed { keyIndex, key -> - tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex) - .createCell(columnIndex + keyIndex + 1) - when (obj[key]) { - is Double -> tempCell.setCellValue(obj[key] as Double) - else -> tempCell.setCellValue(obj[key] as String) - } - } - rowIndex++ - } + +// val currRow = generalCreateReportIndexed(sheet, result, rowIndex, columnIndex) + generalCreateReportIndexed(sheet, result, rowIndex, columnIndex) val sheetCF = sheet.sheetConditionalFormatting val rule1 = sheetCF.createConditionalFormattingRule("AND(J7 >= $lowerLimit, J7 <= 1)") From fd0f7fc0cf9b9dd9e8db8b1c112d988da01e8631 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 27 May 2024 16:19:39 +0800 Subject: [PATCH 13/21] update project potential delay report --- .../modules/report/service/ReportService.kt | 316 ++++++++++-------- .../modules/report/web/ReportController.kt | 8 +- .../modules/report/web/model/ReportRequest.kt | 2 + 3 files changed, 179 insertions(+), 147 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 126f1fe..b3756b9 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 @@ -5,6 +5,7 @@ import com.ffii.tsms.modules.data.entity.Customer import com.ffii.tsms.modules.data.entity.Salary import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.Team +import com.ffii.tsms.modules.project.entity.GradeAllocation import com.ffii.tsms.modules.project.entity.Invoice import com.ffii.tsms.modules.project.entity.Milestone import com.ffii.tsms.modules.project.entity.Project @@ -32,6 +33,7 @@ import java.sql.Time import java.time.LocalDate import java.time.YearMonth import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit import java.util.* import kotlin.jvm.optionals.getOrElse @@ -56,7 +58,8 @@ open class ReportService( private val SALART_LIST_TEMPLATE = "templates/report/Salary Template.xlsx" private val LATE_START_REPORT = "templates/report/AR01_Late Start Report v01.xlsx" private val RESOURCE_OVERCONSUMPTION_REPORT = "templates/report/AR03_Resource Overconsumption.xlsx" - private val COMPLETE_PROJECT_OUTSTANDING_RECEIVABLE = "templates/report/AR06_Project Completion Report with Outstanding Accounts Receivable v02.xlsx" + private val COMPLETE_PROJECT_OUTSTANDING_RECEIVABLE = + "templates/report/AR06_Project Completion Report with Outstanding Accounts Receivable v02.xlsx" private val COMPLETION_PROJECT = "templates/report/AR05_Project Completion Report.xlsx" // ==============================|| GENERATE REPORT ||============================== // @@ -145,6 +148,8 @@ open class ReportService( searchedClient: String, projects: List, timesheets: List, + numberOfDays: Int, + projectCompletion: Int, ): ByteArray { // Generate the Excel report with query results val workbook: Workbook = createProjectPotentialDelayReport( @@ -152,6 +157,8 @@ open class ReportService( searchedClient, projects, timesheets, + numberOfDays, + projectCompletion, PROJECT_POTENTIAL_DELAY_REPORT ) @@ -209,6 +216,7 @@ open class ReportService( return outputStream.toByteArray() } + @Throws(IOException::class) fun generateProjectCompletionReport( args: MutableMap, @@ -811,6 +819,8 @@ open class ReportService( searchedClient: String, projects: List, timesheets: List, + numberOfDays: Int, + projectCompletion: Int, templatePath: String, ): Workbook { // please create a new function for each report template @@ -897,35 +907,36 @@ open class ReportService( } project.milestones.forEach { milestone: Milestone -> - logger.info(milestone.id) - - val tempRow = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex) - rowIndex++ - - tempRow.apply { - createCell(7).apply { - setCellValue(milestone.taskGroup?.name ?: "N/A") - } - - createCell(8).apply { - setCellValue(milestone.endDate?.format(DATE_FORMATTER) ?: "N/A") - } - createCell(9).apply { - cellStyle.dataFormat = workbook.createDataFormat().getFormat("0.00%") + val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]?.sum() ?: 0.0 + val resourceUtilization = manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) +// logger.info(project.name + " : " + milestone.taskGroup?.name + " : " + ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate)) +// logger.info(numberOfDays) + if (ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate) <= numberOfDays.toLong() && resourceUtilization <= projectCompletion.toDouble() / 100.0) { + val tempRow = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex) + rowIndex++ + + tempRow.apply { + createCell(7).apply { + setCellValue(milestone.taskGroup?.name ?: "N/A") + } - if (groupedTimesheets.containsKey(Pair(project.id, milestone.id))) { - val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]!!.sum() + createCell(8).apply { + setCellValue(milestone.endDate?.format(DATE_FORMATTER) ?: "N/A") + } - logger.info("manHoursSpent: $manHoursSpent") - logger.info("milestone.stagePercentAllocation: " + milestone.stagePercentAllocation) - logger.info("project.totalManhour: " + project.totalManhour) + createCell(9).apply { + cellStyle.dataFormat = workbook.createDataFormat().getFormat("0.00%") - val resourceUtilization = - manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) - setCellValue(resourceUtilization ?: 0.0) - } else { - setCellValue(0.0) +// if (groupedTimesheets.containsKey(Pair(project.id, milestone.id))) { +// val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]!!.sum() +// +// val resourceUtilization = +// manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) + setCellValue(resourceUtilization) +// } else { +// setCellValue(0.0) +// } } } } @@ -1259,6 +1270,7 @@ open class ReportService( return workbook } + private fun createProjectCompletionReport( args: MutableMap, result: List>, @@ -1291,10 +1303,11 @@ open class ReportService( tempCell.setCellValue((index + 1).toDouble()) val keys = obj.keys.toList() keys.forEachIndexed { keyIndex, key -> - tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex).createCell(columnIndex + keyIndex + 1) + tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex) + .createCell(columnIndex + keyIndex + 1) when (obj[key]) { is Double -> tempCell.setCellValue(obj[key] as Double) - else -> tempCell.setCellValue(obj[key] as String ) + else -> tempCell.setCellValue(obj[key] as String) } } rowIndex++ @@ -1516,107 +1529,111 @@ open class ReportService( ) return jdbcDao.queryForList(sql.toString(), args) } + open fun getProjectCompletionReport(args: Map): List> { - val sql = StringBuilder("select" - + " result.code, " - + " result.name, " - + " result.teamCode, " - + " result.custCode, " ) + val sql = StringBuilder( + "select" + + " result.code, " + + " result.name, " + + " result.teamCode, " + + " result.custCode, " + ) if (args.get("outstanding") as Boolean) { sql.append(" result.projectFee - (result.totalBudget - COALESCE(i.issueAmount , 0) + COALESCE(i.issueAmount, 0) - COALESCE(i.paidAmount, 0)) as `Receivable Remained`, ") } sql.append( " DATE_FORMAT(result.actualEnd, '%d/%m/%Y') as actualEnd " - + " from ( " - + " SELECT " - + " pt.project_id, " - + " min(p.code) as code, " - + " min(p.name) as name, " - + " min(t.code) as teamCode, " - + " min(c.code) as custCode, " - + " min(p.actualEnd) as actualEnd, " - + " min(p.expectedTotalFee) as projectFee, " - + " sum(COALESCE(tns.totalConsumed*sal.hourlyRate, 0)) as totalBudget " - + " FROM ( " - + " SELECT " - + " t.staffId, " - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " - + " t.projectTaskId AS taskId " - + " FROM timesheet t " - + " LEFT JOIN staff s ON t.staffId = s.id " - + " LEFT JOIN team te on s.teamId = te.id " - + " GROUP BY t.staffId, t.projectTaskId " - + " order by t.staffId " - + " ) AS tns " - + " right join project_task pt ON tns.taskId = pt.id " - + " left join project p on p.id = pt.project_id " - + " left JOIN staff s ON p.teamLead = s.id " - + " left join salary sal on s.salaryId = sal.salaryPoint " - + " left JOIN team t ON s.teamId = t.id " - + " left join customer c on c.id = p.customerId " - + " where p.deleted = false " - + " and p.status = 'Completed' " - + " and p.actualEnd BETWEEN :startDate and :endDate " - + " group by pt.project_id " - + " ) as result " - + " left join invoice i on result.code = i.projectCode " - + " order by result.actualEnd " + + " from ( " + + " SELECT " + + " pt.project_id, " + + " min(p.code) as code, " + + " min(p.name) as name, " + + " min(t.code) as teamCode, " + + " min(c.code) as custCode, " + + " min(p.actualEnd) as actualEnd, " + + " min(p.expectedTotalFee) as projectFee, " + + " sum(COALESCE(tns.totalConsumed*sal.hourlyRate, 0)) as totalBudget " + + " FROM ( " + + " SELECT " + + " t.staffId, " + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " + + " t.projectTaskId AS taskId " + + " FROM timesheet t " + + " LEFT JOIN staff s ON t.staffId = s.id " + + " LEFT JOIN team te on s.teamId = te.id " + + " GROUP BY t.staffId, t.projectTaskId " + + " order by t.staffId " + + " ) AS tns " + + " right join project_task pt ON tns.taskId = pt.id " + + " left join project p on p.id = pt.project_id " + + " left JOIN staff s ON p.teamLead = s.id " + + " left join salary sal on s.salaryId = sal.salaryPoint " + + " left JOIN team t ON s.teamId = t.id " + + " left join customer c on c.id = p.customerId " + + " where p.deleted = false " + + " and p.status = 'Completed' " + + " and p.actualEnd BETWEEN :startDate and :endDate " + + " group by pt.project_id " + + " ) as result " + + " left join invoice i on result.code = i.projectCode " + + " order by result.actualEnd " ) return jdbcDao.queryForList(sql.toString(), args) } open fun getProjectResourceOverconsumptionReport(args: Map): List> { - val sql = StringBuilder("WITH teamNormalConsumed AS (" - + " SELECT " - + " s.teamId, " - + " pt.project_id, " - + " SUM(tns.totalConsumed) AS totalConsumed, " - + " sum(tns.totalBudget) as totalBudget " - + " FROM ( " - + " SELECT " - + " t.staffId, " - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " - + " t.projectTaskId AS taskId, " - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget " - + " FROM timesheet t " - + " LEFT JOIN staff s ON t.staffId = s.id " - + " left join salary sal on sal.salaryPoint = s.salaryId " - + " GROUP BY t.staffId, t.projectTaskId " - + " ) AS tns " - + " INNER JOIN project_task pt ON tns.taskId = pt.id " - + " JOIN staff s ON tns.staffId = s.id " - + " JOIN team t ON s.teamId = t.id " - + " GROUP BY teamId, project_id " - + " ) " - + " SELECT " - + " p.code, " - + " p.name, " - + " t.code as team, " - + " c.code as client, " - + " p.expectedTotalFee * 0.8 as plannedBudget, " - + " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, " - + " COALESCE(p.totalManhour, 0) as plannedManhour, " - + " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, " - + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, " - + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, " - + " CASE " - + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " - + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " - + " then 'Potential Overconsumption' " - + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " - + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 " - + " then 'Overconsumption' " - + " else 'Within Budget' " - + " END as status " - + " FROM project p " - + " LEFT JOIN team t ON p.teamLead = t.teamLead " - + " LEFT JOIN staff s ON p.teamLead = s.id " - + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint " - + " LEFT JOIN customer c ON p.customerId = c.id " - + " left join teamNormalConsumed tns on tns.project_id = p.id " - + " WHERE p.deleted = false " - + " and p.status = 'On-going' " + val sql = StringBuilder( + "WITH teamNormalConsumed AS (" + + " SELECT " + + " s.teamId, " + + " pt.project_id, " + + " SUM(tns.totalConsumed) AS totalConsumed, " + + " sum(tns.totalBudget) as totalBudget " + + " FROM ( " + + " SELECT " + + " t.staffId, " + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " + + " t.projectTaskId AS taskId, " + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget " + + " FROM timesheet t " + + " LEFT JOIN staff s ON t.staffId = s.id " + + " left join salary sal on sal.salaryPoint = s.salaryId " + + " GROUP BY t.staffId, t.projectTaskId " + + " ) AS tns " + + " INNER JOIN project_task pt ON tns.taskId = pt.id " + + " JOIN staff s ON tns.staffId = s.id " + + " JOIN team t ON s.teamId = t.id " + + " GROUP BY teamId, project_id " + + " ) " + + " SELECT " + + " p.code, " + + " p.name, " + + " t.code as team, " + + " c.code as client, " + + " p.expectedTotalFee * 0.8 as plannedBudget, " + + " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, " + + " COALESCE(p.totalManhour, 0) as plannedManhour, " + + " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, " + + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, " + + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, " + + " CASE " + + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " + + " then 'Potential Overconsumption' " + + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 " + + " then 'Overconsumption' " + + " else 'Within Budget' " + + " END as status " + + " FROM project p " + + " LEFT JOIN team t ON p.teamLead = t.teamLead " + + " LEFT JOIN staff s ON p.teamLead = s.id " + + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint " + + " LEFT JOIN customer c ON p.customerId = c.id " + + " left join teamNormalConsumed tns on tns.project_id = p.id " + + " WHERE p.deleted = false " + + " and p.status = 'On-going' " ) if (args != null) { var statusFilter: String = "" @@ -2109,10 +2126,10 @@ open class ReportService( return workbook } - fun getCostAndExpense(clientId: Long?, teamId: Long?): List>{ + fun getCostAndExpense(clientId: Long?, teamId: Long?): List> { val sql = StringBuilder( " with cte_timesheet as ( " - + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId," + + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId," + " t.recordDate" + " from timesheet t" + " left join project_task pt on pt.id = t.projectTaskId" @@ -2134,13 +2151,13 @@ open class ReportService( + " left join team t2 on t2.id = s.teamId" + " where ISNULL(p.code) = False" ) - if(clientId != null){ + if (clientId != null) { sql.append( " and c.id = :clientId " ) } - if(teamId != null){ + if (teamId != null) { sql.append( " and p.teamLead = :teamId " ) @@ -2159,9 +2176,9 @@ open class ReportService( val queryList = jdbcDao.queryForList(sql.toString(), args) val costAndExpenseList = mutableListOf>() - for(item in queryList){ + for (item in queryList) { val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() - if(item["code"] !in costAndExpenseList){ + if (item["code"] !in costAndExpenseList) { costAndExpenseList.add( mapOf( "code" to item["code"], @@ -2169,22 +2186,27 @@ open class ReportService( "client" to item["client"], "teamLead" to item["teamLead"], "budget" to item["expectedTotalFee"], - "totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double, - "manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double ) - + (hourlyRate * item["otConsumed"]as Double * otFactor) + "totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double, + "manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double) + + (hourlyRate * item["otConsumed"] as Double * otFactor) ) ) - }else{ + } else { val existingMap = costAndExpenseList.find { it.containsValue(item["code"]) }!! costAndExpenseList[costAndExpenseList.indexOf(existingMap)] = existingMap.toMutableMap().apply { - put("totalManhours", get("manhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double)) - put("manhourExpenditure", get("manhourExpenditure") as Double + ((hourlyRate * item["normalConsumed"] as Double ) - + (hourlyRate * item["otConsumed"]as Double * otFactor))) + put( + "totalManhours", + get("manhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double) + ) + put( + "manhourExpenditure", + get("manhourExpenditure") as Double + ((hourlyRate * item["normalConsumed"] as Double) + + (hourlyRate * item["otConsumed"] as Double * otFactor)) + ) } } } - val result = costAndExpenseList.map { - item -> + val result = costAndExpenseList.map { item -> val budget = (item["budget"] as? Double)?.times(0.8) ?: 0.0 val budgetRemain = budget - (item["manhourExpenditure"] as? Double ?: 0.0) val remainingPercent = (budgetRemain / budget) @@ -2202,7 +2224,7 @@ open class ReportService( teamId: Long?, clientId: Long?, budgetPercentage: Double? - ): Workbook{ + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -2220,9 +2242,9 @@ open class ReportService( rowNum = 2 val row2: Row = sheet.getRow(rowNum) val row2Cell = row2.getCell(2) - if(teamId == null){ + if (teamId == null) { row2Cell.setCellValue("All") - }else{ + } else { val sql = StringBuilder( " select t.id, t.code, t.name, concat(t.code, \" - \" ,t.name) as teamLead from team t where t.id = :teamId " ) @@ -2233,9 +2255,9 @@ open class ReportService( rowNum = 3 val row3: Row = sheet.getRow(rowNum) val row3Cell = row3.getCell(2) - if(clientId == null){ + if (clientId == null) { row3Cell.setCellValue("All") - }else{ + } else { val sql = StringBuilder( " select c.id, c.name from customer c where c.id = :clientId " ) @@ -2245,15 +2267,15 @@ open class ReportService( val filterList: List> - if(budgetPercentage != null){ + if (budgetPercentage != null) { filterList = costAndExpenseList.filter { ((it["budgetPercentage"] as? Double) ?: 0.0) > budgetPercentage } - }else{ + } else { filterList = costAndExpenseList } rowNum = 6 - for(item in filterList){ + for (item in filterList) { val index = filterList.indexOf(item) val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) val cell = row.getCell(0) ?: row.createCell(0) @@ -2298,13 +2320,13 @@ open class ReportService( val cell7 = row.getCell(7) ?: row.createCell(7) cell7.apply { - cellFormula = "F${rowNum+1}-G${rowNum+1}" + cellFormula = "F${rowNum + 1}-G${rowNum + 1}" } CellUtil.setCellStyleProperty(cell7, "dataFormat", accountingStyle) val cell8 = row.getCell(8) ?: row.createCell(8) cell8.apply { - cellFormula = "H${rowNum+1}/F${rowNum+1}" + cellFormula = "H${rowNum + 1}/F${rowNum + 1}" } CellUtil.setCellStyleProperty(cell8, "dataFormat", percentStyle) @@ -2314,11 +2336,17 @@ open class ReportService( return workbook } - fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray{ + fun genCostAndExpenseReport(request: costAndExpenseRequest): ByteArray { val costAndExpenseList = getCostAndExpense(request.clientId, request.teamId) - val workbook: Workbook = createCostAndExpenseWorkbook(COSTANDEXPENSE_REPORT, costAndExpenseList, request.teamId, request.clientId, request.budgetPercentage) + val workbook: Workbook = createCostAndExpenseWorkbook( + COSTANDEXPENSE_REPORT, + costAndExpenseList, + request.teamId, + request.clientId, + request.budgetPercentage + ) val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) 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 9dc9282..ec12e2f 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 @@ -39,6 +39,7 @@ import com.ffii.tsms.modules.report.web.model.* import com.ffii.tsms.modules.timesheet.entity.Timesheet import org.springframework.data.domain.Example import org.springframework.data.domain.ExampleMatcher +import java.time.temporal.ChronoUnit @RestController @RequestMapping("/reports") @@ -55,7 +56,8 @@ class ReportController( private val leaveRepository: LeaveRepository, private val teamService: TeamService, private val customerService: CustomerService, - private val invoiceService: InvoiceService) { + private val invoiceService: InvoiceService, private val gradeAllocationRepository: GradeAllocationRepository +) { @PostMapping("/fetchProjectsFinancialStatusReport") @Throws(ServletRequestBindingException::class, IOException::class) @@ -105,10 +107,10 @@ class ReportController( }, matcher) val projects = if (team == null) projectRepository.findAll(exampleQuery) else projectRepository.findAll(exampleQuery).filter { it.teamLead == team.staff } - val projectTasks = projectTaskRepository.findAllByProjectIn(projects) val timesheets = timesheetRepository.findAllByProjectTaskIn(projectTasks) - val reportResult: ByteArray = excelReportService.generateProjectPotentialDelayReport(searchedTeam, searchedClient, projects, timesheets) + + val reportResult: ByteArray = excelReportService.generateProjectPotentialDelayReport(searchedTeam, searchedClient, projects, timesheets, request.numberOfDays, request.projectCompletion) return ResponseEntity.ok() .header("filename", "Project Potential Delay Report - " + LocalDate.now() + ".xlsx") .body(ByteArrayResource(reportResult)) 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 c36ca6a..0d17d82 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 @@ -27,6 +27,8 @@ data class ProjectCashFlowReportRequest ( data class ProjectPotentialDelayReportRequest ( val teamId: String, val clientId: String, + val numberOfDays: Int, + val projectCompletion: Int, ) data class StaffMonthlyWorkHourAnalysisReportRequest ( From 0bcfd07a35519df3c2fc0acea46d49cf5dea0bf1 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 27 May 2024 17:17:06 +0800 Subject: [PATCH 14/21] update position --- .../20240527_01_cyril/02_update_position.sql | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/main/resources/db/changelog/changes/20240527_01_cyril/02_update_position.sql diff --git a/src/main/resources/db/changelog/changes/20240527_01_cyril/02_update_position.sql b/src/main/resources/db/changelog/changes/20240527_01_cyril/02_update_position.sql new file mode 100644 index 0000000..3a22bae --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240527_01_cyril/02_update_position.sql @@ -0,0 +1,74 @@ +-- liquibase formatted sql +-- changeset cyril:position + +INSERT INTO position (description,name,code) +VALUES ('Resident Quantity Surveyor','Resident Quantity Surveyor','Resident QS'); +INSERT INTO position (description,name,code) +VALUES ('Resident AQS','Resident AQS','Resident AQS'); +INSERT INTO position (description,name,code) +VALUES ('Administrative Officer','Administrative Officer','Admin Officer'); +INSERT INTO position (description,name,code) +VALUES ('Secretary','Secretary','Secretary'); +INSERT INTO position (description,name,code) +VALUES ('CPMS Assessor','CPMS Assessor','CPMS Assessor'); +INSERT INTO position (description,name,code) +VALUES ('Co-ordinator','Co-ordinator','Co-ordinator'); +INSERT INTO position (description,name,code) +VALUES ('Admin Supporter','Admin Supporter','Admin Supporter'); +INSERT INTO position (description,name,code) +VALUES ('Receptionist','Receptionist','Receptionist'); +INSERT INTO position (description,name,code) +VALUES ('Junior Secretary','Junior Secretary','Junior Secretary'); +INSERT INTO position (description,name,code) +VALUES ('Clerk','Clerk','Clerk'); +INSERT INTO position (description,name,code) +VALUES ('Office Assistant','Office Assistant','Office Assistant'); +INSERT INTO position (description,name,code) +VALUES ('HR Assistant','HR Assistant','HR Assistant'); +INSERT INTO position (description,name,code) +VALUES ('Project Officer','Project Officer','Project Officer'); +INSERT INTO position (description,name,code) +VALUES ('Ex. Secretary','Ex. Secretary','Ex. Secretary'); +INSERT INTO position (description,name,code) +VALUES ('I.T. Officer','I.T. Officer','I.T. Officer'); +INSERT INTO position (description,name,code) +VALUES ('Resident Senior Quantity Surveyor','Resident Senior Quantity Surveyor','Resident SQS'); +INSERT INTO position (description,name,code) +VALUES ('Obsever (SQS)','Obsever (SQS)','Obsever (SQS)'); +INSERT INTO position (description,name,code) +VALUES ('HR Manager','HR Manager','HR Manager'); +INSERT INTO position (description,name,code) +VALUES ('Senior Consultant','Senior Consultant','Senior Consultant'); +UPDATE position +SET description='Quantity Surveying Trainee' +WHERE id=1; +UPDATE position +SET name='Assistant Quantity Surveyor',description='Assistant Quantity Surveyor',code='AQS' +WHERE id=2; +UPDATE position +SET name='Quantity Surveyor',description='Quantity Surveyor' +WHERE id=3; +UPDATE position +SET name='Senior Quantity Surveyor',description='Senior Quantity Surveyor',code='SQS' +WHERE id=4; +UPDATE position +SET description='Assistant Manager' +WHERE id=5; +UPDATE position +SET description='Deputy Manager' +WHERE id=6; +UPDATE position +SET description='Manager' +WHERE id=7; +UPDATE position +SET description='Senior Manager' +WHERE id=8; +UPDATE position +SET description='Assistant Director' +WHERE id=9; +UPDATE position +SET description='Deputy Director' +WHERE id=10; +UPDATE position +SET description='Director' +WHERE id=11; \ No newline at end of file From c13e8de11339586f844d87929cf9c14244f65e8e Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Mon, 27 May 2024 17:50:58 +0800 Subject: [PATCH 15/21] api update --- .../modules/data/service/DashboardService.kt | 36 ++++++++++++------- .../modules/data/web/DashboardController.kt | 11 +++--- 2 files changed, 31 insertions(+), 16 deletions(-) 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 551df9e..eeeceba 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 @@ -721,11 +721,14 @@ open class DashboardService( + " when year(p.planStart) = :year and year(p.planEnd) > :year then 12 - month(p.planStart)" + " else PERIOD_DIFF(DATE_FORMAT(p.planEnd, '%Y%m'), DATE_FORMAT(p.planStart, '%Y%m'))+1" + " end AS 'Duration'," + + " PERIOD_DIFF(DATE_FORMAT(p.planEnd, '%Y%m'), DATE_FORMAT(p.planStart, '%Y%m'))+1 as projectDuration," + + " p.expectedTotalFee*0.8 as totalBudget," + + " (p.expectedTotalFee*0.8) / (PERIOD_DIFF(DATE_FORMAT(p.planEnd, '%Y%m'), DATE_FORMAT(p.planStart, '%Y%m'))+1) as aniticipateExpenditure," + " ROUND(p.totalManhour / (PERIOD_DIFF(DATE_FORMAT(p.planEnd, '%Y%m'), DATE_FORMAT(p.planStart, '%Y%m'))+1), 2) AS 'AverageManhours'," + " p.teamLead, p.totalManhour" + " FROM project p" + " WHERE p.status = 'On-going'" - + " and p.id in (1,2,3,4,5,6)" + + " and p.id in (:projectIds)" + " and (year(p.planStart) <= :year and year(p.planEnd) >= :year)" + " order by teamLead, planStart" ) @@ -734,6 +737,7 @@ open class DashboardService( } fun CashFlowLedger(args: Map): List> { val sql = StringBuilder("select" + + " ROW_NUMBER() OVER (ORDER BY date, income, expenditure) AS id," + " date," + " COALESCE(ROUND(income, 2), 0) AS income," + " COALESCE(ROUND(expenditure, 2), 0) AS expenditure," @@ -830,13 +834,18 @@ open class DashboardService( + " left join team t on p.teamLead = t.teamLead" + " left join invoice i on p.code = i.projectCode" + " where p.status = 'On-going'" - + " and t.id = :teamId" - + " and year(i.receiptDate) = :year" - + " and i.id is not null" - + " group by month(i.receiptDate)" - + " ) as invoice on months.month = invoice.invoiceMonth" + ) + if (args != null) { + if (args.containsKey("teamId")) + sql.append(" AND t.id = :teamId") + } + sql.append(" and year(i.receiptDate) = :year" + + " and i.id is not null" + + " group by month(i.receiptDate)" + + " ) as invoice on months.month = invoice.invoiceMonth") + return jdbcDao.queryForList(sql.toString(), args) } fun TeamCashFlowExpenditure(args: Map): List> { @@ -885,13 +894,16 @@ open class DashboardService( + " left join staff s on t.staffId = s.id" + " left join salary s2 on s.salaryId = s2.salaryPoint" + " where t.id is not null" - + " and t2.id = :teamId" - + " and year(t.recordDate) = :year" - + " group by month(t.recordDate),s2.hourlyRate" - + " ) as r" - + " group by r.recordMonth" - + " ) as expenditure on months.month = expenditure.recordMonth" ) + if (args != null) { + if (args.containsKey("teamId")) + sql.append(" AND t2.id = :teamId") + } + sql.append(" and year(t.recordDate) = :year" + + " group by month(t.recordDate),s2.hourlyRate" + + " ) as r" + + " group by r.recordMonth" + + " ) as expenditure on months.month = expenditure.recordMonth") return jdbcDao.queryForList(sql.toString(), args) } diff --git a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt index 7e6f150..7167dfe 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt @@ -192,10 +192,13 @@ class DashboardController( @GetMapping("/searchTeamCashFlow") fun searchTeamCashFlow(request: HttpServletRequest?): List> { val args = mutableMapOf() - val teamIdList = request?.getParameter("teamIdList") - val teamIds = teamIdList?.split(",")?.map { it.toInt() }?.toList() - if (teamIds != null) { - args["teamIds"] = teamIds + val teamId = request?.getParameter("teamId") + val year = request?.getParameter("year") + if (teamId != null) { + args["teamId"] = teamId + } + if (year != null) { + args["year"] = year } val result = mutableMapOf() val teamCashFlowIncome = dashboardService.TeamCashFlowIncome(args) From 409bc7740966ff84805bbe15d94c973dc54cb004 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Mon, 27 May 2024 17:59:28 +0800 Subject: [PATCH 16/21] update --- .../tsms/modules/data/web/TeamController.kt | 20 ++++++++++++++++++- .../modules/data/web/models/NewTeamRequest.kt | 3 ++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt b/src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt index bb00181..da737f9 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt @@ -1,8 +1,11 @@ package com.ffii.tsms.modules.data.web +import com.ffii.core.exception.NotFoundException import com.ffii.core.response.RecordsRes import com.ffii.core.utils.CriteriaArgsBuilder +import com.ffii.core.utils.Params import com.ffii.tsms.modules.data.entity.Team +import com.ffii.tsms.modules.data.service.StaffsService import com.ffii.tsms.modules.data.service.TeamService import com.ffii.tsms.modules.data.web.models.NewTeamRequest import jakarta.servlet.http.HttpServletRequest @@ -15,7 +18,10 @@ import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/team") -class TeamController(private val teamService: TeamService) { +class TeamController( + private val teamService: TeamService, + private val staffsService: StaffsService, + ) { @GetMapping fun allStaff(args: Map): List> { @@ -34,6 +40,18 @@ class TeamController(private val teamService: TeamService) { return teamService.getTeamDetail(args); } + @GetMapping("/{id}") + fun getStaff(@PathVariable id: Long): MutableMap { + val map: MutableMap = mutableMapOf("team" to teamService.find(id).orElseThrow { NotFoundException() }) + val staffList = staffsService.findAllByTeamId(id).orElseThrow { NotFoundException() } + val staffIdList: MutableList = mutableListOf() + for (staff in staffList) { + staffIdList.add(staff.id as Long) + } + map["staffIds"] = staffIdList + return map + } + // @Transactional(rollbackFor = [Exception::class]) @DeleteMapping("/delete/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) diff --git a/src/main/java/com/ffii/tsms/modules/data/web/models/NewTeamRequest.kt b/src/main/java/com/ffii/tsms/modules/data/web/models/NewTeamRequest.kt index 224a181..c320a7d 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/models/NewTeamRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/models/NewTeamRequest.kt @@ -6,7 +6,8 @@ import java.time.LocalDate data class NewTeamRequest ( val addStaffIds: List?, - + val name: String, + val code: String, val deleteStaffIds: List?, val description: String?, val id: Long? From 0f4fdaf43349e203855cbf6943ff9a5918fef83f Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Mon, 27 May 2024 18:11:12 +0800 Subject: [PATCH 17/21] update --- .../tsms/modules/data/service/TeamService.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt b/src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt index b1893aa..676d98e 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt @@ -41,15 +41,15 @@ open class TeamService( val ids = req.addStaffIds!! // println(ids) val teamLead = staffRepository.findById(ids[0]).orElseThrow() - val teamName = "Team " + teamLead.name - - val initials = teamLead.name.split(" ").map { it.first() } - val teamCode = initials.joinToString("") +// val teamName = "Team " + teamLead.name +// +// val initials = teamLead.name.split(" ").map { it.first() } +// val teamCode = initials.joinToString("") val team = Team().apply { this.staff = teamLead - name = teamName - code = teamCode + name = req.name + code = req.code description = req.description } teamRepository.saveAndFlush(team) @@ -73,11 +73,12 @@ open class TeamService( if (addIds.isNotEmpty()) { val leader = staffRepository.findById(addIds[0].toLong()).orElseThrow() - teamName = "Team " + leader.name +// teamName = "Team " + leader.name + teamName = req.name teamLead = leader; - - val initials = leader.name.split(" ").map { it.first() } - teamCode = initials.joinToString("") +// val initials = leader.name.split(" ").map { it.first() } +// teamCode = initials.joinToString("") + teamCode = req.code } else { teamLead = team.staff teamName = team.name From f43020239353196c5fd641a036c0e15310ed8a74 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Tue, 28 May 2024 11:23:28 +0800 Subject: [PATCH 18/21] update --- .../ffii/tsms/modules/data/service/TeamService.kt | 12 ++++-------- .../com/ffii/tsms/modules/data/web/TeamController.kt | 11 +++++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt b/src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt index 676d98e..22c9318 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt @@ -68,27 +68,23 @@ open class TeamService( val addIds = req.addStaffIds ?: listOf() val teamLead: Staff - val teamName: String - val teamCode: String +// val teamName: String +// val teamCode: String if (addIds.isNotEmpty()) { val leader = staffRepository.findById(addIds[0].toLong()).orElseThrow() // teamName = "Team " + leader.name - teamName = req.name teamLead = leader; // val initials = leader.name.split(" ").map { it.first() } // teamCode = initials.joinToString("") - teamCode = req.code } else { teamLead = team.staff - teamName = team.name - teamCode = team.code } team.apply { this.staff = teamLead - name = teamName - code = teamCode + name = req.name + code = req.code description = req.description } diff --git a/src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt b/src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt index da737f9..95d9831 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt @@ -41,15 +41,18 @@ class TeamController( } @GetMapping("/{id}") - fun getStaff(@PathVariable id: Long): MutableMap { - val map: MutableMap = mutableMapOf("team" to teamService.find(id).orElseThrow { NotFoundException() }) + fun getStaff(@PathVariable id: Long): Map { val staffList = staffsService.findAllByTeamId(id).orElseThrow { NotFoundException() } val staffIdList: MutableList = mutableListOf() for (staff in staffList) { staffIdList.add(staff.id as Long) } - map["staffIds"] = staffIdList - return map +// val map: Map = java.util.Map.of("team" to teamService.find(id).orElseThrow { NotFoundException() }) +// map["staffIds"] = staffIdList + return java.util.Map.of( + "team", staffsService.find(id).orElseThrow { NotFoundException() }, + "staffIds", staffIdList + ) } // @Transactional(rollbackFor = [Exception::class]) From 03dd263a4285331fd9f7326edf7aa5c8a434b30f Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Tue, 28 May 2024 11:45:22 +0800 Subject: [PATCH 19/21] update user --- .../java/com/ffii/tsms/modules/user/req/UpdateUserReq.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/ffii/tsms/modules/user/req/UpdateUserReq.java b/src/main/java/com/ffii/tsms/modules/user/req/UpdateUserReq.java index 853f04b..6bd7e68 100644 --- a/src/main/java/com/ffii/tsms/modules/user/req/UpdateUserReq.java +++ b/src/main/java/com/ffii/tsms/modules/user/req/UpdateUserReq.java @@ -18,6 +18,9 @@ public class UpdateUserReq { @NotBlank private String name; + @NotBlank + private String username; + private String firstname; private String lastname; private LocalDate expiryDate; @@ -55,6 +58,9 @@ public class UpdateUserReq { public void setName(String name) { this.name = name; } + public String getUsername() { return username; } + + public void setUsername(String username) { this.username = username; } public LocalDate getExpiryDate() { return expiryDate; From 620daab74f7137d3fbf1c5f82011559c77c2ec9a Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Tue, 28 May 2024 14:01:41 +0800 Subject: [PATCH 20/21] update --- .../tsms/modules/data/web/models/NewStaffRequest.kt | 10 +++++----- .../ffii/tsms/modules/report/service/ReportService.kt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/web/models/NewStaffRequest.kt b/src/main/java/com/ffii/tsms/modules/data/web/models/NewStaffRequest.kt index c5316f5..9ef5d8c 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/models/NewStaffRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/models/NewStaffRequest.kt @@ -13,21 +13,21 @@ data class NewStaffRequest( val companyId: Long, @field:NotNull(message = "Staff salaryId cannot be empty") val salaryId: Long, - @field:NotNull(message = "joinDate cannot be empty") +// @field:NotNull(message = "joinDate cannot be empty") val joinDate: LocalDate, @field:NotNull(message = "Staff currentPositionId cannot be empty") val currentPositionId: Long, - @field:NotNull(message = "Staff joinPositionId cannot be empty") +// @field:NotNull(message = "Staff joinPositionId cannot be empty") val joinPositionId: Long, - @field:NotNull(message = "Staff departmentId cannot be empty") +// @field:NotNull(message = "Staff departmentId cannot be empty") val departmentId: Long, @field:NotBlank(message = "Staff phone1 cannot be empty") val phone1: String, @field:NotBlank(message = "Staff email cannot be empty") val email: String, - @field:NotBlank(message = "Staff emergContactName cannot be empty") +// @field:NotBlank(message = "Staff emergContactName cannot be empty") val emergContactName: String, - @field:NotBlank(message = "Staff emergContactPhone cannot be empty") +// @field:NotBlank(message = "Staff emergContactPhone cannot be empty") val emergContactPhone: String, @field:NotBlank(message = "Staff employType cannot be empty") val employType: String, 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 53d38ed..b0bafe0 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 @@ -1510,9 +1510,9 @@ open class ReportService( + " left join project_task pt on t.projectTaskId = pt.id" + " left join project p on p.id = pt.project_id" + " where t.staffId = :staffId" + + " and t.recordDate BETWEEN :startDate and :endDate" + " group by p.id, t.recordDate" + " order by p.id, t.recordDate" - + " and t.recordDate BETWEEN :startDate and :endDate" ) return jdbcDao.queryForList(sql.toString(), args) } From c4817fea075e225fd06677a865356bbcc8b92a2b Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 28 May 2024 14:07:19 +0800 Subject: [PATCH 21/21] update report --- .../modules/report/service/ReportService.kt | 4 ++-- .../report/AR01_Late Start Report v01.xlsx | Bin 12958 -> 12474 bytes .../report/AR02_Delay Report v02.xlsx | Bin 12399 -> 12388 bytes .../report/AR03_Resource Overconsumption.xlsx | Bin 13285 -> 13170 bytes .../AR04_Cost and Expense Report v02.xlsx | Bin 12774 -> 12689 bytes .../AR05_Project Completion Report.xlsx | Bin 12475 -> 12370 bytes ...h Outstanding Accounts Receivable v02.xlsx | Bin 12665 -> 12570 bytes .../report/AR07_Project P&L Report v02.xlsx | Bin 12779 -> 12688 bytes ...08_Monthly Work Hours Analysis Report.xlsx | Bin 14869 -> 14716 bytes .../report/EX01_Financial Status Report.xlsx | Bin 13083 -> 12993 bytes .../report/EX02_Project Cash Flow Report.xlsx | Bin 13289 -> 13291 bytes 11 files changed, 2 insertions(+), 2 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 b0bafe0..2964dce 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 @@ -759,7 +759,7 @@ open class ReportService( if (groupedInvoices.containsKey(result)) { groupedInvoices[result]!!.forEachIndexed { _, invoice -> - sheet.getRow(rowIndex++).apply { + sheet.createRow(rowIndex++).apply { createCell(0).apply { setCellValue(result) } @@ -797,7 +797,7 @@ open class ReportService( } if (groupedTimesheets.containsKey(result)) { - sheet.getRow(rowIndex++).apply { + sheet.createRow(rowIndex++).apply { createCell(0).apply { setCellValue(result.format(dateFormatter)) diff --git a/src/main/resources/templates/report/AR01_Late Start Report v01.xlsx b/src/main/resources/templates/report/AR01_Late Start Report v01.xlsx index 24da97fdaf261701fcfd8b1110216245b4c543b3..36d38cf74dff6cc3d06476080b482e6eb7c71d7c 100644 GIT binary patch delta 5077 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 delta 5562 zcmZu#1yodB+Z|%)7zvT?7KTm*i2>=7kcNSw89F3JKw5GL=|+?W>F!QJ9J)hMq@{m+ zzyE*V@B7yO+;!Kv_nx!Yz3aKp*=L{KY>{nEp{as_Nd~|I-~a#sMnGk_IY|f_006C~ zU_nQyz`OYf+BN18C2m>sokX97w8ZvZ7vs&P8H9>sY>^xyW!G0tYk78-e3+>UYU>AU zU+Lb?7pv=mCdD$M1HX{se(kn{ju`u^uS_c@FmRUY{&>7;Nywqlvyjo{@dJ%Cw*Fmo z)S$FdBm@}KJ?dNKFW{Z0)!UJmWJ->&8{CRGIHj{%dmL1hBtgJW)7ojwDC5q1@OzQQt0h*?cjnpF;0V#telw(t7 z93Y&n*zB3?YDE^7eCRXq<<5Lp{no24=4y2ZM?p?NFKAh}XW80l1WfTzaOWY)HQpVe zL#mS`Jp485C-e28z;NS9<>SI*iXe?$h6?`pL%*gsIPSq9I};33$sZ3+$lQz2cX3@$ zQ}ii*kzTzanmF!ynUrx#pJvM!S`Tn;edJ4!E$Ky*sYr*1Hm-40fzh%%BM8D2s5ztJ1_bHuMO2lWk0YzdkzDZN3^aImRnelK_FxV zeawk5xh$!`qK{k&&;_ZFIHhAmjG!I!U#IETLffeN{JL+?FnJ=;FKf(W_NPAX(==eb^HnD1?VjIs_^Y1g9w z#7IoDpFycDu4C}e-b;xQ4?%*Z_-T8Xxz{?7$%x(~v#zR}CEG`Zv-Yfjoi@@jNjLY= z(h*_qJrj155TP+e?Y-y8#abjE5;Hl|HAWc;E@Io&U+Y>fyfM=2{wU}{J0XwIR}^2t zd-QDoeNlcsTa-ep9zz|*=|Qq?G%Y!eyyNH|+=z{1lju3lXi$n3ns0oAJbAxfQWlcM zD!CSxn7W|1m0My7)t#WIVF(taqd{O)55e6QDsb(+RJmD7Q^ zz1$*>d7BcALgzFTj#ngWn-WHdtL5M2Iwsl;FuA~NT+VO32ZEQFc4z%c-=H1L9TLjB z$Ye8QLq87^?*$T~9qWVX=zK)_^R^VGCfyFdHuT3H7@l#QRnbH2tcaz9$h(OJ)ayTr z*~&xJRVgrCYlN7u0)Y!%DO;829jh$3@0F+w^A4I*zC&D=IHDfYOO7Krf|_Q|rR`Ka zg>}Iot;r+MSv*Igisw0#{{qewwn>Ql8LS8?Po{s3Ek$=XEB zJ?)-aWZRAo@OiuoUHYnrr?GJ2z{7qL_QJ?F$asO;GlS=IQ%npsPD{0>JjbL}PiSyr zW$C9g(IoK)yOMpLu?s$kGd@brS|f{Hb$Bn-Q4-n~@^d!kg%dFs>={MNnTKs0qx?{3 z?c&teYK5Pj)l8yZB=C2=O1pazdtjwsh?&nKX~DOoMK5J5N{SdzSW=Ge%|wxyPM%n3 zD-Bk1aI$%f+}&ca*`u0rkSyHSRpM8eSS$%RfVFhYtfMwPM$^wAoun>QAeZ}@B#}o3 z9V`y{-FY3<0H5Z2H!yz?{`s!n741vv$>`YLQW|wZT_GG(#haj<26j}0yGzlyaZVI)auaH2tV~VMC4-et4iG}W6JFmrY#^w zclCT#>hU6LsQPt!Nv+P~i$>%^qFrlY)Aal|Ga63ouvtE#X(;G|%766o3F+}sC8a^{ z(>p9=9tIh1jYb^K2qpk9j_e~~K)f)4&-0N5wGggj~>f=I}4_WM#`%yK7SP zYVid^75K)H>9F1Br^Z@UFg-MZrpCH22viAS7U8R=N-ckp_&8}R|&Bc?f58NNWfuN|4pQ* zSLJKHy#jv0(Ukop#KTrKdm~iH+6SPVW7kHfaK52XyxFs!5*{kc@YuyoVSlyc@ifkF zOynms+A@wG&{wiDXI&|YqIhU5vBMLw$kDRuH6v(+Zr%ei92)J@7`#*~CB);gZbQo% zB24sq?rb6R-Hcvb-R^S6&Q^d@ml`B05}DH(+r$3gBv7>*F*G7%RC!Ly?*NUtrT_r$ z?(Wy*ySt>MCFkI0pir~WeT>0M4p?d-UVb|;kHr+F5K#wTNh>q)f>C`mPS_9~5EN|L zU0Xbd&c-@@sH8XRnk_K^_SpDQt~!w_a*4$$uud`Y3>F0YW!5ci-?V%Sujl|^zUA@^ zr-R#~V`(CYqzM>ShQ(&r%4aOj&Ff=v>4()hk~Qt~_Cu#}wiuEMG}YTly7?C6NkkG* z;}62dyBxngvB;9__jFD!GE17uA7)CsEdS6n>4QF$)1F9gF-UBxbNg<~`|Suk8Fl^e z@W?&y5HnHQk5#(+$^8;4*}v5=>=+pBbMY4hL0rG7FqTdKCPvwHte53(!w`01c{2g& z6)4bK7Qq)GP#U@5Fzrty zOYw%B2)bNAU^xzTa&%qyVveZHyLWFs?_6R3omj{CDfJ#c0tkZj2wU@umuzhns1O^m z*}K9ibWaItpnGBTFHz*~;p=Ga{znqc7>wLYBC;ilU1^+o?pFp!U&A(KXds<{&ocb3 zLC$uOHIaGGgYrpzqtIX|UH!h?yGdTy6hyk@T|Ht-Ww<6eJWEXdwHIl0<%s)4+j+d| z?;73`*GLFNEL!!vm!EJ{8-`HURT|M_EXOJehByvo%k2OqIQlj z_vfEXC`G}0Xy@gqi;PsvFI-a=JH1*R+UM32Qv8ipRr0%SVtXwPp(wm7e#r=1*>?k;bRQ_TV|cjj_t z7iq}fRHXB3c<#nmvLh%k(Q_r7fW<*GDAfsR`&y46dEm>r*9WILRGmp=jRMQ7+#5V# zP|IG`_I5g4uME7r7}3F?%>$r_xDBf5hl(}?kb$_a=eNg%$hGbgkOr$r1lLryDwWPV&vq9GRfb` zOj$50eY9c@WkWdQwT%a*^A2LUVV5sp;1Pv8%v`iJwq-TWm3HyJNbxAbteWb;Zmh-s z-qj-Pms5|qj9IlB+=^`;rIj4E?HuMU^Nm)yQ&7h3#tBR6U9sqhWf&y?04FK~DtAi2 zkS!gok?_Kg%TGTTu%H&ORM>XtlMi&KPJ+69Z6qnlTt^sEC!v?K?>ukR|;>yV!YwrMlVuD)$@`b@uP&rHp#y`5L;5H}|Dl#3pf zzWe?AJg%dz5wPm0RKnuuZn)gm2`EA}YDZ5ccJ{4>2d&WeSBqAwhiuLqUEOx3x4J+} z53mkjP9S31j;(&lnCXk?q1o!e!OXE#$=_y+Dmkfl0grpc4YkFW2o$!@uBbB;tkH76 z)V8yx8rzd5G~EQ2|Fo`XzX%R)tytAU>*&`80T5EY{BI{nC#b}bL0ji8=7b?ij=)646(sucWPHbjeVj5)w>QWKeB~u&{p>r{d zhJ>nyJM2ey_!J9)x=DLjuX3l za(!y&e#)EtV~J^yQ30@s=V$O zv3y#J1ojE0W_^r&5h=3y%GPsfhNk;=VUU;tp6$zy@Gj%Uo&T8n~~iN~Bqe$krqStSb6 zjjj^7Cbs4XL~@eZ%<>adt6A?R@qWk#U&4|h>v_)M#dN;D=b_>~^D_lMMV_WKS-x2! zh-n_YC>qsrjgCQ%=Oj#0r{J!;AR+3pa| z!_!AyD_=~+r6U`(>HAj5``4>wurOJEtU?2y1vWj|qs!wDCg+5onWG*`%1q14Xqr$v z^kJ*2t42xH{wnNuuX&<2Rgf%PK+VcvZ@6DqW?#BSvDVl?l8ATR29u0vW%^-8y0iI$ zVCP~rSv13#rRs*_ue$K0vhP;AuM4AE2nQ3|pTgiH>stYK$PM{r6ke&=NorC`IdT|i z`Qr~Q={X|ft-P+zikI{x$r!;+JDc7UZ7e1<-?)CGLseJ><(Y%g-|P$MvAAjLR5YZc{EO&vOQchjoIY_gemb6jdlJ$N@p$+-Vx7JcLuWyn-eb7) zW$Mq88`Yggx%yT1YHxZ+6m>>%WV#kb&(8bb0d24L6bMyNKcg^)b-GBMUyEBGwE>aq zGI=JZePu>hPvOcS?*V@xmyf^lAzZN!*}Te56Va)tq}~M{Uf8|OGsu6!=0TEZ_nZxw z#qN>PyIJ7mf7%}7#Wk~Yc1N<|`n;bpiz12TRuzV^4vhD1s_c-vCHyP3{rPhjh4-mt zs2%2FL_>5r&+vi54xYD$Z28S8$&ee0SA66t31`wtj?=T zuJ1hlqUqIzEU!*P{G=?*YR$c`A;OhcUc|`OGaHexuVg1{FRYy*!Xy+U>_RyjfKS(F zr3~WX2C{l&z*32$lk@#(kpsggQ4h0S;AC%;8{Nh?ZG^mGcTJ?C4O?& zsq)T6_Gmxd=lo7v!?xJ?0(voCl!ZQ2gWQt=wkl3KdNn=1*jRFwsKDZ{>f%==f+u3+ z2Ofx;d1i}K2dLl-FHH5@TpXNOy5sux(gbgM0Hvv}|GqAueApr=YNK!2e3N1Aor}|y z%5Ku-5mx%Qw0s32k`&chR9SNZM0V8Hy%3b|Mso!q5m6eNM>Fp>VLMr007nh zb^X;2AkBGsX#U>y-RrwY|ItrNh#V%MLJISOke_*}(CJ8!2fVD*|1Amsfvh9JU2fWC43WyJj_nIZXk$!Pv& ziu>0*_y=%*)y0X_;bWHnyWa0Ty?gcgZvlA>09e7Hnr?6xcd&(v%O7&oQ~_fB8A5n} M*xe)O!GG@l2LR$M&Hw-a diff --git a/src/main/resources/templates/report/AR02_Delay Report v02.xlsx b/src/main/resources/templates/report/AR02_Delay Report v02.xlsx index 1d844349d56aafd889458b6a688a8caec9490764..d07a414637b4051f5a1c9282ddbdbf8bf4b085d5 100644 GIT binary patch delta 2466 zcmV;T30?N@VB}!1+6Dw!DY`6^-Uc0iYj2}C6#c%^{0GE$3g(dnR60rmXeu?MD$TUJ zQl!c;PO%D@wM~<1H2;0BA&+*lT4g(n5*t4_KKI^puP;AutI~L(LNZ>*Cc1D;gVu!S ztbU5kZ;Q+tnTAxj&T+|W8k?_FnxC&e{c*YB;(5jSvjG6LjLkx+CbDfw3R+=*dBGc6 z!^)bA3M&|jCtEgx;#?M#s;abI#~IiaX0_QPMB*F_zFspz6HeNS)~W{)w8RSN%Yrp> zk5-X$;3_Ph+r}ciY5-!zN~T^rP}8VLbp2FwfmbD%y+whs74RFtA34yt`;%blUV(>v$g9Fy!2!@>BMnfR2PAd^O z?}KQ;ftzbyU9MS4?|bPQxM{xPN=K?RjS@?h=1kE%Hit0e8+x1ot3=yOT2{iUI}AKz z+E)iT-wGH&?8l{|qQ;6&d96TdJLFhP{izq#E&>ZgwAbOS5(d0qCpQp_JNyEXf%xaU#JvXBBk!>$B0`Vf z{V?+q&+{#BG;l0G9r{+7j6y3#;V5%bH$ma}2N)v;5#hL1#V+U?IyQZX@X68)Z}%(6 ziCUH)X?%BfkEQ!xn*G`jZLltH_l$1jQ673|Y#&&i^G$zjTJ9)x!K~NAMCieA(=9)+ zTos_O(D4s*UnqMjfHMk)`a4Jj%`Gjm^l{=k??i9!5eP2HtuS=gg(gIf8)_HOt-6t_>p_tWm?Ga9 z51r7roYa2{EFTMEmPTvn;Ss3jv7$m(-lD zCs=~IdV_%PTe6d2G;utj=w~Rid=xs?WHRurAj!O7h>~dWj}EoFfa|uCslAuU)lai=3P=JDT-@x>YXkrQR15$B z7yy%AE*g{J3>G7Ut-F}2wcW0beQEcWO3m$eP_V^Vr-eujJ-I-oXO&Dhg)Ppg&b`Xb77GPq8vb*Ld%&`e6rVX;b3TGxog%Bp@u)RIji9K z=YJH&P?pVTS-h17tPITVn@O#LLE_n}Ka{D@m;3Qvi6hsSR1UU&}1vv|H5#)+V z;@S?q=d|~wM~{0|;1*NXA}FN_9e)DI1JK+s99RTMDpwev$dbxqnyO`?z(An@pitZ} z99RTMsB$}$$23&KLV%i_O>mTbMvyc$l3l8tnj`S1(002q?000yKlOQh^e}q!OP6HtnyqowB z1kaY$7-Jfi9=6q+X|(+SvOIS&@L1ukY5jV!n`*LM^#(J;n_)mMcHXgfh{*;l`PHo8 z46qKyVpH<%=Kk`IvqTz=b^)Q}dq{kt&d+2@#PV58C2xtkxe%#t!Rs^&9bjf&LiC#Q zHZ~%45wuAy0C^{_e~aQ)cx{ohI`l}{_lC0`?OPAkH=ty)N|LJj7y*erlWsr?Qc`>o z1G(CDP!kxoj^Q=0*a{6>YB~`f`8*}kq@r>*+l&o1p#2y-&U)1TSDz}F6YgR>Xt115 zd7gkz(1o1*0T805{91=gOvqB`V-0Le*4d9ixrU73pZy)$5dF8kB{;ojLzJJ?BL4;T z36r4%6tlV=Itm3Q*xMIPv-u|`0e?YD!!Q_z?}GoKZ0p1yaU8|?r|ZBd`{#i^nKYGjZSCK5 z)}68iAHuPHvT`goO%pYvx_*u_aARgL$-5Oa2g+0m?|3^+et(PB=AOf#L2l-3#Hd{Xy8b-vRZHq=?+Ut)6SabdD7$bynNoy8O41(5gN*Iy$429u!!6q7434zq_UHUk7% zDY`6^!7YIaT-@x>YXkrQRFg|Cxe4#mj`S1(002sp4KGsxqLX1SPy(bLlgb_`liV*5 g1t!?r7fqA#FBJjYlLs(90br9xFe3&iEdT%j06(dnX8-^I delta 2511 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 Z00000=9By{J^^c!J1`>#Gc5oB0003Yo(li~ diff --git a/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx b/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx index ff196684bcd0749acb7fbb232bfe5014990cfa71..88dd3c84e085a54dff23962317db12778795a695 100644 GIT binary patch delta 4927 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 H{&V?1vJn(L delta 5032 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 G6Z{`c;xPmO diff --git a/src/main/resources/templates/report/AR04_Cost and Expense Report v02.xlsx b/src/main/resources/templates/report/AR04_Cost and Expense Report v02.xlsx index 889948be48004e40b1f58aaa48f03fb2695595d4..a8aecebf09dce1486c46280b9b5d99d028819f57 100644 GIT binary patch delta 4540 zcmZ8lXE+?(wjSN+J;I1OT7=OFi5f(&qekyW7Y1WS$w!DjO4LN}Wz^^`Y77xVki_VM z5YhX|ch0@vbI;vB)?UwE&t5;)v-W!5{cbpP;U979#O`JzOH z+TqeBi}EL3Xcf%Gzi=R~EDL%mON^UsNF>)lTsDEcmSz+fi0RIb2agG*X z(e)7~Q+71TZgEN&C!4m|r$r#Ch!twaBN%q!zn#(Bl%&(Y|dVMf!+rs#~QZ86#n_z6Xzv zZ>~-6L=b&t4}jlqBi^4opnShV)uV`V|57&JQ7?jzE?E7kNNpz_I(zMox2W2 zP-=X_$GinKI=&O;VD2@U18PWg(pCI2lL4$kl((DR~ zO9}DPtr%_A5#`ojP5BOYI;VSN!=2uZd$pCe3u_uV&Ks>$HlU@I10TIaFy@%aedBY;pFy$XVNc|k;4 zBTahN4Q35SkNAZb<`mBOOwEWDx~{QU=RW+-JlObNE6FkaJcpX8B6dDZryj;BYhoYd zVAK2qc>-zs!FMthnu)&Qf+Ckt*WRrAy%(vuZL(bo{;Rt?2w4UHeVqm{0--Va4Kz_C zBacG8Gkbx0fL4g%9!CX~&l6cS=V@b=cuf`X&;b z;Ch^9?8Z$}^(QX{1-XBvS26K?L z5UhZuMmv4AsQOELNzu19-oaI&*>C4?ga#rVfvP30c$gJQ={l-7c_qHn95um)%7Q%t zUD9JJvd6TD)yD~zJtI6aCWarH4=@2|AMSL^s9n?bF&A7R@Y zYf9MC2lMsn&iRWZ>osk`snqp=#<<9N3Fb~gDgwJ4oIs246i9E+XlEIO^l6!Mnat;` zQiqDWGi&sZTymGdi;JC?b~{nC^-TRb_2S*BzXzk2q8wbE+8{aVw=80LZkX<_+QX)x zJuXEcqb2&8r^mmWg~{d{j=%%tgueCNpyitXV? zc(O!}q$AA9;^5fYDaNrb<+(_T8gt#9W&I=*LjQF$lzwN`9U>7elwu4)2q8ga3P7ZZ zN)75l>B;|Mf}_bqV~Y_+ZmI+Wed1=Oi6?|(2EAY^oPlJR8fE)B-0c^cVs!`yGw_%F zon`A0$MI*UIj5!J&GZ)JYqKn*5Ai!I9A(niFg`|Er;>8X-7kbaY7FRBakt|Rt{P^3 z9$TJ#`+@$;tYNOCOETu%TBGVXRpvm3FOI8;_z%Lq8?>BY6r|@TTJHcFduS?2Pz8lL zS%gFYfB_uoO5M(m{5UZ$ zHl!{dpM@{WV=>;$ueI?qg*FywNoHM{s#3AqP?!De1$N#Zg@1{`M|@XP^G%j(J6t);ICost$D(!5W6-y zr&nnaa&tUSy4w`?$`fk@KM7pJD4ZX*9by_zN5AJ|yqWoe_7B#JieA;Y<^)WL5OUN4Zmzw&o=d04(qK7D!H$cea}7yPBmLKD6rhhM z>C*D&Ow3g4sw7v&GI=`CX z)9ID|*Qw9Sc_4fA`-6vrLiaWmw^|BVM75`Bn8vox;j%IOdIdgXu8fzpUmlgXx>PL0Tr7%DEYQy!#e%gATiI5D1YH7I6K4t^Hh#&CF-O_YdL{w0odx28TQc}sWPs1v zH4*Ag;{2#7r;XHrcybD-_x^;c0&9ME@7vinkPaWvZl0_=CM@CQ4;DjTzdiodyD1r` z5Q%{%(N$T6Q{_-)u~I)@)Hg46i!{=JY2S^7E5+HyIOgFR>+sSBIC(@dvH29<-_XF0;kuX1*US}vVAU15^o%C4-!CF z?Uu)M-Wzm!98Oh46nppS9Cqs}A`^wUJZ-dL;1Atl5OnoWvPGT`k%PQ(ArTwr5oNN@ z%&yp>-q|Z>k${?r9}-u6Mxrk=&{JfCRhy`#u8@=K4+?Kg5u8DD2+t~7sSqZJpOBIN zMPdz6^t!8;ZH$fx=%PimGi^WT%?*Bc9V*P=V7*tX)49jZ96347(oOH0I0bX>t9S88 zh+bu#sHdJx#91~YAiM9%hh(EX2F|dS6d@i)QJll@NG4G^>z>{6&xbB>Z6=_tIqGMK zhORiTS-{NpIo=zXj<09mEZz9>*|m0>nmFpC zv{2D)Q{Zh+-%lGvZFt2{fJHFH66u!aJmXnW+^m5IInah;9MF;YtyR8gGdr4Tfmmin zMW^P+d@WOZspPL*+pTCX%8pQM9hflMEBkjS0m*&~vz3ZNYDTxS2LL#stJ%0wcP`kYj|*c0tI9Q=8ZOLGCu>$58_NrgzOLr! z@D1WRUXnXe;o0&1-YI>!Av)z~Y5FU6!o$Pf_b~{lh@Q8~;bIcm>F7%U=#8yaHV``; z{AI13iWK>sAKpAPT*%`-WOz~i-r~H#^P|ruw%vj@vo^Wgx7^Q$%kbIgtEnvu)T|~e zUX~~2!T0-kHB;;lN=a~rJSlTeRY&7u+y{`hvBD}UmAx8^2I$4@qs7C7>_3mS* zJ96gAw}k^i=f&H&S_hHl;E!@Zsfo%;;ie$bn@Kf5Z9OItcaQSnlPfBS=?gs1JM^~% zU%g}%l0X-_t>y)MW#au;dd#UyJmu=hh#vm*`GVQf2wKIXd}RdhkB+rz`+o` zuX9CBdHSH~(kHb=D|bF!OS^gsu8xCox1<3YcX~1dEq@js(0$L0iJ||rjuDI!fk0;k zReV+T0daH6mCvHBUp;@P81Y;i2%RMpn~8k#;p&8UJ^S}UZ)1fdGhZKZezDN{LzU1? ziNPgjMi|72ng*l- zWaM}81r45+?erR-OcI2NxkEoSFZBGVmUAXYlEs`%jTL;6(3#N=q1ErDQZw{f6aMwc<568O#uvzofUdUjL~rU*qb1PCS&3_acNMdY|Bgo$wp0>@x$3i0VQO>7#l( zC3K*RPN_pd3(w2yl-;?cmZ=%@poIh*`0*C_7+c`+g!*n6MTwO#nE=N-v-5h9_0_$d zK^K8!SUHT9Ani9PzxVhsbqLxaQv05My=t$al&V_^Lp$-qSL9wKdo!Xz88X;V#vRmx z+`CtMB~gmo4g9r?AW~(b&rco#$DaCrp1zHPPEjJxn&fzir7omOX63Z5v<|;@fJA3` zxSXaBRnXiEqO z=%0~<4*)RyZ|L8Uo; oPQX8J@lP7s|4M_8MvC&V{?ku?6d9WTipQY4M7fDgg#SML7uMWzjsO4v delta 4615 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 I;eQqX3re4rTmS$7 diff --git a/src/main/resources/templates/report/AR05_Project Completion Report.xlsx b/src/main/resources/templates/report/AR05_Project Completion Report.xlsx index 126f10c084b8f3e52d2ffb21730aef5fef89e34f..d9187b622fdff9b34cd15a661f6be51609bb5024 100644 GIT binary patch delta 4225 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 delta 4360 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 NRzg;o1_S@w^gnQw`+fib diff --git a/src/main/resources/templates/report/AR06_Project Completion Report with Outstanding Accounts Receivable v02.xlsx b/src/main/resources/templates/report/AR06_Project Completion Report with Outstanding Accounts Receivable v02.xlsx index db5ea39aafdf68860d7c3975b35b780ed2f3f64a..700dc590c09515ef027acd8132b7309a0eaab360 100644 GIT binary patch delta 4438 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 b^RWCcvi|-`p#LWiK|c`ZCi*V?Pw>A0jI21P delta 4504 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 BN5}vG diff --git a/src/main/resources/templates/report/AR07_Project P&L Report v02.xlsx b/src/main/resources/templates/report/AR07_Project P&L Report v02.xlsx index 7febce934dfae57a46ec459c555f0af86810111e..111482b2c004552590c5eb303cd4f4e84a504884 100644 GIT binary patch delta 4522 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 KCo_@w`|v-EMN#Si delta 4639 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 T|2YPMMp2GpJfv?#{@(l-_qCUd diff --git a/src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx b/src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx index 08129d72d57569f0e1b8ae48c2173e84325e1146..c471a6a4b6f33e637715ceb29ed99533993071f6 100644 GIT binary patch delta 6545 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 delta 6669 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 delta 4666 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 Opn4^^h^j^ZLH`Bb@~{8^