diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/StaffSkillsetRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/StaffSkillsetRepository.java index f99a39a..86f2c50 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/StaffSkillsetRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/StaffSkillsetRepository.java @@ -1,6 +1,11 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface StaffSkillsetRepository extends AbstractRepository { + + List findByStaff(@Param("staff") Staff staff); } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java index 39038c0..3a3cf41 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java @@ -7,4 +7,6 @@ import java.util.List; public interface TeamRepository extends AbstractRepository { List findByDeletedFalse(); + + Team findByStaff(Staff staff); } \ 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 8d807bd..23f90c6 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 @@ -10,6 +10,7 @@ import com.ffii.tsms.modules.data.web.models.SaveCustomerResponse import com.ffii.tsms.modules.project.web.models.SaveCustomerRequest import org.springframework.beans.BeanUtils import org.springframework.stereotype.Service +import java.math.BigDecimal import java.util.Optional @Service @@ -23,9 +24,7 @@ open class DashboardService( fun CustomerSubsidiary(args: Map): List> { val sql = StringBuilder("select" - + " row_number()OVER (" - + " ORDER BY c.id" - + " ) as id," + + " ROW_NUMBER() OVER (ORDER BY c.id, c.name, c.code, c.address, c.district, c.brNo, c.typeId, s.id, s.name, s.code, s.address, s.district, s.brNo, s.typeId) AS RowNum," + " c.id as customerId," + " c.name as customerName," + " c.code as customerCode," @@ -45,6 +44,7 @@ open class DashboardService( + " left join project p on c.id = p.customerId" + " left join subsidiary s on p.customerSubsidiaryId = s.id" + " where c.deleted = 0" + + " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" ) if (args != null) { if (args.containsKey("customerName")) @@ -58,6 +58,7 @@ open class DashboardService( fun searchCustomerSubsidiaryProject(args: Map): List> { val sql = StringBuilder("select" + + " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id," + " p.id as id," + " p.id as projectId," + " p.code as projectCode," @@ -91,6 +92,7 @@ open class DashboardService( + " ) milestonePayment on 1=1" + " where p.customerId = :customerId" + " and p.customerSubsidiaryId = :subsidiaryId" + + " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" + " group by p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone" ) @@ -99,6 +101,7 @@ open class DashboardService( fun searchCustomerNonSubsidiaryProject(args: Map): List> { val sql = StringBuilder("select" + + " ROW_NUMBER() OVER (ORDER BY p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone) AS id," + " p.id as id," + " p.id as projectId," + " p.code as projectCode," @@ -132,11 +135,94 @@ open class DashboardService( + " ) milestonePayment on 1=1" + " where p.customerId = :customerId" + " and isNull(p.customerSubsidiaryId)" + + " and p.status not in (\"Pending to Start\",\"Completed\",\"Deleted\")" + " group by p.id, p.code, p.name, te.code, s.name, tg.name, p.totalManhour, milestonePayment.comingPaymentMilestone" ) return jdbcDao.queryForList(sql.toString(), args) } + + open fun getFinancialStatus(): List> { + val sql = StringBuilder( + " with cte_timesheet as (" + + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.hourlyRate" + + " 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 team t2 on t2.id = s.teamId" + + " )," + + " 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 as client, concat(t.code, \' - \', t.name) as teamLead, p.planStart , p.planEnd , p.expectedTotalFee," + + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed," + + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount" + + " from project p" + + " left join cte_timesheet cte_ts on p.code = cte_ts.code" + + " left join customer c on c.id = p.customerId" + + " left join tsmsdb.team t on t.teamLead = p.teamLead" + + " left join cte_invoice cte_i on cte_i.code = p.code" + + " where p.status = \'On-going\'" + ) + + sql.append(" order by p.code") + + return jdbcDao.queryForList(sql.toString()) + } + + fun searchFinancialSummary(): List> { + val financialStatus: List> = getFinancialStatus() + + val otFactor = BigDecimal(1) + + val tempList = mutableListOf>() + + for (item in financialStatus) { + val normalConsumed = item.getValue("normalConsumed") as Double + val hourlyRate = item.getValue("hourlyRate") as BigDecimal +// println("normalConsumed------------- $normalConsumed") +// println("hourlyRate------------- $hourlyRate") + val manHourRate = normalConsumed.toBigDecimal().multiply(hourlyRate) +// println("manHourRate------------ $manHourRate") + + val otConsumed = item.getValue("otConsumed") as Double + val manOtHourRate = otConsumed.toBigDecimal().multiply(hourlyRate).multiply(otFactor) + + if (!tempList.any { it.containsValue(item.getValue("code")) }) { + + tempList.add( + mapOf( + "code" to item.getValue("code"), + "description" to item.getValue("description"), + "client" to item.getValue("client"), + "teamLead" to item.getValue("teamLead"), + "planStart" to item.getValue("planStart"), + "planEnd" to item.getValue("planEnd"), + "expectedTotalFee" to item.getValue("expectedTotalFee"), + "normalConsumed" to manHourRate, + "otConsumed" to manOtHourRate, + "issuedAmount" to item.getValue("sumIssuedAmount"), + "paidAmount" to item.getValue("sumPaidAmount"), + ) + ) + } else { + // Find the existing Map in the tempList that has the same "code" value + val existingMap = tempList.find { it.containsValue(item.getValue("code")) }!! + + // Update the existing Map with the new manHourRate and manOtHourRate values + tempList[tempList.indexOf(existingMap)] = existingMap.toMutableMap().apply { + put("normalConsumed", (get("normalConsumed") as BigDecimal).add(manHourRate)) + put("otConsumed", (get("otConsumed") as BigDecimal).add(manOtHourRate)) + } + } + } + return tempList + } } 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 aaefa49..b9532c9 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 @@ -95,7 +95,6 @@ open class StaffsService( @Transactional(rollbackFor = [Exception::class]) open fun saveStaff(req: NewStaffRequest): Staff { -// if (req.staffId) val checkStaffIdList: List = staffRepository.findStaffSearchInfoByAndDeletedFalse() checkStaffIdList.forEach{ s -> if (s.staffId == req.staffId) { @@ -108,7 +107,6 @@ open class StaffsService( val grade = if (req.gradeId != null && req.gradeId > 0L) gradeRepository.findById(req.gradeId).orElseThrow() else null val team = if (req.teamId != null && req.teamId > 0L) teamRepository.findById(req.teamId).orElseThrow() else null val salary = salaryRepository.findBySalaryPoint(req.salaryId).orElseThrow() -// val salaryEffective = salaryEffectiveRepository.findById(req.salaryEffId).orElseThrow() val department = departmentRepository.findById(req.departmentId).orElseThrow() val user = userRepository.saveAndFlush( @@ -117,7 +115,6 @@ open class StaffsService( password = passwordEncoder.encode("mms1234") name = req.name phone1 = req.phone1 -// phone2 = req.phone2 ?: null email = req.email ?: null } ) @@ -142,7 +139,6 @@ open class StaffsService( this.company = company this.grade = grade this.team = team -// this.skill = skill this.salary = salary this.department = department } @@ -158,31 +154,31 @@ open class StaffsService( staffSkillsetRepository.save(ss) } } - -// val skillBatchInsertValues: MutableList> -// if (!req.skillSetId.isNullOrEmpty()) { -// skillBatchInsertValues = req.skillSetId.stream() -// .map { skillId -> mutableMapOf(("staffId" to staff.id) as Pair, "skillId" to skillId) } -// .collect(Collectors.toList()) -// jdbcDao.batchUpdate( -// "INSERT IGNORE INTO staff_skillset (staffId, skillId)" -// + " VALUES (:staffId, :skillId)", -// skillBatchInsertValues); -// } - salaryEffectiveService.saveSalaryEffective(staff.id!!, salary.id!!) return staff } @Transactional(rollbackFor = [Exception::class]) open fun updateStaff(req: NewStaffRequest, staff: Staff): Staff { + val args = java.util.Map.of("staffId", staff.id) + if(!req.skillSetId.isNullOrEmpty()) { + // remove all skills of the staff + jdbcDao.executeUpdate("DELETE FROM staff_skillset WHERE staffId = :staffId", args); + // add skiils + for (skillId in req.skillSetId) { + val skill = skillRepository.findById(skillId).orElseThrow() + val ss = StaffSkillset().apply { + this.staff = staff + this.skill = skill + } + staffSkillsetRepository.save(ss) + } + } val currentPosition = positionRepository.findById(req.currentPositionId).orElseThrow() val joinPosition = positionRepository.findById(req.joinPositionId).orElseThrow() val company = companyRepository.findById(req.companyId).orElseThrow() val grade = if (req.gradeId != null && req.gradeId > 0L) gradeRepository.findById(req.gradeId).orElseThrow() else null val team = if (req.teamId != null && req.teamId > 0L) teamRepository.findById(req.teamId).orElseThrow() else null -// val skill = if (req.skillSetId != null && req.skillSetId > 0L) skillRepository.findById(req.skillSetId).orElseThrow() else null val salary = salaryRepository.findById(req.salaryId).orElseThrow() -// val salaryEffective = salaryEffectiveRepository.findById(req.salaryEffId).orElseThrow() val department = departmentRepository.findById(req.departmentId).orElseThrow() staff.apply { @@ -203,7 +199,6 @@ open class StaffsService( this.company = company this.grade = grade this.team = team -// this.skill = skill this.salary = salary this.department = department } diff --git a/src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt b/src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt index 580c3aa..b1893aa 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt @@ -162,4 +162,18 @@ open class TeamService( ) return jdbcDao.queryForList(sql.toString(), args) } + + open fun combo2(): List> { + val sql = StringBuilder("select" + + " t.teamLead as id," + + " t.name, t.code " + + " from team t" + + " where t.deleted = false " + ) + return jdbcDao.queryForList(sql.toString()) + } + + open fun getMyTeamForStaff(staff: Staff): Team? { + return teamRepository.findByStaff(staff) + } } \ No newline at end of file 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 dea71c7..06d9f5f 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 @@ -69,4 +69,9 @@ class DashboardController( } return result } + + @GetMapping("/searchFinancialSummary") + fun searchFinancialSummary(): List>{ + return dashboardService.searchFinancialSummary() + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt b/src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt index 871c268..bb00181 100644 --- a/src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt +++ b/src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt @@ -53,4 +53,10 @@ class TeamController(private val teamService: TeamService) { ) ) } + + @GetMapping("/combo2") + @Throws(ServletRequestBindingException::class) + fun combo2(): List> { + return teamService.combo2() + } } \ No newline at end of file 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 2f37049..41831ae 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 @@ -9,6 +9,9 @@ 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.entity.projections.MonthlyLeave +import com.ffii.tsms.modules.timesheet.entity.projections.ProjectMonthlyHoursWithDate +import com.ffii.tsms.modules.timesheet.entity.projections.TimesheetHours import com.ffii.tsms.modules.timesheet.web.models.LeaveEntry import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory @@ -22,6 +25,7 @@ import org.springframework.stereotype.Service import java.io.ByteArrayOutputStream import java.io.IOException import java.math.BigDecimal +import java.sql.Time import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.* @@ -44,9 +48,9 @@ open class ReportService( // ==============================|| GENERATE REPORT ||============================== // - fun genFinancialStatusReport(projectId: Long): ByteArray { + fun genFinancialStatusReport(teamLeadId: Long): ByteArray { - val financialStatus: List> = getFinancialStatus(projectId) + val financialStatus: List> = getFinancialStatus(teamLeadId) val otFactor = BigDecimal(1) @@ -55,10 +59,10 @@ open class ReportService( for (item in financialStatus){ val normalConsumed = item.getValue("normalConsumed") as Double val hourlyRate = item.getValue("hourlyRate") as BigDecimal - println("normalConsumed------------- $normalConsumed") - println("hourlyRate------------- $hourlyRate") +// println("normalConsumed------------- $normalConsumed") +// println("hourlyRate------------- $hourlyRate") val manHourRate = normalConsumed.toBigDecimal().multiply(hourlyRate) - println("manHourRate------------ $manHourRate") +// println("manHourRate------------ $manHourRate") val otConsumed = item.getValue("otConsumed") as Double val manOtHourRate = otConsumed.toBigDecimal().multiply(hourlyRate).multiply(otFactor) @@ -90,9 +94,9 @@ open class ReportService( } } - println("tempList---------------------- $tempList") +// println("tempList---------------------- $tempList") - val workbook: Workbook = createFinancialStatusReport(FINANCIAL_STATUS_REPORT, tempList) + val workbook: Workbook = createFinancialStatusReport(FINANCIAL_STATUS_REPORT, tempList, teamLeadId) val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) @@ -105,10 +109,11 @@ open class ReportService( fun generateProjectCashFlowReport( project: Project, invoices: List, - timesheets: List + timesheets: List, + dateType: String ): ByteArray { // Generate the Excel report with query results - val workbook: Workbook = createProjectCashFlowReport(project, invoices, timesheets, PROJECT_CASH_FLOW_REPORT) + val workbook: Workbook = createProjectCashFlowReport(project, invoices, timesheets, dateType, PROJECT_CASH_FLOW_REPORT) // Write the workbook to a ByteArrayOutputStream val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() @@ -122,9 +127,8 @@ open class ReportService( fun generateStaffMonthlyWorkHourAnalysisReport( month: LocalDate, staff: Staff, - timesheets: List, - leaves: List, - projectList: List + timesheets: List>, + leaves: List>, ): ByteArray { // Generate the Excel report with query results val workbook: Workbook = createStaffMonthlyWorkHourAnalysisReport( @@ -132,7 +136,6 @@ open class ReportService( staff, timesheets, leaves, - projectList, MONTHLY_WORK_HOURS_ANALYSIS_REPORT ) @@ -180,6 +183,7 @@ open class ReportService( private fun createFinancialStatusReport( templatePath: String, projects: List>, + teamLeadId: Long ) : Workbook { val resource = ClassPathResource(templatePath) @@ -190,14 +194,50 @@ open class ReportService( val sheet = workbook.getSheetAt(0) + //Set Column 2, 3, 4 to auto width + sheet.setColumnWidth(2, 20*256) + sheet.setColumnWidth(3, 45*256) + sheet.setColumnWidth(4, 15*256) + val boldFont = sheet.workbook.createFont() boldFont.bold = true val boldFontCellStyle = workbook.createCellStyle() boldFontCellStyle.setFont(boldFont) + var rowNum = 14 + if (projects.isEmpty()){ + // Fill the cell in Row 2-12 with thr calculated sum + rowNum = 1 + val row1: Row = sheet.getRow(rowNum) + val genDateCell = row1.createCell(2) + genDateCell.setCellValue(LocalDate.now().toString()) + + rowNum = 2 + val row2: Row = sheet.getRow(rowNum) + val row2Cell = row2.createCell(2) + val sql = StringBuilder("select" + + " t.teamLead as id," + + " t.name, t.code " + + " from team t" + + " where t.deleted = false " + + " and t.teamLead = :teamLead " + ) + val args = mapOf("teamLead" to teamLeadId) + val team = jdbcDao.queryForMap(sql.toString(), args).get() + val code = team["code"] + val name = team["name"] + row2Cell.apply { + setCellValue("$code - $name") + } - var rowNum = 14 + rowNum = 4 + val row4: Row = sheet.getRow(rowNum) + val row4Cell = row4.createCell(2) + row4Cell.setCellValue(projects.size.toString()) + + return workbook + } for(item in projects){ val row: Row = sheet.createRow(rowNum++) @@ -209,7 +249,10 @@ open class ReportService( descriptionCell.setCellValue(if (item["description"] != null) item.getValue("description").toString() else "N/A") val clientCell = row.createCell(2) - clientCell.setCellValue(if (item["client"] != null) item.getValue("client").toString() else "N/A") + clientCell.apply { + setCellValue(if (item["client"] != null) item.getValue("client").toString() else "N/A") + } + CellUtil.setAlignment(clientCell, HorizontalAlignment.CENTER) val teamLeadCell = row.createCell(3) teamLeadCell.setCellValue(if (item["teamLead"] != null) item.getValue("teamLead").toString() else "N/A") @@ -227,7 +270,6 @@ open class ReportService( cellStyle.dataFormat = accountingStyle } - val budgetCell = row.createCell(7) budgetCell.apply { cellFormula = "G${rowNum} * 80%" @@ -259,14 +301,14 @@ open class ReportService( val uninvoiceCell = row.createCell(11) uninvoiceCell.apply { - cellFormula = " IF(H${rowNum}, timesheets: List, + dateType: String, templatePath: String, ): Workbook { // please create a new function for each report template @@ -531,7 +586,7 @@ open class ReportService( rowIndex = 15 - val dateFormatter = DateTimeFormatter.ofPattern("MMM YYYY") + val dateFormatter = if (dateType == "Date") DateTimeFormatter.ofPattern("yyyy/MM/dd") else DateTimeFormatter.ofPattern("MMM YYYY") val combinedResults = (invoices.map { it.receiptDate } + timesheets.map { it.recordDate }).filterNotNull().sortedBy { it } .map { it.format(dateFormatter) }.distinct() @@ -675,21 +730,35 @@ open class ReportService( private fun createStaffMonthlyWorkHourAnalysisReport( month: LocalDate, staff: Staff, - timesheets: List, - leaves: List, - projectList: List, + timesheets: List>, + leaves: List>, templatePath: String, ): Workbook { -// val yearMonth = YearMonth.of(2022, 5) // May 2022 - println("t $timesheets") - println("l $leaves") - println("p $projectList") + var projectList: List = listOf() + println("----timesheets-----") + println(timesheets) + // result = timesheet record mapped + var result: Map = mapOf() + if (timesheets.isNotEmpty()) { + projectList = timesheets.map{ "${it["code"]}\n ${it["name"]}"}.toList() + result = timesheets.groupBy( + { it["id"].toString() }, + { mapOf( + "date" to it["recordDate"], + "normalConsumed" to it["normalConsumed"], + "otConsumed" to it["otConsumed"], + ) } + ) + } + println("---result---") + println(result) + println("l $projectList") val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") - val monthStyle = workbook.createDataFormat().getFormat("mmm, yyyy") + val monthStyle = workbook.createDataFormat().getFormat("MMM YYYY") val dateStyle = workbook.createDataFormat().getFormat("dd/mm/yyyy") val boldStyle = workbook.createCellStyle() @@ -705,8 +774,6 @@ open class ReportService( val sheet: Sheet = workbook.getSheetAt(0) -// sheet.forceFormulaRecalculation = true; //Calculate formulas - var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field var columnIndex = 1 @@ -723,7 +790,6 @@ open class ReportService( rowIndex = 2 sheet.getRow(rowIndex).getCell(columnIndex).apply { setCellValue(month) -// cellStyle.setFont(boldStyle) cellStyle.dataFormat = monthStyle } @@ -758,11 +824,9 @@ open class ReportService( tempCell.setCellValue(dayInfo.date) tempCell.cellStyle = boldStyle tempCell.cellStyle.dataFormat = dateStyle -// cellStyle.alignment = HorizontalAlignment.LEFT tempCell = sheet.getRow(rowIndex).createCell(1) tempCell.setCellValue(dayInfo.weekday) tempCell.cellStyle = boldStyle -// cellStyle.alignment = HorizontalAlignment.LEFT } rowIndex += 1 @@ -784,10 +848,11 @@ open class ReportService( var normalConsumed = 0.0 var otConsumed = 0.0 var leaveHours = 0.0 + // normalConsumed data if (timesheets.isNotEmpty()) { timesheets.forEach { t -> - normalConsumed += t.normalConsumed!! - otConsumed += t.otConsumed ?: 0.0 + normalConsumed += t["normalConsumed"] as Double + otConsumed += t["otConsumed"] as Double } } tempCell = sheet.getRow(rowIndex).createCell(2) @@ -815,9 +880,10 @@ open class ReportService( tempCell.cellStyle = boldStyle CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + // cal total leave hour if (leaves.isNotEmpty()) { leaves.forEach { l -> - leaveHours += l.leaveHours!! + leaveHours += l["leaveHours"] as Double } } tempCell = sheet.getRow(rowIndex).createCell(2) @@ -862,28 +928,30 @@ open class ReportService( tempCell.setCellValue(0.0) tempCell.cellStyle.dataFormat = accountingStyle } - timesheets.forEach { timesheet -> - dayInt = timesheet.recordDate!!.dayOfMonth - tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) - tempCell.setCellValue(timesheet.normalConsumed!!) - + result.forEach{(id, list) -> + for (i in 0 until id.toInt()) { + val temp: List> = list as List> + temp.forEachIndexed { i, _ -> + dayInt = temp[i]["date"].toString().toInt() + tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) + tempCell.setCellValue(temp[i]["normalConsumed"] as Double) + } + } } columnIndex++ } } - // dates + // leave hours data if (leaves.isNotEmpty()) { leaves.forEach { leave -> for (i in 0 until rowSize) { tempCell = sheet.getRow(8 + i).createCell(columnIndex) tempCell.setCellValue(0.0) tempCell.cellStyle.dataFormat = accountingStyle - } - dayInt = leave.recordDate!!.dayOfMonth + dayInt = leave["recordDate"].toString().toInt() tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) - tempCell.setCellValue(leave.leaveHours!!) - + tempCell.setCellValue(leave["leaveHours"] as Double) } } ///////////////////////////////////////////////////////// Leave Hours //////////////////////////////////////////////////////////////////// @@ -1040,33 +1108,73 @@ open class ReportService( return workbook } - open fun getFinancialStatus(projectId: Long?): List> { + open fun getFinancialStatus(teamLeadId: Long?): List> { val sql = StringBuilder( - " with cte_invoice as (select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" + " with cte_timesheet as (" + + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.hourlyRate" + + " 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 team t2 on t2.id = s.teamId" + + " )," + + " 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" + + " )" + + " select p.code, p.description, c.name as client, concat(t.code, \' - \', t.name) as teamLead, p.planStart , p.planEnd , p.expectedTotalFee," + + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed," + + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount" + + " from project p" + + " left join cte_timesheet cte_ts on p.code = cte_ts.code" + " left join customer c on c.id = p.customerId" - + " left join team t2 on t2.id = s.teamId" + + " left join tsmsdb.team t on t.teamLead = p.teamLead" + " left join cte_invoice cte_i on cte_i.code = p.code" + + " where p.status = \'On-going\'" ) - - if (projectId!! > 0) { - sql.append(" where p.id = :projectId ") + if (teamLeadId!! > 0) { + sql.append(" and p.teamLead = :teamLeadId ") } sql.append(" order by p.code") - val args = mapOf("projectId" to projectId) + val args = mapOf("teamLeadId" to teamLeadId) return jdbcDao.queryForList(sql.toString(), args) } -} \ No newline at end of file + open fun getTimesheet(args: Map): List> { + val sql = StringBuilder( + "SELECT" + + " p.id," + + " p.name," + + " p.code," + + " CAST(DATE_FORMAT(t.recordDate, '%d') AS SIGNED) AS recordDate," + + " sum(t.normalConsumed) as normalConsumed," + + " IFNULL(sum(t.otConsumed), 0.0) as otConsumed" + + " from timesheet t" + + " left join project_task pt on t.projectTaskId = pt.id" + + " left join project p on p.id = pt.project_id" + + " where t.staffId = :staffId" + + " group by p.id, t.recordDate" + + " order by p.id, t.recordDate" + + " and t.recordDate BETWEEN :startDate and :endDate" + ) + return jdbcDao.queryForList(sql.toString(), args) + } + open fun getLeaves(args: Map): List> { + val sql = StringBuilder( + " SELECT " + + " sum(leaveHours) as leaveHours, " + + " CAST(DATE_FORMAT(recordDate, '%d') AS SIGNED) AS recordDate " + + " from `leave` " + + " where staffId = :staffId " + + " and recordDate BETWEEN :startDate and :endDate " + + " group by recordDate " + + " order by recordDate " + ) + return jdbcDao.queryForList(sql.toString(), args) + } + +} 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 50fb74c..4ff3f7a 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 @@ -13,6 +13,7 @@ import com.ffii.tsms.modules.report.web.model.LateStartReportRequest import com.ffii.tsms.modules.project.entity.ProjectRepository import com.ffii.tsms.modules.timesheet.entity.LeaveRepository import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository +import com.ffii.tsms.modules.timesheet.entity.projections.ProjectMonthlyHoursWithDate import jakarta.validation.Valid import org.springframework.core.io.ByteArrayResource import org.springframework.core.io.Resource @@ -57,8 +58,8 @@ class ReportController( @Throws(ServletRequestBindingException::class, IOException::class) fun getFinancialStatusReport(@RequestBody @Valid request: FinancialStatusReportRequest): ResponseEntity { - - val reportResult: ByteArray = excelReportService.genFinancialStatusReport(request.projectId) + println(request.teamLeadId) + val reportResult: ByteArray = excelReportService.genFinancialStatusReport(request.teamLeadId) return ResponseEntity.ok() .header("filename", "Financial Status Report - " + LocalDate.now() + ".xlsx") @@ -74,7 +75,7 @@ class ReportController( val invoices = invoiceService.findAllByProjectAndPaidAmountIsNotNull(project) val timesheets = timesheetRepository.findAllByProjectTaskIn(projectTasks) - val reportResult: ByteArray = excelReportService.generateProjectCashFlowReport(project, invoices, timesheets) + val reportResult: ByteArray = excelReportService.generateProjectCashFlowReport(project, invoices, timesheets, request.dateType) // val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") return ResponseEntity.ok() // .contentType(mediaType) @@ -89,14 +90,15 @@ class ReportController( val nextMonth = request.yearMonth.plusMonths(1).atDay(1) val staff = staffRepository.findById(request.id).orElseThrow() - val timesheets = timesheetRepository.findByStaffAndRecordDateBetweenOrderByRecordDate(staff, thisMonth, nextMonth) - val leaves = leaveRepository.findByStaffAndRecordDateBetweenOrderByRecordDate(staff, thisMonth, nextMonth) - - val projects = timesheetRepository.findDistinctProjectTaskByStaffAndRecordDateBetweenOrderByRecordDate(staff, thisMonth, nextMonth) - val projectList: List = projects.map { p -> "${p.projectTask!!.project!!.code}\n ${p.projectTask!!.project!!.name}" } - - - val reportResult: ByteArray = excelReportService.generateStaffMonthlyWorkHourAnalysisReport(thisMonth, staff, timesheets, leaves, projectList) + val args: Map = mutableMapOf( + "staffId" to request.id, + "startDate" to thisMonth, + "endDate" to nextMonth, + ) + val timesheets= excelReportService.getTimesheet(args) + val leaves= excelReportService.getLeaves(args) + + val reportResult: ByteArray = excelReportService.generateStaffMonthlyWorkHourAnalysisReport(thisMonth, staff, timesheets, leaves) // val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") return ResponseEntity.ok() // .contentType(mediaType) @@ -166,7 +168,7 @@ fun downloadLateStartReport(@RequestBody @Valid request: LateStartReportRequest) @GetMapping("/financialReport/{id}") fun getFinancialReport(@PathVariable id: Long): List> { - println(excelReportService.genFinancialStatusReport(id)) +// println(excelReportService.genFinancialStatusReport(id)) return excelReportService.getFinancialStatus(id) } diff --git a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt index ee971e7..8a85bc5 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt @@ -4,10 +4,11 @@ import java.time.LocalDate import java.time.YearMonth data class FinancialStatusReportRequest ( - val projectId: Long + val teamLeadId: Long ) data class ProjectCashFlowReportRequest ( - val projectId: Long + val projectId: Long, + val dateType: String, // "Date", "Month" ) data class StaffMonthlyWorkHourAnalysisReportRequest ( diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/entity/projections/MonthlyHours.kt b/src/main/java/com/ffii/tsms/modules/timesheet/entity/projections/MonthlyHours.kt index ea5f40a..1509171 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/entity/projections/MonthlyHours.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/entity/projections/MonthlyHours.kt @@ -2,7 +2,16 @@ package com.ffii.tsms.modules.timesheet.entity.projections import java.time.LocalDate -data class MonthlyHours( +data class MonthlyLeave( val date: LocalDate, - val nomralConsumed: Number + val leaveHours: Double ) + +data class ProjectMonthlyHoursWithDate( + val id: Long, + val name: String, + val code: String, + val date: LocalDate, + val normalConsumed: Double, + val otConsumed: Double, + ) \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt index 04903af..1324c4b 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt @@ -2,16 +2,19 @@ package com.ffii.tsms.modules.timesheet.service import com.ffii.core.exception.BadRequestException import com.ffii.tsms.modules.data.service.StaffsService +import com.ffii.tsms.modules.data.service.TeamService import com.ffii.tsms.modules.project.entity.ProjectRepository import com.ffii.tsms.modules.project.entity.ProjectTaskRepository import com.ffii.tsms.modules.project.entity.TaskRepository import com.ffii.tsms.modules.timesheet.entity.Timesheet import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository +import com.ffii.tsms.modules.timesheet.web.models.TeamMemberTimeEntries import com.ffii.tsms.modules.timesheet.web.models.TimeEntry import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate import java.time.format.DateTimeFormatter +import kotlin.jvm.optionals.getOrDefault import kotlin.jvm.optionals.getOrNull @Service @@ -20,7 +23,8 @@ open class TimesheetsService( private val projectTaskRepository: ProjectTaskRepository, private val projectRepository: ProjectRepository, private val taskRepository: TaskRepository, - private val staffsService: StaffsService + private val staffsService: StaffsService, + private val teamService: TeamService ) { @Transactional open fun saveTimesheet(recordTimeEntry: Map>): Map> { @@ -52,12 +56,56 @@ open class TimesheetsService( return transformToTimeEntryMap(savedTimesheets) } + @Transactional + open fun saveMemberTimeEntry(staffId: Long, entry: TimeEntry, recordDate: LocalDate?): Map> { + val currentStaff = staffsService.currentStaff() ?: throw BadRequestException() + // Make sure current staff is a team lead + teamService.getMyTeamForStaff(currentStaff) ?: throw BadRequestException() + + val memberStaff = staffsService.getStaff(staffId) + + val timesheet = timesheetRepository.findById(entry.id).getOrDefault(Timesheet()).apply { + val task = entry.taskId?.let { taskRepository.findById(it).getOrNull() } + val project = entry.projectId?.let { projectRepository.findById(it).getOrNull() } + val projectTask = project?.let { p -> task?.let { t -> projectTaskRepository.findByProjectAndTask(p, t) } } + + this.normalConsumed = entry.inputHours + this.otConsumed = entry.otHours + this.projectTask = projectTask + this.remark = entry.remark + this.recordDate = this.recordDate ?: recordDate + this.staff = this.staff ?: memberStaff + } + + timesheetRepository.save(timesheet) + return transformToTimeEntryMap(timesheetRepository.findAllByStaff(memberStaff)) + } + open fun getTimesheet(): Map> { // Need to be associated with a staff val currentStaff = staffsService.currentStaff() ?: return emptyMap() return transformToTimeEntryMap(timesheetRepository.findAllByStaff(currentStaff)) } + open fun getTimeMemberTimesheet(): Map { + val currentStaff = staffsService.currentStaff() ?: return emptyMap() + // Get team where current staff is team lead + val myTeam = teamService.getMyTeamForStaff(currentStaff) ?: return emptyMap() + + val teamMembers = staffsService.findAllByTeamId(myTeam.id!!).getOrDefault(emptyList()) + + return teamMembers.associate { member -> + Pair( + member.id!!, + TeamMemberTimeEntries( + staffId = member.staffId, + name = member.name, + timeEntries = transformToTimeEntryMap(timesheetRepository.findAllByStaff(member)) + ) + ) + } + } + private fun transformToTimeEntryMap(timesheets: List): Map> { return timesheets .groupBy { timesheet -> timesheet.recordDate!!.format(DateTimeFormatter.ISO_LOCAL_DATE) } diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt b/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt index 02f8f31..be9bbcc 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt @@ -5,6 +5,8 @@ import com.ffii.tsms.modules.timesheet.entity.LeaveType import com.ffii.tsms.modules.timesheet.service.LeaveService import com.ffii.tsms.modules.timesheet.service.TimesheetsService import com.ffii.tsms.modules.timesheet.web.models.LeaveEntry +import com.ffii.tsms.modules.timesheet.web.models.TeamMemberTimeEntries +import com.ffii.tsms.modules.timesheet.web.models.TeamTimeEntry import com.ffii.tsms.modules.timesheet.web.models.TimeEntry import jakarta.validation.Valid import org.springframework.web.bind.annotation.GetMapping @@ -43,6 +45,18 @@ class TimesheetsController(private val timesheetsService: TimesheetsService, pri return timesheetsService.getTimesheet() } + @GetMapping("/teamTimesheets") + fun getTeamMemberTimesheetEntries(): Map { + return timesheetsService.getTimeMemberTimesheet() + } + + @PostMapping("/saveMemberEntry") + fun saveMemberEntry(@Valid @RequestBody request: TeamTimeEntry): Map> { + return timesheetsService.saveMemberTimeEntry(request.staffId, request.entry, runCatching { + LocalDate.parse(request.recordDate, DateTimeFormatter.ISO_LOCAL_DATE) + }.getOrNull()) + } + @GetMapping("/leaves") fun getLeaveEntry(): Map> { return leaveService.getLeaves() diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamMemberTimeEntries.kt b/src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamMemberTimeEntries.kt new file mode 100644 index 0000000..084440d --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamMemberTimeEntries.kt @@ -0,0 +1,7 @@ +package com.ffii.tsms.modules.timesheet.web.models + +data class TeamMemberTimeEntries( + val timeEntries: Map>, + val staffId: String?, + val name: String?, +) diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamTimeEntry.kt b/src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamTimeEntry.kt new file mode 100644 index 0000000..9976073 --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamTimeEntry.kt @@ -0,0 +1,7 @@ +package com.ffii.tsms.modules.timesheet.web.models + +data class TeamTimeEntry( + val staffId: Long, + val entry: TimeEntry, + val recordDate: String, +) diff --git a/src/main/java/com/ffii/tsms/modules/user/req/UpdateUserReq.java b/src/main/java/com/ffii/tsms/modules/user/req/UpdateUserReq.java index 87838be..853f04b 100644 --- a/src/main/java/com/ffii/tsms/modules/user/req/UpdateUserReq.java +++ b/src/main/java/com/ffii/tsms/modules/user/req/UpdateUserReq.java @@ -24,7 +24,7 @@ public class UpdateUserReq { private String locale; private String remarks; - @NotBlank +// @NotBlank @Email private String email; // @NotBlank diff --git a/src/main/java/com/ffii/tsms/modules/user/web/UserController.java b/src/main/java/com/ffii/tsms/modules/user/web/UserController.java index d939da1..bf27805 100644 --- a/src/main/java/com/ffii/tsms/modules/user/web/UserController.java +++ b/src/main/java/com/ffii/tsms/modules/user/web/UserController.java @@ -153,19 +153,10 @@ public class UserController{ @PatchMapping("/admin-change-password") @ResponseStatus(HttpStatus.NO_CONTENT) @PreAuthorize("hasAuthority('MAINTAIN_USER')") - public void adminChangePassword(@RequestBody @Valid ChangePwdReq req) { + public void adminChangePassword(@RequestBody @Valid AdminChangePwdReq req) { long id = req.getId(); User instance = userService.find(id).orElseThrow(NotFoundException::new); - logger.info("TEST req: "+req.getPassword()); - logger.info("TEST instance: "+instance.getPassword()); -// if (!passwordEncoder.matches(req.getPassword(), instance.getPassword())) { -// throw new BadRequestException(); -// } - PasswordRule rule = new PasswordRule(settingsService); - if (!PasswordUtils.checkPwd(req.getNewPassword(), rule)) { - throw new UnprocessableEntityException(ErrorCodes.USER_WRONG_NEW_PWD); - } instance.setPassword(passwordEncoder.encode(req.getNewPassword())); userService.save(instance); } @@ -188,6 +179,20 @@ public class UserController{ return new PasswordRule(settingsService); } + public static class AdminChangePwdReq { + private Long id; + @NotBlank + private String newPassword; + + public Long getId() { return id; } + public Long setId(Long id) { return this.id = id; } + public String getNewPassword() { + return newPassword; + } + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } + } public static class ChangePwdReq { private Long id; @NotBlank diff --git a/src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx b/src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx index 001d546..08129d7 100644 Binary files a/src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx and b/src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx differ diff --git a/src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx b/src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx index d2e48b6..0f63db7 100644 Binary files a/src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx and b/src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx differ