| @@ -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.CustomerType | ||||
| import com.ffii.tsms.modules.data.entity.CustomerRepository | import com.ffii.tsms.modules.data.entity.CustomerRepository | ||||
| import com.ffii.tsms.modules.data.entity.CustomerTypeRepository | 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.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.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.beans.BeanUtils | ||||
| import org.springframework.core.io.ClassPathResource | |||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import java.io.ByteArrayOutputStream | |||||
| import java.io.IOException | |||||
| import java.math.BigDecimal | import java.math.BigDecimal | ||||
| import java.time.LocalDate | |||||
| import java.time.format.DateTimeFormatter | |||||
| import java.util.Optional | import java.util.Optional | ||||
| @Service | @Service | ||||
| @@ -22,6 +38,11 @@ open class DashboardService( | |||||
| private val staffsService: StaffsService, | private val staffsService: StaffsService, | ||||
| private val jdbcDao: JdbcDao | 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<String, Any>): List<Map<String, Any>> { | fun CustomerSubsidiary(args: Map<String, Any>): List<Map<String, Any>> { | ||||
| val sql = StringBuilder("select" | val sql = StringBuilder("select" | ||||
| @@ -2105,6 +2126,156 @@ open class DashboardService( | |||||
| "no_authority" | "no_authority" | ||||
| } | } | ||||
| } | } | ||||
| @Throws(IOException::class) | |||||
| fun exportFinancialSummaryByClientExcel( | |||||
| financialSummaryByClients: List<FinancialSummaryByClient>, | |||||
| ): 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<FinancialSummaryByClient>, | |||||
| 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 | |||||
| } | |||||
| } | } | ||||
| @@ -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.Customer | ||||
| import com.ffii.tsms.modules.data.entity.CustomerType | 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.GetMapping | ||||
| import org.springframework.web.bind.annotation.RequestMapping | import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RestController | 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.response.RecordsRes | ||||
| import com.ffii.core.utils.CriteriaArgsBuilder | import com.ffii.core.utils.CriteriaArgsBuilder | ||||
| import com.ffii.tsms.modules.data.service.* | 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 | @RestController | ||||
| @RequestMapping("/dashboard") | @RequestMapping("/dashboard") | ||||
| @@ -440,4 +442,12 @@ class DashboardController( | |||||
| val args = mutableMapOf<String, Any>() | val args = mutableMapOf<String, Any>() | ||||
| return dashboardService.staffCombo(args) | return dashboardService.staffCombo(args) | ||||
| } | } | ||||
| @PostMapping("/exportFinancialSummaryByClientExcel") | |||||
| fun exportFinancialSummaryByClientExcel(@Valid @RequestBody request: ExportFinancialSummaryByClientExcelRequest): ResponseEntity<Resource> { | |||||
| val reportResult: ByteArray = dashboardService.exportFinancialSummaryByClientExcel(request.financialSummaryByClients) | |||||
| return ResponseEntity.ok() | |||||
| .header("filename", "Financial Summary for Client - " + LocalDate.now() + ".xlsx") | |||||
| .body(ByteArrayResource(reportResult)) | |||||
| } | |||||
| } | } | ||||
| @@ -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<FinancialSummaryByClient> | |||||
| ) | |||||
| @@ -58,6 +58,7 @@ open class ProjectsService( | |||||
| private val customerRepository: CustomerRepository, | private val customerRepository: CustomerRepository, | ||||
| private val subsidiaryRepository: SubsidiaryRepository, | private val subsidiaryRepository: SubsidiaryRepository, | ||||
| private val customerSubsidiaryService: CustomerSubsidiaryService, | private val customerSubsidiaryService: CustomerSubsidiaryService, | ||||
| private val staffsService: StaffsService, | |||||
| ) { | ) { | ||||
| open fun allProjects(): List<ProjectSearchInfo> { | open fun allProjects(): List<ProjectSearchInfo> { | ||||
| return projectRepository.findProjectSearchInfoByDeletedIsFalseOrderByCodeDesc() | return projectRepository.findProjectSearchInfoByDeletedIsFalseOrderByCodeDesc() | ||||
| @@ -83,12 +84,15 @@ open class ProjectsService( | |||||
| } | } | ||||
| open fun allAssignedProjects(): List<AssignedProject> { | open fun allAssignedProjects(): List<AssignedProject> { | ||||
| val currentStaff = staffsService.currentStaff() | |||||
| return SecurityUtils.getUser().getOrNull()?.let { user -> | return SecurityUtils.getUser().getOrNull()?.let { user -> | ||||
| staffRepository.findByUserId(user.id).getOrNull()?.let { staff -> | staffRepository.findByUserId(user.id).getOrNull()?.let { staff -> | ||||
| staffAllocationRepository.findOnGoingAssignedProjectsByStaff(staff).map { project -> | staffAllocationRepository.findOnGoingAssignedProjectsByStaff(staff).map { project -> | ||||
| val timesheetHours = timesheetRepository.totalHoursConsumedByProject(project) | val timesheetHours = timesheetRepository.totalHoursConsumedByProject(project) | ||||
| val staffTimesheetHours = timesheetRepository.totalHoursConsumedByProjectAndStaff(project, currentStaff!!) | |||||
| AssignedProject(id = project.id!!, | |||||
| AssignedProject( | |||||
| id = project.id!!, | |||||
| code = project.code!!, | code = project.code!!, | ||||
| name = project.name!!, | name = project.name!!, | ||||
| tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }, | tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }, | ||||
| @@ -104,7 +108,9 @@ open class ProjectsService( | |||||
| hoursAllocated = project.totalManhour ?: 0.0, | hoursAllocated = project.totalManhour ?: 0.0, | ||||
| hoursAllocatedOther = 0.0, | hoursAllocatedOther = 0.0, | ||||
| hoursSpent = timesheetHours.normalConsumed, | hoursSpent = timesheetHours.normalConsumed, | ||||
| hoursSpentOther = timesheetHours.otConsumed | |||||
| hoursSpentOther = timesheetHours.otConsumed, | |||||
| currentStaffHoursSpent = staffTimesheetHours.normalConsumed, | |||||
| currentStaffHoursSpentOther = staffTimesheetHours.otConsumed | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| @@ -213,7 +219,7 @@ open class ProjectsService( | |||||
| project.apply { | project.apply { | ||||
| name = request.projectName | name = request.projectName | ||||
| description = request.projectDescription | description = request.projectDescription | ||||
| code = if (request.mainProjectId != null && mainProject != null) createSubProjectCode( | |||||
| code = if (request.mainProjectId != null && mainProject != null && request.projectCode == null) createSubProjectCode( | |||||
| mainProject, | mainProject, | ||||
| project | project | ||||
| ) else request.projectCode | ) else request.projectCode | ||||
| @@ -14,6 +14,8 @@ data class AssignedProject( | |||||
| val hoursAllocatedOther: Double, | val hoursAllocatedOther: Double, | ||||
| val hoursSpent: Double, | val hoursSpent: Double, | ||||
| val hoursSpentOther: Double, | val hoursSpentOther: Double, | ||||
| val currentStaffHoursSpent: Double, | |||||
| val currentStaffHoursSpentOther: Double, | |||||
| ) | ) | ||||
| data class MilestoneInfo( | data class MilestoneInfo( | ||||
| @@ -21,6 +21,9 @@ interface TimesheetRepository : AbstractRepository<Timesheet, Long> { | |||||
| @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") | @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 | 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<Timesheet> | fun findByStaffAndRecordDateBetweenOrderByRecordDate(staff: Staff, start: LocalDate, end: LocalDate): List<Timesheet> | ||||
| fun findDistinctProjectTaskByStaffAndRecordDateBetweenOrderByRecordDate(staff: Staff, start: LocalDate, end: LocalDate): List<Timesheet> | fun findDistinctProjectTaskByStaffAndRecordDateBetweenOrderByRecordDate(staff: Staff, start: LocalDate, end: LocalDate): List<Timesheet> | ||||
| @@ -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); | |||||