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 e9c8b73..388d397 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 @@ -6,11 +6,27 @@ import com.ffii.tsms.modules.data.entity.Customer import com.ffii.tsms.modules.data.entity.CustomerType import com.ffii.tsms.modules.data.entity.CustomerRepository import com.ffii.tsms.modules.data.entity.CustomerTypeRepository +import com.ffii.tsms.modules.data.web.models.FinancialSummaryByClient import com.ffii.tsms.modules.data.web.models.SaveCustomerResponse +import com.ffii.tsms.modules.project.entity.Invoice +import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.project.web.models.SaveCustomerRequest +import com.ffii.tsms.modules.timesheet.entity.Timesheet +import org.apache.poi.ss.usermodel.BorderStyle +import org.apache.poi.ss.usermodel.HorizontalAlignment +import org.apache.poi.ss.usermodel.Sheet +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.ss.util.CellRangeAddress +import org.apache.poi.ss.util.CellUtil +import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.springframework.beans.BeanUtils +import org.springframework.core.io.ClassPathResource import org.springframework.stereotype.Service +import java.io.ByteArrayOutputStream +import java.io.IOException import java.math.BigDecimal +import java.time.LocalDate +import java.time.format.DateTimeFormatter import java.util.Optional @Service @@ -22,6 +38,11 @@ open class DashboardService( private val staffsService: StaffsService, private val jdbcDao: JdbcDao ) { + private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd") + private val FORMATTED_TODAY = LocalDate.now().format(DATE_FORMATTER) + + private val FINANCIAL_SUMMARY_FOR_CLIENT = "templates/report/[Dashboard] Financial Summary for client.xlsx" + private val FINANCIAL_SUMMARY_FOR_PROJET = "templates/report/[Dashboard] Financial Summary for project.xlsx" fun CustomerSubsidiary(args: Map): List> { val sql = StringBuilder("select" @@ -2105,6 +2126,156 @@ open class DashboardService( "no_authority" } } + + @Throws(IOException::class) + fun exportFinancialSummaryByClientExcel( + financialSummaryByClients: List, + ): ByteArray { + // Generate the Excel report with query results + val workbook: Workbook = + createFinancialSummaryByClientExcel(financialSummaryByClients, FINANCIAL_SUMMARY_FOR_CLIENT) + + // Write the workbook to a ByteArrayOutputStream + val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() + workbook.write(outputStream) + workbook.close() + + return outputStream.toByteArray() + } + + @Throws(IOException::class) + private fun createFinancialSummaryByClientExcel( + financialSummaryByClients: 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) + + // accounting style + comma style + val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") + + // normal font + val normalFont = workbook.createFont().apply { + fontName = "Times New Roman" + } + + val normalFontStyle = workbook.createCellStyle().apply { + setFont(normalFont) + } + + var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field + var columnIndex = 1 + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue(FORMATTED_TODAY) + } + + rowIndex = 4 + financialSummaryByClients.forEach { financialSummaryByClient: FinancialSummaryByClient -> + sheet.createRow(rowIndex++).apply { + createCell(0).apply { + setCellValue(financialSummaryByClient.customerCode) + cellStyle = normalFontStyle + CellUtil.setAlignment(this, HorizontalAlignment.LEFT) + } + + createCell(1).apply { + setCellValue(financialSummaryByClient.customerName) + cellStyle = normalFontStyle + CellUtil.setAlignment(this, HorizontalAlignment.LEFT) + } + + createCell(2).apply { + setCellValue(financialSummaryByClient.projectNo) + cellStyle = normalFontStyle.apply { + dataFormat = accountingStyle + } + CellUtil.setAlignment(this, HorizontalAlignment.LEFT) + } + + createCell(3).apply { + cellFormula = "IF(E${rowIndex}>=1,\"Positive\",\"Negative\")" + cellStyle = normalFontStyle + CellUtil.setAlignment(this, HorizontalAlignment.CENTER) + } + + createCell(4).apply { + cellFormula = "IFERROR(IF(K${rowIndex}=1, 0, K${rowIndex}/J${rowIndex}),0)" + cellStyle = normalFontStyle.apply { + dataFormat = accountingStyle + } + CellUtil.setAlignment(this, HorizontalAlignment.RIGHT) + } + + createCell(5).apply { + cellFormula = "IF(G${rowIndex}>=1,\"Positive\",\"Negative\")" + cellStyle = normalFontStyle + CellUtil.setAlignment(this, HorizontalAlignment.CENTER) + } + + createCell(6).apply { + cellFormula = "IFERROR(H${rowIndex}/J${rowIndex},0)" + cellStyle = normalFontStyle.apply { + dataFormat = accountingStyle + } + CellUtil.setAlignment(this, HorizontalAlignment.RIGHT) + } + + createCell(7).apply { + setCellValue(financialSummaryByClient.totalFee) + cellStyle = normalFontStyle.apply { + dataFormat = accountingStyle + } + CellUtil.setAlignment(this, HorizontalAlignment.RIGHT) + } + + createCell(8).apply { + cellFormula = "H${rowIndex}*80%" + cellStyle = normalFontStyle.apply { + dataFormat = accountingStyle + } + CellUtil.setAlignment(this, HorizontalAlignment.RIGHT) + } + + createCell(9).apply { + setCellValue(financialSummaryByClient.cumulativeExpenditure) + cellStyle = normalFontStyle.apply { + dataFormat = accountingStyle + } + CellUtil.setAlignment(this, HorizontalAlignment.RIGHT) + } + + createCell(10).apply { + setCellValue(financialSummaryByClient.totalInvoiced) + cellStyle = normalFontStyle.apply { + dataFormat = accountingStyle + } + CellUtil.setAlignment(this, HorizontalAlignment.RIGHT) + } + + createCell(11).apply { + cellFormula = "IF(H${rowIndex}-K${rowIndex}<0,0,H${rowIndex}-K${rowIndex})" + cellStyle = normalFontStyle.apply { + dataFormat = accountingStyle + } + CellUtil.setAlignment(this, HorizontalAlignment.RIGHT) + } + + createCell(12).apply { + setCellValue(financialSummaryByClient.totalReceived) + cellStyle = normalFontStyle.apply { + dataFormat = accountingStyle + } + CellUtil.setAlignment(this, HorizontalAlignment.RIGHT) + } + } + } + + return workbook + } } 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 679a734..615af9f 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 @@ -2,9 +2,6 @@ package com.ffii.tsms.modules.data.web import com.ffii.tsms.modules.data.entity.Customer import com.ffii.tsms.modules.data.entity.CustomerType -import com.ffii.tsms.modules.data.web.models.CustomerResponse -import com.ffii.tsms.modules.data.web.models.SaveCustomerResponse -import com.ffii.tsms.modules.project.web.models.SaveCustomerRequest import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -19,6 +16,11 @@ import org.springframework.web.bind.annotation.ResponseStatus import com.ffii.core.response.RecordsRes import com.ffii.core.utils.CriteriaArgsBuilder import com.ffii.tsms.modules.data.service.* +import com.ffii.tsms.modules.data.web.models.ExportFinancialSummaryByClientExcelRequest +import org.springframework.core.io.ByteArrayResource +import org.springframework.core.io.Resource +import org.springframework.http.ResponseEntity +import java.time.LocalDate @RestController @RequestMapping("/dashboard") @@ -440,4 +442,12 @@ class DashboardController( val args = mutableMapOf() return dashboardService.staffCombo(args) } + + @PostMapping("/exportFinancialSummaryByClientExcel") + fun exportFinancialSummaryByClientExcel(@Valid @RequestBody request: ExportFinancialSummaryByClientExcelRequest): ResponseEntity { + val reportResult: ByteArray = dashboardService.exportFinancialSummaryByClientExcel(request.financialSummaryByClients) + return ResponseEntity.ok() + .header("filename", "Financial Summary for Client - " + LocalDate.now() + ".xlsx") + .body(ByteArrayResource(reportResult)) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/web/models/ExportDashboardExcelRequest.kt b/src/main/java/com/ffii/tsms/modules/data/web/models/ExportDashboardExcelRequest.kt new file mode 100644 index 0000000..54b71b6 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/data/web/models/ExportDashboardExcelRequest.kt @@ -0,0 +1,15 @@ +package com.ffii.tsms.modules.data.web.models + +data class FinancialSummaryByClient ( + val customerCode: String, + val customerName: String, + val projectNo: Double, + val totalFee: Double, + val cumulativeExpenditure: Double, + val totalInvoiced: Double, + val totalReceived: Double, +) + +data class ExportFinancialSummaryByClientExcelRequest ( + val financialSummaryByClients: List +) \ 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 d043718..36ee1e3 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 @@ -58,6 +58,7 @@ open class ProjectsService( private val customerRepository: CustomerRepository, private val subsidiaryRepository: SubsidiaryRepository, private val customerSubsidiaryService: CustomerSubsidiaryService, + private val staffsService: StaffsService, ) { open fun allProjects(): List { return projectRepository.findProjectSearchInfoByDeletedIsFalseOrderByCodeDesc() @@ -83,12 +84,15 @@ open class ProjectsService( } open fun allAssignedProjects(): List { + val currentStaff = staffsService.currentStaff() return SecurityUtils.getUser().getOrNull()?.let { user -> staffRepository.findByUserId(user.id).getOrNull()?.let { staff -> staffAllocationRepository.findOnGoingAssignedProjectsByStaff(staff).map { project -> val timesheetHours = timesheetRepository.totalHoursConsumedByProject(project) + val staffTimesheetHours = timesheetRepository.totalHoursConsumedByProjectAndStaff(project, currentStaff!!) - AssignedProject(id = project.id!!, + AssignedProject( + id = project.id!!, code = project.code!!, name = project.name!!, tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }, @@ -104,7 +108,9 @@ open class ProjectsService( hoursAllocated = project.totalManhour ?: 0.0, hoursAllocatedOther = 0.0, hoursSpent = timesheetHours.normalConsumed, - hoursSpentOther = timesheetHours.otConsumed + hoursSpentOther = timesheetHours.otConsumed, + currentStaffHoursSpent = staffTimesheetHours.normalConsumed, + currentStaffHoursSpentOther = staffTimesheetHours.otConsumed ) } } @@ -213,7 +219,7 @@ open class ProjectsService( project.apply { name = request.projectName description = request.projectDescription - code = if (request.mainProjectId != null && mainProject != null) createSubProjectCode( + code = if (request.mainProjectId != null && mainProject != null && request.projectCode == null) createSubProjectCode( mainProject, project ) else request.projectCode diff --git a/src/main/java/com/ffii/tsms/modules/project/web/models/AssignedProject.kt b/src/main/java/com/ffii/tsms/modules/project/web/models/AssignedProject.kt index 52e33a9..d7e9f7f 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/models/AssignedProject.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/models/AssignedProject.kt @@ -14,6 +14,8 @@ data class AssignedProject( val hoursAllocatedOther: Double, val hoursSpent: Double, val hoursSpentOther: Double, + val currentStaffHoursSpent: Double, + val currentStaffHoursSpentOther: Double, ) data class MilestoneInfo( diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt b/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt index 2826b9f..d1ef25e 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/entity/TimesheetRepository.kt @@ -21,6 +21,9 @@ interface TimesheetRepository : AbstractRepository { @Query("SELECT new com.ffii.tsms.modules.timesheet.entity.projections.TimesheetHours(IFNULL(SUM(normalConsumed), 0), IFNULL(SUM(otConsumed), 0)) FROM Timesheet t JOIN ProjectTask pt on t.projectTask = pt WHERE pt.project = ?1") fun totalHoursConsumedByProject(project: Project): TimesheetHours + @Query("SELECT new com.ffii.tsms.modules.timesheet.entity.projections.TimesheetHours(IFNULL(SUM(normalConsumed), 0), IFNULL(SUM(otConsumed), 0)) FROM Timesheet t JOIN ProjectTask pt on t.projectTask = pt WHERE pt.project = ?1 and t.staff = ?2") + fun totalHoursConsumedByProjectAndStaff(project: Project, staff: Staff): TimesheetHours + fun findByStaffAndRecordDateBetweenOrderByRecordDate(staff: Staff, start: LocalDate, end: LocalDate): List fun findDistinctProjectTaskByStaffAndRecordDateBetweenOrderByRecordDate(staff: Staff, start: LocalDate, end: LocalDate): List diff --git a/src/main/resources/db/changelog/changes/20240710_01_cyril/01_update_authority.sql b/src/main/resources/db/changelog/changes/20240710_01_cyril/01_update_authority.sql new file mode 100644 index 0000000..1abb68a --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240710_01_cyril/01_update_authority.sql @@ -0,0 +1,9 @@ +-- liquibase formatted sql +-- changeset cyril:authority, user_authority + +INSERT INTO authority (authority,name) +VALUES ('MAINTAIN_NORMAL_STAFF_WORKSPACE','Maintain Normal Staff Workspace'); +INSERT INTO authority (authority,name) +VALUES ('MAINTAIN_MANAGEMENT_STAFF_WORKSPACE','Maintain Management Staff Workspace'); + +INSERT INTO `user_authority` VALUES (1,43); \ No newline at end of file diff --git a/src/main/resources/templates/report/[Dashboard] Financial Summary for client.xlsx b/src/main/resources/templates/report/[Dashboard] Financial Summary for client.xlsx new file mode 100644 index 0000000..d2674ba Binary files /dev/null and b/src/main/resources/templates/report/[Dashboard] Financial Summary for client.xlsx differ diff --git a/src/main/resources/templates/report/[Dashboard] Financial Summary for project.xlsx b/src/main/resources/templates/report/[Dashboard] Financial Summary for project.xlsx new file mode 100644 index 0000000..bb7e7f9 Binary files /dev/null and b/src/main/resources/templates/report/[Dashboard] Financial Summary for project.xlsx differ