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/entity/projections/FinancialStatusReportInfo.kt b/src/main/java/com/ffii/tsms/modules/data/entity/projections/FinancialStatusReportInfo.kt new file mode 100644 index 0000000..51275c9 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/data/entity/projections/FinancialStatusReportInfo.kt @@ -0,0 +1,20 @@ +package com.ffii.tsms.modules.data.entity.projections + +import java.math.BigDecimal +import java.time.LocalDate + +interface FinancialStatusReportInfo { + val code: String + val description: String + val client: String + val teamLead: String + val planStart: LocalDate + val planEnd: LocalDate + val expectedTotalFee: BigDecimal + val staff: String + val normalConsumed: BigDecimal + val otConsumed: BigDecimal + val hourlyRate: BigDecimal + val sumIssuedAmount: BigDecimal + val sumPaidAmount: BigDecimal +} \ No newline at end of file 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 4add027..8d807bd 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 @@ -23,6 +23,9 @@ open class DashboardService( fun CustomerSubsidiary(args: Map): List> { val sql = StringBuilder("select" + + " row_number()OVER (" + + " ORDER BY c.id" + + " ) as id," + " c.id as customerId," + " c.name as customerName," + " c.code as customerCode," @@ -30,18 +33,17 @@ open class DashboardService( + " c.district as cutomerDistrict," + " c.brNo as customerBrNo," + " c.typeId as customerTypeId," - + " s.id as subsidiaryId," - + " s.name as subsidiaryName," - + " s.code as subsidiaryCode," - + " s.address as subsidiaryAddress," - + " s.district as subsidiaryDistrict," - + " s.brNo as subsidiaryBrNo," - + " s.typeId as subsidiaryTypeId," + + " coalesce (s.id,'-') as subsidiaryId," + + " coalesce (s.name,'-') as subsidiaryName," + + " coalesce (s.code,'-') as subsidiaryCode," + + " coalesce (s.address,'-') as subsidiaryAddress," + + " coalesce (s.district,'-') as subsidiaryDistrict," + + " coalesce (s.brNo,'-') as subsidiaryBrNo," + + " coalesce (s.typeId,'-') as subsidiaryTypeId," + " count(p.id) as projectNo" + " from customer c" - + " left join customer_subsidiary cs on c.id = cs.customerId" - + " left join subsidiary s on cs.subsidiaryId = s.id" - + " left join project p on cs.customerId = p.customerId and cs.subsidiaryId = p.customerSubsidiaryId " + + " left join project p on c.id = p.customerId" + + " left join subsidiary s on p.customerSubsidiaryId = s.id" + " where c.deleted = 0" ) if (args != null) { @@ -94,4 +96,47 @@ open class DashboardService( return jdbcDao.queryForList(sql.toString(), args) } + + fun searchCustomerNonSubsidiaryProject(args: Map): List> { + val sql = StringBuilder("select" + + " p.id as id," + + " p.id as projectId," + + " p.code as projectCode," + + " p.name as projectName," + + " te.code as team," + + " s.name as teamLead," + + " tg.name as expectedStage," + + " p.totalManhour as budgetedManhour," + + " sum(t.normalConsumed) + sum(t.otConsumed) as spentManhour," + + " p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed) as remainedManhour," + + " coalesce (round(((sum(t.normalConsumed) - sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage," + + " DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d') as comingPaymentMilestone" + + " 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 team te on p.teamLead = te.teamLead" + + " left join staff s on te.teamLead = s.id" + + " left join milestone m on p.id = m.projectId and curdate() >= m.startDate and curdate() <= m.endDate" + + " left join task_group tg on m.taskGroupId = tg.id" + + " left join (" + + " select" + + " mp.date as comingPaymentMilestone" + + " from project p" + + " left join milestone m on p.id = m.projectId" + + " left join milestone_payment mp on m.id = mp.milestoneId" + + " where p.customerId = :customerId" + + " and isNull(p.customerSubsidiaryId)" + + " and mp.date >= curdate()" + + " order by date asc" + + " limit 1" + + " ) milestonePayment on 1=1" + + " where p.customerId = :customerId" + + " and isNull(p.customerSubsidiaryId)" + + " group by p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone" + ) + + return jdbcDao.queryForList(sql.toString(), args) + } } + + 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/data/web/DashboardController.kt b/src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt index dbe7e3a..dea71c7 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 @@ -54,6 +54,7 @@ class DashboardController( val customerId = request?.getParameter("customerId") val subsidiaryId = request?.getParameter("subsidiaryId") val args = mutableMapOf() + var result: List> = emptyList() if (customerId != null) { args["customerId"] = customerId } @@ -61,6 +62,11 @@ class DashboardController( args["subsidiaryId"] = subsidiaryId } - return dashboardService.searchCustomerSubsidiaryProject(args) + if (customerId != null && subsidiaryId != null) { + result = dashboardService.searchCustomerSubsidiaryProject(args) + } else if (customerId != null && subsidiaryId == null){ + result = dashboardService.searchCustomerNonSubsidiaryProject(args) + } + return result } } \ No newline at end of file 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 2cd303e..10a950c 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 @@ -15,5 +15,5 @@ interface ProjectRepository : AbstractRepository { fun findInvoiceInfoSearchInfoById(id: Long): List - fun findFirstByIsClpProjectAndIdNotOrderByIdDesc(isClpProject: Boolean, id: Serializable?): Project? + fun findFirstByIsClpProjectAndIdIsNotOrderByIdDesc(isClpProject: Boolean, id: Serializable?): Project? } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index 17c4cf9..d16ac4d 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -131,7 +131,7 @@ open class ProjectsService( val checkIsClpProject = isClpProject != null && isClpProject val prefix = if (checkIsClpProject) "C" else "M" - val latestProjectCode = projectRepository.findFirstByIsClpProjectAndIdNotOrderByIdDesc(checkIsClpProject, project.id) + val latestProjectCode = projectRepository.findFirstByIsClpProjectAndIdIsNotOrderByIdDesc(checkIsClpProject, project.id ?: -1) if (latestProjectCode != null) { val lastFix = latestProjectCode.code!!.split("-")[1] //C-0001 -> 0001 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 f862e6b..1515f24 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 @@ -1,12 +1,17 @@ package com.ffii.tsms.modules.report.service +import com.ffii.core.support.JdbcDao import com.ffii.tsms.modules.data.entity.Salary import com.ffii.tsms.modules.data.entity.Staff 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.* +import org.apache.poi.ss.util.CellAddress import org.apache.poi.ss.util.CellRangeAddress import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.springframework.core.io.ClassPathResource @@ -16,11 +21,15 @@ import java.io.IOException import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.* -import org.apache.poi.ss.util.CellAddress +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) @@ -32,8 +41,8 @@ open class ReportService { // ==============================|| GENERATE REPORT ||============================== // - fun genFinancialStatusReport(): ByteArray { - val workbook: Workbook = createFinancialStatusReport(FINANCIAL_STATUS_REPORT) + fun genFinancialStatusReport(projectId: Long): ByteArray { + val workbook: Workbook = createFinancialStatusReport(FINANCIAL_STATUS_REPORT, projectId) val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) @@ -41,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) @@ -55,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() @@ -82,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() @@ -94,7 +121,8 @@ open class ReportService { // EX01 Financial Report private fun createFinancialStatusReport( templatePath: String, - ) : Workbook { + projectId: Long, + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -117,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 } @@ -160,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 } @@ -176,88 +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 } + 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} + 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 + } + } + } - if (invoice != null) { - sheet.getRow(rowIndex++).apply { + 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 + ) + } + } - getCell(0).apply { - setCellValue(result.format(dateFormatter)) - } + groupedTimesheets.entries.forEach { (key, value) -> + logger.info("key: $key") + logger.info("value: " + value.sumOf { it }) + } - getCell(1).apply { - setCellValue(0.0) - cellStyle.dataFormat = accountingStyle - } + 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") + } + } - getCell(2).apply { - setCellValue(invoice.paidAmount!!.toDouble()) - cellStyle.dataFormat = accountingStyle - } + combinedResults.forEach { result: String -> - 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()) + if (groupedInvoices.containsKey(result)) { + groupedInvoices[result]!!.forEachIndexed { _, invoice -> + sheet.getRow(rowIndex++).apply { + createCell(0).apply { + setCellValue(result) } - cellStyle.dataFormat = accountingStyle - } - getCell(4).apply { - setCellValue(invoice.milestonePayment!!.description!!) + createCell(1).apply { + setCellValue(0.0) + cellStyle.dataFormat = accountingStyle + } + + createCell(2).apply { + setCellValue(invoice["paidAmount"] as Double) + cellStyle.dataFormat = accountingStyle + } + + 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) { + if (groupedTimesheets.containsKey(result)) { sheet.getRow(rowIndex++).apply { - 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") } } @@ -370,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 { @@ -392,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 { @@ -406,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 { @@ -431,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 @@ -465,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 } @@ -490,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 @@ -506,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++ } @@ -526,7 +609,7 @@ open class ReportService { @Throws(IOException::class) private fun createSalaryList( - salarys : List, + salarys: List, templatePath: String, ): Workbook { @@ -567,29 +650,30 @@ open class ReportService { project: Project, templatePath: String ):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) - // Start populating project data starting at row 7 - val startRow = 6 // 0-based index for row 7 - val projectDataRow = sheet.createRow(startRow) - - // Populate the row with project data - projectDataRow.createCell(1).setCellValue(project.code) // Column B - projectDataRow.createCell(2).setCellValue(project.name) // Column C - projectDataRow.createCell(3).setCellValue(project.teamLead?.name) // Column D - projectDataRow.createCell(4).setCellValue(project.custLeadName) // Column E - projectDataRow.createCell(5).setCellValue( - project.planStart?.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) ?: "N/A" // Column F - ) + // // Start populating project data starting at row 7 + // val startRow = 6 // 0-based index for row 7 + // val projectDataRow = sheet.createRow(startRow) + + // // Populate the row with project data + // projectDataRow.createCell(1).setCellValue(project.code) // Column B + // projectDataRow.createCell(2).setCellValue(project.name) // Column C + // projectDataRow.createCell(3).setCellValue(project.teamLead?.name) // Column D + // projectDataRow.createCell(4).setCellValue(project.custLeadName) // Column E + // projectDataRow.createCell(5).setCellValue( + // project.planStart?.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) ?: "N/A" // Column F + // ) // Styling for cell A1: Font size 16 and bold val headerFont = workbook.createFont().apply { @@ -602,7 +686,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) } @@ -611,7 +695,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) @@ -624,7 +708,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) { @@ -642,8 +726,37 @@ 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" + ) + + if (projectId!! > 0) { + sql.append(" where p.id = :projectId ") + } + sql.append(" order by p.code") + val args = mapOf("projectId" to projectId) + + return jdbcDao.queryForList(sql.toString(), args) + } + } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt index d96d1b0..3accd6d 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 @@ -44,7 +44,8 @@ class ReportController( @Throws(ServletRequestBindingException::class, IOException::class) fun getFinancialStatusReport(@RequestBody @Valid request: FinancialStatusReportRequest): ResponseEntity { - val reportResult: ByteArray = excelReportService.genFinancialStatusReport() + + val reportResult: ByteArray = excelReportService.genFinancialStatusReport(request.projectId) return ResponseEntity.ok() .header("filename", "Financial Status Report - " + LocalDate.now() + ".xlsx")