@@ -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); |