瀏覽代碼

Merge branch 'master' of https://git.2fi-solutions.com/davidhui/TSMS-backend

tags/Baseline_30082024_BACKEND_UAT
leoho2fi 1 年之前
父節點
當前提交
d6636c72cb
共有 19 個檔案被更改,包括 428 行新增114 行删除
  1. +5
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/StaffSkillsetRepository.java
  2. +2
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java
  3. +89
    -3
      src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt
  4. +14
    -19
      src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt
  5. +14
    -0
      src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt
  6. +5
    -0
      src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt
  7. +6
    -0
      src/main/java/com/ffii/tsms/modules/data/web/TeamController.kt
  8. +172
    -64
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  9. +14
    -12
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
  10. +3
    -2
      src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt
  11. +11
    -2
      src/main/java/com/ffii/tsms/modules/timesheet/entity/projections/MonthlyHours.kt
  12. +49
    -1
      src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt
  13. +14
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt
  14. +7
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamMemberTimeEntries.kt
  15. +7
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamTimeEntry.kt
  16. +1
    -1
      src/main/java/com/ffii/tsms/modules/user/req/UpdateUserReq.java
  17. +15
    -10
      src/main/java/com/ffii/tsms/modules/user/web/UserController.java
  18. 二進制
      src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx
  19. 二進制
      src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx

+ 5
- 0
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<StaffSkillset, Long> {

List<StaffSkillset> findByStaff(@Param("staff") Staff staff);
}

+ 2
- 0
src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java 查看文件

@@ -7,4 +7,6 @@ import java.util.List;

public interface TeamRepository extends AbstractRepository<Team, Long> {
List<Team> findByDeletedFalse();

Team findByStaff(Staff staff);
}

+ 89
- 3
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<String, Any>): List<Map<String, Any>> {
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<String, Any>): List<Map<String, Any>> {
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<String, Any>): List<Map<String, Any>> {
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<Map<String, Any>> {
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<Map<String, Any>> {
val financialStatus: List<Map<String, Any>> = getFinancialStatus()

val otFactor = BigDecimal(1)

val tempList = mutableListOf<Map<String, Any>>()

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
}
}



+ 14
- 19
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<StaffSearchInfo> = 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<MutableMap<String, Any>>
// if (!req.skillSetId.isNullOrEmpty()) {
// skillBatchInsertValues = req.skillSetId.stream()
// .map { skillId -> mutableMapOf<String, Any>(("staffId" to staff.id) as Pair<String, Any>, "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<String, Any>("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
}


+ 14
- 0
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<Map<String, Any>> {
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)
}
}

+ 5
- 0
src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt 查看文件

@@ -69,4 +69,9 @@ class DashboardController(
}
return result
}

@GetMapping("/searchFinancialSummary")
fun searchFinancialSummary(): List<Map<String, Any>>{
return dashboardService.searchFinancialSummary()
}
}

+ 6
- 0
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<Map<String, Any>> {
return teamService.combo2()
}
}

+ 172
- 64
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<Map<String, Any>> = getFinancialStatus(projectId)
val financialStatus: List<Map<String, Any>> = 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<Invoice>,
timesheets: List<Timesheet>
timesheets: List<Timesheet>,
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<Timesheet>,
leaves: List<Leave>,
projectList: List<String>
timesheets: List<Map<String, Any>>,
leaves: List<Map<String, Any>>,
): 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<Map<String, Any>>,
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}<I${rowNum},H${rowNum}-K${rowNum},I${rowNum}-K${rowNum}) "
cellFormula = " IF(H${rowNum}<=I${rowNum}, H${rowNum}-K${rowNum}, IF(AND(H${rowNum}>I${rowNum}, I${rowNum}<K${rowNum}), 0, IF(AND(H${rowNum}>I${rowNum}, I${rowNum}>=K${rowNum}), I${rowNum}-K${rowNum}, 0))) "
cellStyle = boldFontCellStyle
cellStyle.dataFormat = accountingStyle
}

val cpiCell = row.createCell(12)
cpiCell.apply {
cellFormula = "K${rowNum}/I${rowNum}"
cellFormula = "IF(K${rowNum} = 0, 0, K${rowNum}/I${rowNum})"
}

val receivedAmountCell = row.createCell(13)
@@ -276,15 +318,14 @@ open class ReportService(
cellStyle.dataFormat = accountingStyle
}


