diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java index 46eb775..3b0b4d1 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java @@ -5,6 +5,7 @@ import com.ffii.core.support.AbstractRepository; import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo; import org.springframework.data.repository.query.Param; +import java.io.Serializable; import java.util.List; import java.util.Map; import java.util.Optional; @@ -13,7 +14,7 @@ public interface StaffRepository extends AbstractRepository { List findStaffSearchInfoByAndDeletedFalse(); List findStaffSearchInfoByAndDeletedFalseAndTeamIdIsNull(); - List findAllStaffSearchInfoByIdIn(List ids); + List findAllStaffSearchInfoByIdIn(List ids); Optional findByStaffId(@Param("staffId") String staffId); Optional findStaffSearchInfoById(@Param("id") Long id); diff --git a/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt b/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt index 9fd341f..14dccc6 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt @@ -35,7 +35,7 @@ open class StaffsService( ) : AbstractBaseEntityService(jdbcDao, staffRepository) { open fun getTeamLeads(): List { // TODO: Replace by actual logic - val teamIds = teamRepository.findAll().map { team -> team.staff.id } + val teamIds = teamRepository.findAll().filter { team -> team.deleted == false }.map { team -> team.staff.id } return staffRepository.findAllStaffSearchInfoByIdIn(teamIds) } 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 373e7ac..c249c81 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 @@ -7,6 +7,7 @@ import com.ffii.tsms.modules.project.entity.Invoice import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.timesheet.entity.Leave import com.ffii.tsms.modules.timesheet.entity.Timesheet +import com.ffii.tsms.modules.timesheet.web.models.LeaveEntry import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory import org.apache.poi.ss.usermodel.* @@ -20,13 +21,14 @@ import java.io.IOException import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.* +import kotlin.time.times data class DayInfo(val date: String?, val weekday: String?) + @Service -open class ReportService ( +open class ReportService( private val jdbcDao: JdbcDao, - ) -{ +) { private val logger: Log = LogFactory.getLog(javaClass) private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd") private val FORMATTED_TODAY = LocalDate.now().format(DATE_FORMATTER) @@ -48,8 +50,13 @@ open class ReportService ( return outputStream.toByteArray() } + @Throws(IOException::class) - fun generateProjectCashFlowReport(project: Project, invoices: List, timesheets: List): ByteArray { + fun generateProjectCashFlowReport( + project: Project, + invoices: List, + timesheets: List + ): ByteArray { // Generate the Excel report with query results val workbook: Workbook = createProjectCashFlowReport(project, invoices, timesheets, PROJECT_CASH_FLOW_REPORT) @@ -62,9 +69,22 @@ open class ReportService ( } @Throws(IOException::class) - fun generateStaffMonthlyWorkHourAnalysisReport(month: LocalDate, staff: Staff, timesheets: List, leaves: List, projectList: List): ByteArray { + fun generateStaffMonthlyWorkHourAnalysisReport( + month: LocalDate, + staff: Staff, + timesheets: List, + leaves: List, + projectList: List + ): ByteArray { // Generate the Excel report with query results - val workbook: Workbook = createStaffMonthlyWorkHourAnalysisReport(month, staff, timesheets, leaves, projectList, MONTHLY_WORK_HOURS_ANALYSIS_REPORT) + val workbook: Workbook = createStaffMonthlyWorkHourAnalysisReport( + month, + staff, + timesheets, + leaves, + projectList, + MONTHLY_WORK_HOURS_ANALYSIS_REPORT + ) // Write the workbook to a ByteArrayOutputStream val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() @@ -89,7 +109,7 @@ open class ReportService ( @Throws(IOException::class) fun generateLateStartReport(project: Project): ByteArray { - val workbook: Workbook = createLateStartReport(project,LATE_START_REPORT) + val workbook: Workbook = createLateStartReport(project, LATE_START_REPORT) val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) workbook.close() @@ -102,7 +122,7 @@ open class ReportService ( private fun createFinancialStatusReport( templatePath: String, projectId: Long, - ) : Workbook { + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -125,42 +145,42 @@ open class ReportService ( val sheet: Sheet = workbook.getSheetAt(0) // accounting style + comma style - val accountingStyle = workbook.createDataFormat() .getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") + val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field var columnIndex = 2 - sheet.getRow(rowIndex).getCell(columnIndex).apply { + sheet.getRow(rowIndex).createCell(columnIndex).apply { setCellValue(FORMATTED_TODAY) } rowIndex = 2 - sheet.getRow(rowIndex).getCell(columnIndex).apply { + sheet.getRow(rowIndex).createCell(columnIndex).apply { setCellValue(project.code) } rowIndex = 3 - sheet.getRow(rowIndex).getCell(columnIndex).apply { + sheet.getRow(rowIndex).createCell(columnIndex).apply { setCellValue(project.name) } rowIndex = 4 - sheet.getRow(rowIndex).getCell(columnIndex).apply { + sheet.getRow(rowIndex).createCell(columnIndex).apply { setCellValue(if (project.customer?.name == null) "N/A" else project.customer?.name) } rowIndex = 5 - sheet.getRow(rowIndex).getCell(columnIndex).apply { + sheet.getRow(rowIndex).createCell(columnIndex).apply { setCellValue(if (project.teamLead?.team?.name == null) "N/A" else project.teamLead?.team?.name) } rowIndex = 9 sheet.getRow(rowIndex).apply { - getCell(1).apply { + createCell(1).apply { setCellValue(project.expectedTotalFee!!) cellStyle.dataFormat = accountingStyle } - getCell(2).apply { + createCell(2).apply { setCellValue(project.expectedTotalFee!! / 0.8) cellStyle.dataFormat = accountingStyle } @@ -168,15 +188,17 @@ open class ReportService ( rowIndex = 10 val actualIncome = invoices.sumOf { invoice -> invoice.paidAmount!! } - val actualExpenditure = timesheets.sumOf { timesheet -> timesheet.staff!!.salary.hourlyRate.toDouble() * ((timesheet.normalConsumed ?: 0.0) + (timesheet.otConsumed ?: 0.0)) } + val actualExpenditure = timesheets.sumOf { timesheet -> + timesheet.staff!!.salary.hourlyRate.toDouble() * ((timesheet.normalConsumed ?: 0.0) + (timesheet.otConsumed + ?: 0.0)) + } sheet.getRow(rowIndex).apply { - getCell(1).apply { - // TODO: Replace by actual expenditure + createCell(1).apply { setCellValue(actualExpenditure) cellStyle.dataFormat = accountingStyle } - getCell(2).apply { + createCell(2).apply { setCellValue(actualIncome.toDouble()) cellStyle.dataFormat = accountingStyle } @@ -184,102 +206,137 @@ open class ReportService ( rowIndex = 11 sheet.getRow(rowIndex).apply { - getCell(1).apply { - // TODO: Replace by actual expenditure + createCell(1).apply { cellFormula = "B10-B11" cellStyle.dataFormat = accountingStyle } - getCell(2).apply { + createCell(2).apply { cellFormula = "C10-C11" cellStyle.dataFormat = accountingStyle } } - // TODO: Add expenditure rowIndex = 15 - val combinedResults = (invoices.map { it.receiptDate } + timesheets.map { it.recordDate }).filterNotNull().sortedBy { it } - logger.info("combinedResults-------------- $combinedResults") - invoices.forEach{ - logger.info("Invoice--------- $it. \n") - } + + val dateFormatter = DateTimeFormatter.ofPattern("MMM YYYY") - combinedResults.forEach { result: LocalDate -> - val invoice = invoices.find { invoice: Invoice -> invoice.receiptDate == result } - val timesheet = timesheets.find { timesheet: Timesheet -> timesheet.recordDate == result} - - logger.info("result--------------: $result") - if (invoice != null) { - sheet.getRow(rowIndex++)?.apply { - - logger.info("INVOICE NOT NULL--------------:") - logger.info("getCell(0)--------------: ${getCell(0)}") - logger.info("dateFormatter--------------: $dateFormatter") - logger.info("result.format--------------: ${result.format(dateFormatter)}") - getCell(0)?.apply { - setCellValue(result.format(dateFormatter).toString()) + val combinedResults = + (invoices.map { it.receiptDate } + timesheets.map { it.recordDate }).filterNotNull().sortedBy { it } + .map { it.format(dateFormatter) }.distinct() + val groupedTimesheets = timesheets.sortedBy { it.recordDate } + .groupBy { timesheetEntry -> timesheetEntry.recordDate?.format(dateFormatter).toString() } + .mapValues { (_, timesheetEntries) -> + timesheetEntries.map { timesheet -> + if (timesheet.normalConsumed != null) { + 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()) + } else { + 0.0 } + } + } - getCell(1)?.apply { - setCellValue(0.0) - cellStyle.dataFormat = accountingStyle - } + val groupedInvoices = invoices.sortedBy { it.receiptDate }.filter { it.paidAmount != null } + .groupBy { invoiceEntry -> invoiceEntry.invoiceDate?.format(dateFormatter).toString() } + .mapValues { (_, invoiceEntries) -> + invoiceEntries.map { invoice -> + mapOf( + "paidAmount" to invoice.paidAmount?.toDouble(), + "description" to invoice.milestonePayment?.description + ) + } + } - logger.info("invoice.paidAmount------------: ${invoice.paidAmount}") - logger.info("invoice.paidAmount------------: ${invoice.paidAmount!!.toDouble()}") - getCell(2)?.apply { - setCellValue(invoice.paidAmount!!.toDouble()) - cellStyle.dataFormat = accountingStyle - } + groupedTimesheets.entries.forEach { (key, value) -> + logger.info("key: $key") + logger.info("value: " + value.sumOf { it }) + } - getCell(3)?.apply { - val lastRow = rowIndex - 1 - if (lastRow == 15) { - cellFormula = "C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString()) - } else { - cellFormula = "IF(B{currentRow}>0,D{lastRow}-B{currentRow},D{lastRow}+C{currentRow})".replace("{currentRow}", rowIndex.toString()).replace("{lastRow}", lastRow.toString()) + groupedInvoices.entries.forEach { (key, value) -> + logger.info("key: $key") + logger.info("value: " + value) + groupedInvoices[key]!!.forEachIndexed { index, invoice -> + logger.info("index: $index") + logger.info("invoice: $invoice") + invoice.get("paidAmount") + } + } + + combinedResults.forEach { result: String -> + + if (groupedInvoices.containsKey(result)) { + groupedInvoices[result]!!.forEachIndexed { _, invoice -> + sheet.getRow(rowIndex++).apply { + createCell(0).apply { + setCellValue(result) + } + + createCell(1).apply { + setCellValue(0.0) + cellStyle.dataFormat = accountingStyle + } + + createCell(2).apply { + setCellValue(invoice["paidAmount"] as Double) + cellStyle.dataFormat = accountingStyle } - cellStyle.dataFormat = accountingStyle - } - getCell(4)?.apply { - setCellValue(invoice.milestonePayment!!.description!!) + createCell(3).apply { + val lastRow = rowIndex - 1 + if (lastRow == 15) { + cellFormula = + "C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString()) + } else { + cellFormula = + "IF(B{currentRow}>0,D{lastRow}-B{currentRow},D{lastRow}+C{currentRow})".replace( + "{currentRow}", + rowIndex.toString() + ).replace("{lastRow}", lastRow.toString()) + } + cellStyle.dataFormat = accountingStyle + } + + createCell(4)?.apply { + setCellValue(invoice["description"].toString()) + } } } } - if (timesheet != null) { - sheet.getRow(rowIndex++)?.apply { + if (groupedTimesheets.containsKey(result)) { + sheet.getRow(rowIndex++).apply { - logger.info("TIMESHEET NOT NULL--------------:") - logger.info("getCell(0)--------------: ${getCell(0)}") - logger.info("dateFormatter--------------: $dateFormatter") - logger.info("result.format--------------: ${result.format(dateFormatter)}") - getCell(0)?.apply { + createCell(0).apply { setCellValue(result.format(dateFormatter)) } - getCell(1)?.apply { - setCellValue(timesheet.staff!!.salary.hourlyRate.toDouble() * ((timesheet.normalConsumed ?: 0.0) + (timesheet.otConsumed ?: 0.0))) + createCell(1).apply { + setCellValue(groupedTimesheets[result]!!.sum()) cellStyle.dataFormat = accountingStyle } - getCell(2)?.apply { + createCell(2).apply { setCellValue(0.0) cellStyle.dataFormat = accountingStyle } - getCell(3)?.apply { + createCell(3).apply { val lastRow = rowIndex - 1 if (lastRow == 15) { cellFormula = "C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString()) } else { - cellFormula = "IF(B{currentRow}>0,D{lastRow}-B{currentRow},D{lastRow}+C{currentRow})".replace("{currentRow}", rowIndex.toString()).replace("{lastRow}", lastRow.toString()) + cellFormula = + "IF(B{currentRow}>0,D{lastRow}-B{currentRow},D{lastRow}+C{currentRow})".replace( + "{currentRow}", + rowIndex.toString() + ).replace("{lastRow}", lastRow.toString()) } cellStyle.dataFormat = accountingStyle } - getCell(4)?.apply { + createCell(4).apply { setCellValue("Monthly Manpower Expenditure") } } @@ -392,7 +449,7 @@ open class ReportService ( // cellStyle.borderBottom = BorderStyle.DOUBLE cellStyle.alignment = HorizontalAlignment.CENTER } - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) // rowIndex += 1 sheet.getRow(rowIndex).getCell(0).apply { @@ -414,7 +471,7 @@ open class ReportService ( cellStyle.alignment = HorizontalAlignment.CENTER cellStyle.dataFormat = accountingStyle } - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) // rowIndex += 1 sheet.getRow(rowIndex).getCell(0).apply { @@ -428,7 +485,7 @@ open class ReportService ( cellStyle.dataFormat = accountingStyle } - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) rowIndex += 1 sheet.getRow(rowIndex).getCell(0).apply { @@ -453,12 +510,12 @@ open class ReportService ( // cellStyle.borderBottom = BorderStyle.DOUBLE } sheet.getRow(rowIndex).getCell(2).apply { - cellFormula = "C${rowIndex-2}+C${rowIndex-1}" + cellFormula = "C${rowIndex - 2}+C${rowIndex - 1}" cellStyle.dataFormat = accountingStyle // cellStyle.borderTop = BorderStyle.THIN // cellStyle.borderBottom = BorderStyle.DOUBLE } - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// rowIndex = 7 columnIndex = 2 @@ -487,21 +544,21 @@ open class ReportService ( } } } - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - sheet.getRow(rowIndex).apply { - getCell(columnIndex).setCellValue("Leave Hours") - getCell(columnIndex).cellStyle.alignment = HorizontalAlignment.CENTER + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + sheet.getRow(rowIndex).apply { + getCell(columnIndex).setCellValue("Leave Hours") + getCell(columnIndex).cellStyle.alignment = HorizontalAlignment.CENTER - columnIndex += 1 - getCell(columnIndex).setCellValue("Daily Manhour Spent\n" + "(Excluding Leave Hours)") - getCell(columnIndex).cellStyle.alignment = HorizontalAlignment.CENTER - columnSize = columnIndex - } - sheet.addMergedRegion(CellRangeAddress(6,6 , 2, columnIndex)) + columnIndex += 1 + getCell(columnIndex).setCellValue("Daily Manhour Spent\n" + "(Excluding Leave Hours)") + getCell(columnIndex).cellStyle.alignment = HorizontalAlignment.CENTER + columnSize = columnIndex + } + sheet.addMergedRegion(CellRangeAddress(6, 6, 2, columnIndex)) ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// for (i in 2 until columnIndex) { for (k in 0 until rowSize) { - sheet.getRow(8+k).getCell(i).apply { + sheet.getRow(8 + k).getCell(i).apply { setCellValue(0.0) cellStyle.dataFormat = accountingStyle } @@ -512,14 +569,17 @@ open class ReportService ( if (sheet.getRow(rowIndex - 1).getCell(2).stringCellValue != "Leave Hours") { // cal daily spent manhour for (i in 0 until rowSize) { - val cell = sheet.getRow(rowIndex)?.getCell(columnIndex) ?: sheet.getRow(rowIndex + i)?.createCell(columnIndex) - cell?.cellFormula = "SUM(${getColumnAlphabet(2)}${rowIndex+1}:${getColumnAlphabet(columnIndex)}${rowIndex+1})" // should columnIndex - 2 + val cell = + sheet.getRow(rowIndex)?.getCell(columnIndex) ?: sheet.getRow(rowIndex + i)?.createCell(columnIndex) + cell?.cellFormula = + "SUM(${getColumnAlphabet(2)}${rowIndex + 1}:${getColumnAlphabet(columnIndex)}${rowIndex + 1})" // should columnIndex - 2 rowIndex++ } // cal subtotal for (i in 0 until columnSize) { // minus last col val cell = sheet.getRow(rowIndex)?.getCell(2) ?: sheet.getRow(rowIndex)?.createCell(2) - cell?.cellFormula = "SUM(${getColumnAlphabet(2)}${rowIndex}:${getColumnAlphabet(columnIndex + i)}${rowIndex})" + cell?.cellFormula = + "SUM(${getColumnAlphabet(2)}${rowIndex}:${getColumnAlphabet(columnIndex + i)}${rowIndex})" cell?.cellStyle?.dataFormat = accountingStyle cell?.cellStyle?.setFont(boldFont) // cell?.cellStyle?.borderTop = BorderStyle.THIN @@ -528,7 +588,8 @@ open class ReportService ( } else { // just for preview when no data // cal daily spent manhour for (i in 0 until rowSize) { - val cell = sheet.getRow(rowIndex)?.getCell(columnIndex) ?: sheet.getRow(rowIndex + i)?.createCell(columnIndex) + val cell = + sheet.getRow(rowIndex)?.getCell(columnIndex) ?: sheet.getRow(rowIndex + i)?.createCell(columnIndex) cell?.setCellValue("daily spent manhour") rowIndex++ } @@ -548,7 +609,7 @@ open class ReportService ( @Throws(IOException::class) private fun createSalaryList( - salarys : List, + salarys: List, templatePath: String, ): Workbook { @@ -588,19 +649,19 @@ open class ReportService ( private fun createLateStartReport( project: Project, templatePath: String - ):Workbook{ + ): Workbook { project val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) val sheet = workbook.getSheetAt(0) - + // Formatting the current date to "YYYY/MM/DD" and setting it to cell C2 val formattedToday = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) val dateCell = sheet.getRow(1)?.getCell(2) ?: sheet.getRow(1).createCell(2) dateCell.setCellValue(formattedToday) - + // Styling for cell A1: Font size 16 and bold val headerFont = workbook.createFont().apply { bold = true @@ -612,7 +673,7 @@ open class ReportService ( val headerCell = sheet.getRow(0)?.getCell(0) ?: sheet.getRow(0).createCell(0) headerCell.cellStyle = headerCellStyle headerCell.setCellValue("Report Title") - + // Apply styles from A2 to A4 (bold) val boldFont = workbook.createFont().apply { bold = true } val boldCellStyle = workbook.createCellStyle().apply { setFont(boldFont) } @@ -621,7 +682,7 @@ open class ReportService ( val cell = row?.getCell(0) ?: row.createCell(0) cell.cellStyle = boldCellStyle } - + // Apply styles from A6 to J6 (bold, bottom border, center alignment) val styleA6ToJ6 = workbook.createCellStyle().apply { setFont(boldFont) @@ -634,7 +695,7 @@ open class ReportService ( val cell = row?.getCell(cellAddress.column) ?: row.createCell(cellAddress.column) cell.cellStyle = styleA6ToJ6 } - + // Setting column widths dynamically based on content length (example logic) val maxContentWidths = IntArray(10) { 8 } // Initial widths for A to J for (rowIndex in 0..sheet.lastRowNum) { @@ -652,31 +713,31 @@ open class ReportService ( for (colIndex in 0..9) { sheet.setColumnWidth(colIndex, (maxContentWidths[colIndex] + 2) * 256) // Set the width for each column } - + return workbook } open fun getFinancialStatus(projectId: Long?): List> { val sql = StringBuilder( "with 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, t2.name, p.planStart , p.planEnd , p.expectedTotalFee ," - + " s.name , IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.hourlyRate," - + " cte_i.sumIssuedAmount, cte_i.sumPaidAmount" - + " 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 customer c on c.id = p.customerId" - + " left join team t2 on t2.id = s.teamId" - + " left join cte_invoice cte_i on cte_i.code = p.code" + + "from invoice i" + + "left join project p on p.code = i.projectCode" + + "group by p.code" + + ")" + + " Select p.code, p.description, c.name, t2.name, p.planStart , p.planEnd , p.expectedTotalFee ," + + " s.name , IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.hourlyRate," + + " cte_i.sumIssuedAmount, cte_i.sumPaidAmount" + + " 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 customer c on c.id = p.customerId" + + " left join team t2 on t2.id = s.teamId" + + " left join cte_invoice cte_i on cte_i.code = p.code" ) - if (projectId!! > 0){ + if (projectId!! > 0) { sql.append(" where p.id = :projectId ") } sql.append(" order by p.code")