val unsettledAmountCell = row.createCell(14)
unsettledAmountCell.apply {
cellFormula = "K${rowNum}-N${rowNum}"
cellStyle = boldFontCellStyle
cellStyle.dataFormat = accountingStyle
}

}
// Last row calculate the sum
val lastRowNum = rowNum + 1

val row: Row = sheet.createRow(rowNum)
@@ -373,14 +414,27 @@ open class ReportService(
CellUtil.setCellStyleProperty(sumUnSettleCell,"borderTop", BorderStyle.THIN)
CellUtil.setCellStyleProperty(sumUnSettleCell,"borderBottom", BorderStyle.DOUBLE)

// 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)
if (teamLeadId < 0) {
row2Cell.setCellValue("All")
}else{
row2Cell.apply {
cellFormula = "D15"
}
}

rowNum = 4
val row4: Row = sheet.getRow(rowNum)
val row4Cell = row4.createCell(2)
row4Cell.setCellValue(projects.size.toString())

rowNum = 5
val row5: Row = sheet.getRow(rowNum)
@@ -446,6 +500,7 @@ open class ReportService(
project: Project,
invoices: List<Invoice>,
timesheets: List<Timesheet>,
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<Timesheet>,
leaves: List<Leave>,
projectList: List<String>,
timesheets: List<Map<String, Any>>,
leaves: List<Map<String, Any>>,
templatePath: String,
): Workbook {
// val yearMonth = YearMonth.of(2022, 5) // May 2022
println("t $timesheets")
println("l $leaves")
println("p $projectList")
var projectList: List<String> = listOf()
println("----timesheets-----")
println(timesheets)
// result = timesheet record mapped
var result: Map<String, Any> = 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<Map<String, Any>> = list as List<Map<String, Any>>
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<Map<String, Any>> {
open fun getFinancialStatus(teamLeadId: Long?): List<Map<String, Any>> {
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)
}

}
open fun getTimesheet(args: Map<String, Any>): List<Map<String, Any>> {
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<String, Any>): List<Map<String, Any>> {
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)
}

}

+ 14
- 12
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<Resource> {

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<String> = 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<String, Any> = 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<Map<String, Any>> {
println(excelReportService.genFinancialStatusReport(id))
// println(excelReportService.genFinancialStatusReport(id))
return excelReportService.getFinancialStatus(id)
}


+ 3
- 2
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 (


+ 11
- 2
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,
)

+ 49
- 1
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<LocalDate, List<TimeEntry>>): Map<String, List<TimeEntry>> {
@@ -52,12 +56,56 @@ open class TimesheetsService(
return transformToTimeEntryMap(savedTimesheets)
}

@Transactional
open fun saveMemberTimeEntry(staffId: Long, entry: TimeEntry, recordDate: LocalDate?): Map<String, List<TimeEntry>> {
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<String, List<TimeEntry>> {
// Need to be associated with a staff
val currentStaff = staffsService.currentStaff() ?: return emptyMap()
return transformToTimeEntryMap(timesheetRepository.findAllByStaff(currentStaff))
}

open fun getTimeMemberTimesheet(): Map<Long, TeamMemberTimeEntries> {
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<Timesheet>): Map<String, List<TimeEntry>> {
return timesheets
.groupBy { timesheet -> timesheet.recordDate!!.format(DateTimeFormatter.ISO_LOCAL_DATE) }


+ 14
- 0
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<Long, TeamMemberTimeEntries> {
return timesheetsService.getTimeMemberTimesheet()
}

@PostMapping("/saveMemberEntry")
fun saveMemberEntry(@Valid @RequestBody request: TeamTimeEntry): Map<String, List<TimeEntry>> {
return timesheetsService.saveMemberTimeEntry(request.staffId, request.entry, runCatching {
LocalDate.parse(request.recordDate, DateTimeFormatter.ISO_LOCAL_DATE)
}.getOrNull())
}

@GetMapping("/leaves")
fun getLeaveEntry(): Map<String, List<LeaveEntry>> {
return leaveService.getLeaves()


+ 7
- 0
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<String, List<TimeEntry>>,
val staffId: String?,
val name: String?,
)

+ 7
- 0
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,
)

+ 1
- 1
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


+ 15
- 10
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


二進制
src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx 查看文件


二進制
src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx 查看文件


Loading…
取消
儲存