ソースを参照

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

tags/Baseline_30082024_BACKEND_UAT
leoho2fi 1年前
コミット
be52f71a39
19個のファイルの変更261行の追加57行の削除
  1. +15
    -1
      src/main/java/com/ffii/tsms/config/security/jwt/web/JwtAuthenticationController.java
  2. +10
    -2
      src/main/java/com/ffii/tsms/model/JwtResponse.java
  3. +2
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java
  4. +69
    -10
      src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt
  5. +20
    -0
      src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt
  6. +9
    -6
      src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt
  7. +62
    -30
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  8. +10
    -4
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
  9. +3
    -2
      src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt
  10. +11
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt
  11. +11
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt
  12. +10
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt
  13. +6
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamEntryDelete.kt
  14. +3
    -2
      src/main/java/com/ffii/tsms/modules/user/entity/UserRepository.java
  15. +12
    -0
      src/main/java/com/ffii/tsms/modules/user/service/GroupService.java
  16. +8
    -0
      src/main/resources/db/changelog/changes/20240529_01_cyril/01_insert_authority.sql
  17. バイナリ
      src/main/resources/templates/report/AR02_Delay Report v02.xlsx
  18. バイナリ
      src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx
  19. バイナリ
      src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx

+ 15
- 1
src/main/java/com/ffii/tsms/config/security/jwt/web/JwtAuthenticationController.java ファイルの表示

@@ -2,8 +2,12 @@ package com.ffii.tsms.config.security.jwt.web;


import java.time.Instant; import java.time.Instant;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
import java.util.Set; import java.util.Set;


import com.ffii.tsms.modules.data.entity.Staff;
import com.ffii.tsms.modules.data.entity.StaffRepository;
import com.ffii.tsms.modules.user.service.GroupService;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
@@ -51,6 +55,12 @@ public class JwtAuthenticationController {
@Autowired @Autowired
private JwtUserDetailsService userDetailsService; private JwtUserDetailsService userDetailsService;


@Autowired
private GroupService groupService;

@Autowired
private StaffRepository staffRepository;

@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;


@@ -90,10 +100,14 @@ public class JwtAuthenticationController {
final String accessToken = jwtTokenUtil.generateToken(user); final String accessToken = jwtTokenUtil.generateToken(user);
final String refreshToken = jwtTokenUtil.createRefreshToken(user.getUsername()).getToken(); final String refreshToken = jwtTokenUtil.createRefreshToken(user.getUsername()).getToken();


final Map<String, Object> args = Map.of("userId", user.getId());
final String role = groupService.getGroupName(args);
final Staff staff = staffRepository.findIdAndNameByUserIdAndDeletedFalse(user.getId()).orElse(null);

Set<AbilityModel> abilities = new HashSet<>(); Set<AbilityModel> abilities = new HashSet<>();
userAuthorityService.getUserAuthority(user).forEach(auth -> abilities.add(new AbilityModel(auth.getAuthority()))); userAuthorityService.getUserAuthority(user).forEach(auth -> abilities.add(new AbilityModel(auth.getAuthority())));


return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken, null, user, abilities));
return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken, role, user, abilities, staff));
} }


@PostMapping("/refresh-token") @PostMapping("/refresh-token")


+ 10
- 2
src/main/java/com/ffii/tsms/model/JwtResponse.java ファイルの表示

@@ -3,6 +3,7 @@ package com.ffii.tsms.model;
import java.io.Serializable; import java.io.Serializable;
import java.util.Set; import java.util.Set;


import com.ffii.tsms.modules.data.entity.Staff;
import com.ffii.tsms.modules.user.entity.User; import com.ffii.tsms.modules.user.entity.User;


public class JwtResponse implements Serializable { public class JwtResponse implements Serializable {
@@ -15,8 +16,11 @@ public class JwtResponse implements Serializable {
private final String refreshToken; private final String refreshToken;
private final String role; private final String role;
private final Set<AbilityModel> abilities; private final Set<AbilityModel> abilities;
private final Staff staff;


public JwtResponse(String accessToken, String refreshToken, String role, User user, Set<AbilityModel> abilities) {


public JwtResponse(String accessToken, String refreshToken, String role, User user, Set<AbilityModel> abilities, Staff staff) {
this.accessToken = accessToken; this.accessToken = accessToken;
this.refreshToken = refreshToken; this.refreshToken = refreshToken;
this.role = role; this.role = role;
@@ -24,7 +28,8 @@ public class JwtResponse implements Serializable {
this.name = user.getName(); this.name = user.getName();
this.email = user.getEmail(); this.email = user.getEmail();
this.abilities = abilities; this.abilities = abilities;
}
this.staff = staff;
}


public String getAccessToken() { public String getAccessToken() {
return this.accessToken; return this.accessToken;
@@ -50,6 +55,9 @@ public class JwtResponse implements Serializable {
return email; return email;
} }


public Staff getStaff() { return staff; }


public Set<AbilityModel> getAbilities() { public Set<AbilityModel> getAbilities() {
return abilities; return abilities;
} }

+ 2
- 0
src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java ファイルの表示

@@ -21,4 +21,6 @@ public interface StaffRepository extends AbstractRepository<Staff, Long> {


Optional<Staff> findByUserId(@Param("userId") Long userId); Optional<Staff> findByUserId(@Param("userId") Long userId);
Optional<List<Staff>> findAllByTeamIdAndDeletedFalse(Long id); Optional<List<Staff>> findAllByTeamIdAndDeletedFalse(Long id);

Optional<Staff> findIdAndNameByUserIdAndDeletedFalse(Long id);
} }

+ 69
- 10
src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt ファイルの表示

@@ -110,8 +110,8 @@ open class DashboardService(
+ " s.name as teamLead," + " s.name as teamLead,"
+ " tg.name as expectedStage," + " tg.name as expectedStage,"
+ " p.totalManhour as budgetedManhour," + " p.totalManhour as budgetedManhour,"
+ " sum(t.normalConsumed) + sum(t.otConsumed) as spentManhour,"
+ " p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed) as remainedManhour,"
+ " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour,"
+ " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour,"
+ " coalesce (round(((sum(t.normalConsumed) - sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage," + " coalesce (round(((sum(t.normalConsumed) - sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage,"
+ " DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d') as comingPaymentMilestone" + " DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d') as comingPaymentMilestone"
+ " from project p" + " from project p"
@@ -254,8 +254,8 @@ open class DashboardService(
+ " s.name as teamLead," + " s.name as teamLead,"
+ " tg.name as expectedStage," + " tg.name as expectedStage,"
+ " p.totalManhour as budgetedManhour," + " p.totalManhour as budgetedManhour,"
+ " sum(t.normalConsumed) + sum(t.otConsumed) as spentManhour,"
+ " p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed) as remainedManhour,"
+ " COALESCE (sum(t.normalConsumed) + sum(t.otConsumed),0) as spentManhour,"
+ " COALESCE (p.totalManhour - sum(t.normalConsumed) - sum(t.otConsumed),0) as remainedManhour,"
+ " coalesce (round(((sum(t.normalConsumed) - sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage," + " coalesce (round(((sum(t.normalConsumed) - sum(t.otConsumed))/p.totalManhour)*100,2),0) as manhourConsumptionPercentage,"
+ " DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d') as comingPaymentMilestone" + " DATE_FORMAT(milestonePayment.comingPaymentMilestone, '%Y-%m-%d') as comingPaymentMilestone"
+ " from project p" + " from project p"
@@ -301,7 +301,18 @@ open class DashboardService(
+ " coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) as cpi" + " coalesce(round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2),0) as cpi"
+ " from team t" + " from team t"
+ " left join project p on t.teamLead = p.teamLead" + " left join project p on t.teamLead = p.teamLead"
+ " left join invoice i on p.code = i.projectCode"
+ " left join ("
+ " select"
+ " t3.id as tid,"
+ " sum(i3.issueAmount) as issueAmount,"
+ " sum(i3.paidAmount) as paidAmount"
+ " from team t3"
+ " left join project p3 on t3.teamLead = p3.teamLead"
+ " left join invoice i3 on p3.code = i3.projectCode"
+ " where t3.deleted = 0"
+ " and p3.status = 'On-going'"
+ " group by t3.id"
+ " ) as i on i.tid = t.id"
+ " left join (" + " left join ("
+ " select" + " select"
+ " r.teamId as teamId," + " r.teamId as teamId,"
@@ -347,7 +358,17 @@ open class DashboardService(
+ " end as cashFlowStatus," + " end as cashFlowStatus,"
+ " round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) as cpi" + " round(sum(i.issueAmount) / (expenditure.cumulativeExpenditure),2) as cpi"
+ " from project p" + " from project p"
+ " left join invoice i on p.code = i.projectCode"
+ " left join ("
+ " select"
+ " p3.id as pid,"
+ " sum(i3.issueAmount) as issueAmount,"
+ " sum(i3.paidAmount) as paidAmount"
+ " from project p3"
+ " left join invoice i3 on p3.code = i3.projectCode"
+ " where p3.deleted = 0"
+ " and p3.status = 'On-going'"
+ " group by p3.id"
+ " ) as i on i.pid = p.id"
+ " left join (" + " left join ("
+ " select" + " select"
+ " sum(r.cumulativeExpenditure) as cumulativeExpenditure" + " sum(r.cumulativeExpenditure) as cumulativeExpenditure"
@@ -399,6 +420,7 @@ open class DashboardService(
+ " left join customer c on p.customerId = c.id" + " left join customer c on p.customerId = c.id"
+ " left join (" + " left join ("
+ " select" + " select"
+ " t3.id as tid,"
+ " c3.id as cid," + " c3.id as cid,"
+ " sum(i3.issueAmount) as issueAmount," + " sum(i3.issueAmount) as issueAmount,"
+ " sum(i3.paidAmount) as paidAmount" + " sum(i3.paidAmount) as paidAmount"
@@ -415,8 +437,8 @@ open class DashboardService(
if (args.containsKey("teamId")) if (args.containsKey("teamId"))
sql.append(" AND t3.id = :teamId"); sql.append(" AND t3.id = :teamId");
} }
sql.append( " group by c3.id"
+ " ) as i on i.cid = c.id"
sql.append( " group by c3.id, t3.id"
+ " ) as i on i.cid = c.id and i.tid = t.id"
+ " left join (" + " left join ("
+ " select" + " select"
+ " r.teamId as teamId," + " r.teamId as teamId,"
@@ -657,7 +679,17 @@ open class DashboardService(
+ " coalesce (expenditure.expenditure,0) as totalExpenditure," + " coalesce (expenditure.expenditure,0) as totalExpenditure,"
+ " coalesce (sum(p.expectedTotalFee)*0.8 - expenditure.expenditure,0) as expenditureReceivable" + " coalesce (sum(p.expectedTotalFee)*0.8 - expenditure.expenditure,0) as expenditureReceivable"
+ " from project p" + " from project p"
+ " left join invoice i on p.code = i.projectCode"
+ " left join ("
+ " select"
+ " p3.id as pid,"
+ " sum(i3.issueAmount) as issueAmount,"
+ " sum(i3.paidAmount) as paidAmount"
+ " from project p3"
+ " left join invoice i3 on p3.code = i3.projectCode"
+ " where p3.deleted = 0"
+ " and p3.status = 'On-going'"
+ " group by p3.id"
+ " ) as i on i.pid = p.id"
+ " left join(" + " left join("
+ " select" + " select"
+ " sum(r.expenditure) as expenditure" + " sum(r.expenditure) as expenditure"
@@ -933,9 +965,12 @@ open class DashboardService(
+ " p.id as id," + " p.id as id,"
+ " p.code as projectCode," + " p.code as projectCode,"
+ " p.name as projectName," + " p.name as projectName,"
+ " concat(c.code,'-',c.name) as customerCodeAndName"
+ " concat(c.code,'-',c.name) as customerCodeAndName,"
+ " concat(c.code,'-',c.name) as customerCodeAndName,"
+ " concat(s.code,'-',s.name) as subsidiaryCodeAndName"
+ " from project p" + " from project p"
+ " left join customer c on p.customerId = c.id" + " left join customer c on p.customerId = c.id"
+ " left join subsidiary s on p.customerSubsidiaryId = s.id"
+ " where p.status = 'On-going'" + " where p.status = 'On-going'"
+ " and p.deleted = 0" + " and p.deleted = 0"
) )
@@ -1033,7 +1068,31 @@ open class DashboardService(


return jdbcDao.queryForList(sql.toString(), args) return jdbcDao.queryForList(sql.toString(), args)
} }
fun monthlyActualTeamTotalManhoursSpent(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder(
" WITH RECURSIVE date_series AS ("
+ " SELECT DATE_FORMAT(:startdate, '%Y-%m-01') AS month"
+ " UNION ALL"
+ " SELECT DATE_FORMAT(DATE_ADD(month, INTERVAL 1 MONTH), '%Y-%m-01')"
+ " FROM date_series"
+ " WHERE month < DATE_FORMAT(:enddate, '%Y-%m-01')"
+ " )"
+ " SELECT"
+ " ds.month AS yearMonth,"
+ " IFNULL(SUM(IFNULL(ts.normalConsumed, 0) + IFNULL(ts.otConsumed, 0)), 0) AS 'TotalManhourConsumed',"
+ " :teamId AS teamId"
+ " FROM date_series ds"
+ " LEFT JOIN staff st"
+ " on st.teamId = :teamId"
+ " LEFT JOIN timesheet ts"
+ " ON DATE_FORMAT(ts.recordDate, '%Y-%m') = DATE_FORMAT(ds.month, '%Y-%m') and ts.staffID = st.id"
+ " WHERE ds.month BETWEEN DATE_FORMAT(:startdate, '%Y-%m-01') AND DATE_FORMAT(:enddate, '%Y-%m-01')"
+ " GROUP BY ds.month, st.teamId"
+ " ORDER BY ds.month"
)


return jdbcDao.queryForList(sql.toString(), args)
}
} }





+ 20
- 0
src/main/java/com/ffii/tsms/modules/data/web/DashboardController.kt ファイルの表示

@@ -232,4 +232,24 @@ class DashboardController(
result["summarySubStage"] = summarySubStage result["summarySubStage"] = summarySubStage
return listOf(result) return listOf(result)
} }
@GetMapping("/searchMonthlyActualTeamTotalManhoursSpent")
fun searchMonthlyActualTeamTotalManhoursSpent(request: HttpServletRequest?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val teamId = request?.getParameter("teamId")
val startdate = request?.getParameter("startdate")
val enddate = request?.getParameter("enddate")
if (teamId != null) {
args["teamId"] = teamId
}
if (startdate != null) {
args["startdate"] = startdate
}
if (enddate != null) {
args["enddate"] = enddate
}
val result = mutableMapOf<String, Any>()
val monthlyActualTeamTotalManhoursSpent = dashboardService.monthlyActualTeamTotalManhoursSpent(args)
result["monthlyActualTeamTotalManhoursSpent"] = monthlyActualTeamTotalManhoursSpent
return listOf(result)
}
} }

+ 9
- 6
src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt ファイルの表示

@@ -2,44 +2,47 @@ package com.ffii.tsms.modules.data.web


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.common.SecurityUtils
import com.ffii.tsms.modules.data.entity.Skill import com.ffii.tsms.modules.data.entity.Skill
import com.ffii.tsms.modules.data.service.SkillService import com.ffii.tsms.modules.data.service.SkillService
import com.ffii.tsms.modules.data.web.models.NewSkillRequest import com.ffii.tsms.modules.data.web.models.NewSkillRequest
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.Valid import jakarta.validation.Valid
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.ServletRequestBindingException import org.springframework.web.bind.ServletRequestBindingException
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*




@RestController @RestController
@RequestMapping("/skill") @RequestMapping("/skill")
class SkillController(private val skillService: SkillService) {
open class SkillController(private val skillService: SkillService) {


@PostMapping("/save") @PostMapping("/save")
fun saveSkill(@Valid @RequestBody newSkill: NewSkillRequest): Skill {
@PreAuthorize("hasAuthority('MAINTAIN_MASTERDATA')")
open fun saveSkill(@Valid @RequestBody newSkill: NewSkillRequest): Skill {
return skillService.saveOrUpdate(newSkill) return skillService.saveOrUpdate(newSkill)
} }
@GetMapping("/{id}") @GetMapping("/{id}")
fun list(@Valid @PathVariable id: Long): List<Map<String, Any>> {
open fun list(@Valid @PathVariable id: Long): List<Map<String, Any>> {
val args: MutableMap<String, Any> = HashMap() val args: MutableMap<String, Any> = HashMap()
args["id"] = id args["id"] = id
return skillService.list(args); return skillService.list(args);
} }
@DeleteMapping("/delete/{id}") @DeleteMapping("/delete/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
fun delete(@PathVariable id: Long?) {
open fun delete(@PathVariable id: Long?) {
skillService.markDelete(id) skillService.markDelete(id)
} }


@GetMapping @GetMapping
fun list(): List<Map<String, Any>> {
open fun list(): List<Map<String, Any>> {
val args: MutableMap<String, Any> = HashMap() val args: MutableMap<String, Any> = HashMap()
return skillService.list(args); return skillService.list(args);
} }
@GetMapping("/combo") @GetMapping("/combo")
@Throws(ServletRequestBindingException::class) @Throws(ServletRequestBindingException::class)
fun combo(request: HttpServletRequest?): RecordsRes<Map<String, Any>> {
open fun combo(request: HttpServletRequest?): RecordsRes<Map<String, Any>> {
println(request) println(request)
return RecordsRes<Map<String, Any>>( return RecordsRes<Map<String, Any>>(
skillService.combo( skillService.combo(


+ 62
- 30
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt ファイルの表示

@@ -178,8 +178,8 @@ open class ReportService(
searchedClient: String, searchedClient: String,
projects: List<Project>, projects: List<Project>,
timesheets: List<Timesheet>, timesheets: List<Timesheet>,
numberOfDays: Int,
projectCompletion: Int,
daysUntilCurrentStageEnd: Int,
resourceUtilizationPercentage: Int,
): ByteArray { ): ByteArray {
// Generate the Excel report with query results // Generate the Excel report with query results
val workbook: Workbook = createProjectPotentialDelayReport( val workbook: Workbook = createProjectPotentialDelayReport(
@@ -187,8 +187,8 @@ open class ReportService(
searchedClient, searchedClient,
projects, projects,
timesheets, timesheets,
numberOfDays,
projectCompletion,
daysUntilCurrentStageEnd,
resourceUtilizationPercentage,
PROJECT_POTENTIAL_DELAY_REPORT PROJECT_POTENTIAL_DELAY_REPORT
) )


@@ -673,28 +673,43 @@ open class ReportService(


rowIndex = 4 rowIndex = 4
sheet.getRow(rowIndex).createCell(columnIndex).apply { sheet.getRow(rowIndex).createCell(columnIndex).apply {
setCellValue(if (project.customer?.name == null) "N/A" else project.customer?.name)
setCellValue(
if (project.customer?.code != null && project.customer?.name != null) project.customer!!.code + " - " + project.customer!!.name
else if (project.customer?.code != null) project.customer!!.code
else if (project.customer?.name != null) project.customer!!.name
else "N/A"
)
} }


rowIndex = 5 rowIndex = 5
sheet.getRow(rowIndex).createCell(columnIndex).apply {
setCellValue(
if (project.customerSubsidiary?.code != null && project.customerSubsidiary?.name != null) project.customerSubsidiary!!.code + " - " + project.customerSubsidiary!!.name
else if (project.customerSubsidiary?.code != null) project.customerSubsidiary!!.code
else if (project.customerSubsidiary?.name != null) project.customerSubsidiary!!.name
else "N/A"
)
}

rowIndex = 6
sheet.getRow(rowIndex).createCell(columnIndex).apply { sheet.getRow(rowIndex).createCell(columnIndex).apply {
setCellValue(if (project.teamLead?.team?.name == null) "N/A" else project.teamLead?.team?.name) setCellValue(if (project.teamLead?.team?.name == null) "N/A" else project.teamLead?.team?.name)
} }


rowIndex = 9
rowIndex = 10
sheet.getRow(rowIndex).apply { sheet.getRow(rowIndex).apply {
createCell(1).apply { createCell(1).apply {
setCellValue(project.expectedTotalFee!!)
setCellValue(project.expectedTotalFee!! * 0.8)
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }


createCell(2).apply { createCell(2).apply {
setCellValue(project.expectedTotalFee!! / 0.8)
setCellValue(project.expectedTotalFee!!)
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }
} }


rowIndex = 10
rowIndex = 11
val actualIncome = invoices.sumOf { invoice -> invoice.paidAmount!! } val actualIncome = invoices.sumOf { invoice -> invoice.paidAmount!! }
val actualExpenditure = timesheets.sumOf { timesheet -> val actualExpenditure = timesheets.sumOf { timesheet ->
timesheet.staff!!.salary.hourlyRate.toDouble() * ((timesheet.normalConsumed ?: 0.0) + (timesheet.otConsumed timesheet.staff!!.salary.hourlyRate.toDouble() * ((timesheet.normalConsumed ?: 0.0) + (timesheet.otConsumed
@@ -712,21 +727,20 @@ open class ReportService(
} }
} }


rowIndex = 11
rowIndex = 12
sheet.getRow(rowIndex).apply { sheet.getRow(rowIndex).apply {
createCell(1).apply { createCell(1).apply {
cellFormula = "B10-B11"
cellFormula = "B11-B12"
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }


createCell(2).apply { createCell(2).apply {
cellFormula = "C10-C11"
cellFormula = "C11-C12"
cellStyle.dataFormat = accountingStyle cellStyle.dataFormat = accountingStyle
} }
} }


rowIndex = 15

rowIndex = 16


val dateFormatter = val dateFormatter =
if (dateType == "Date") DateTimeFormatter.ofPattern("yyyy/MM/dd") else DateTimeFormatter.ofPattern("MMM YYYY") if (dateType == "Date") DateTimeFormatter.ofPattern("yyyy/MM/dd") else DateTimeFormatter.ofPattern("MMM YYYY")
@@ -796,7 +810,7 @@ open class ReportService(


createCell(3).apply { createCell(3).apply {
val lastRow = rowIndex - 1 val lastRow = rowIndex - 1
if (lastRow == 15) {
if (lastRow == 16) {
cellFormula = cellFormula =
"C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString()) "C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString())
} else { } else {
@@ -835,7 +849,7 @@ open class ReportService(


createCell(3).apply { createCell(3).apply {
val lastRow = rowIndex - 1 val lastRow = rowIndex - 1
if (lastRow == 15) {
if (lastRow == 16) {
cellFormula = "C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString()) cellFormula = "C{currentRow}-B{currentRow}".replace("{currentRow}", rowIndex.toString())
} else { } else {
cellFormula = cellFormula =
@@ -863,8 +877,8 @@ open class ReportService(
searchedClient: String, searchedClient: String,
projects: List<Project>, projects: List<Project>,
timesheets: List<Timesheet>, timesheets: List<Timesheet>,
numberOfDays: Int,
projectCompletion: Int,
daysUntilCurrentStageEnd: Int,
resourceUtilizationPercentage: Int,
templatePath: String, templatePath: String,
): Workbook { ): Workbook {
// please create a new function for each report template // please create a new function for each report template
@@ -939,15 +953,29 @@ open class ReportService(


createCell(4).apply { createCell(4).apply {
val currentClient = project.customer val currentClient = project.customer
val currentSubsidiary = project.customerSubsidiary
setCellValue(if (currentSubsidiary != null) currentSubsidiary.code + " - " + currentSubsidiary.name else currentClient?.code + " - " + currentClient?.name)
setCellValue(
if (currentClient?.code != null && currentClient.name != null) currentClient.code + " - " + currentClient.name
else if (currentClient?.code != null) currentClient.code
else if (currentClient?.name != null) currentClient.name
else "N/A"
)
} }


createCell(5).apply { createCell(5).apply {
setCellValue(project.actualStart?.format(DATE_FORMATTER))
val currentSubsidiary = project.customerSubsidiary
setCellValue(
if (currentSubsidiary?.code != null && currentSubsidiary.name != null) currentSubsidiary.code + " - " + currentSubsidiary.name
else if (currentSubsidiary?.code != null) currentSubsidiary.code
else if (currentSubsidiary?.name != null) currentSubsidiary.name
else "N/A"
)
} }


createCell(6).apply { createCell(6).apply {
setCellValue(project.actualStart?.format(DATE_FORMATTER))
}

createCell(7).apply {
setCellValue(project.planEnd?.format(DATE_FORMATTER)) setCellValue(project.planEnd?.format(DATE_FORMATTER))
} }
} }
@@ -958,22 +986,22 @@ open class ReportService(
val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]?.sum() ?: 0.0 val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]?.sum() ?: 0.0
val resourceUtilization = manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) val resourceUtilization = manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!)
// logger.info(project.name + " : " + milestone.taskGroup?.name + " : " + ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate)) // logger.info(project.name + " : " + milestone.taskGroup?.name + " : " + ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate))
// logger.info(numberOfDays)
if (ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate) <= numberOfDays.toLong() && resourceUtilization <= projectCompletion.toDouble() / 100.0) {
// logger.info(daysUntilCurrentStageEnd)
if (ChronoUnit.DAYS.between(LocalDate.now(), milestone.endDate) <= daysUntilCurrentStageEnd.toLong() && resourceUtilization <= resourceUtilizationPercentage.toDouble() / 100.0) {
milestoneCount++ milestoneCount++
val tempRow = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex) val tempRow = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex)
rowIndex++ rowIndex++


tempRow.apply { tempRow.apply {
createCell(7).apply {
createCell(8).apply {
setCellValue(milestone.taskGroup?.name ?: "N/A") setCellValue(milestone.taskGroup?.name ?: "N/A")
} }


createCell(8).apply {
createCell(9).apply {
setCellValue(milestone.endDate?.format(DATE_FORMATTER) ?: "N/A") setCellValue(milestone.endDate?.format(DATE_FORMATTER) ?: "N/A")
} }


createCell(9).apply {
createCell(10).apply {
cellStyle.dataFormat = workbook.createDataFormat().getFormat("0.00%") cellStyle.dataFormat = workbook.createDataFormat().getFormat("0.00%")


// if (groupedTimesheets.containsKey(Pair(project.id, milestone.id))) { // if (groupedTimesheets.containsKey(Pair(project.id, milestone.id))) {
@@ -1396,18 +1424,18 @@ open class ReportService(
generalCreateReportIndexed(sheet, result, rowIndex, columnIndex) generalCreateReportIndexed(sheet, result, rowIndex, columnIndex)


val sheetCF = sheet.sheetConditionalFormatting val sheetCF = sheet.sheetConditionalFormatting
val rule1 = sheetCF.createConditionalFormattingRule("AND(J7 >= $lowerLimit, J7 <= 1)")
val rule2 = sheetCF.createConditionalFormattingRule("J7 > 1")
val rule1 = sheetCF.createConditionalFormattingRule("AND(K7 >= $lowerLimit, K7 <= 1)")
val rule2 = sheetCF.createConditionalFormattingRule("K7 > 1")
var fillOrange = rule1.createPatternFormatting() var fillOrange = rule1.createPatternFormatting()
fillOrange.setFillBackgroundColor(IndexedColors.LIGHT_ORANGE.index); fillOrange.setFillBackgroundColor(IndexedColors.LIGHT_ORANGE.index);
fillOrange.setFillPattern(PatternFormatting.SOLID_FOREGROUND) fillOrange.setFillPattern(PatternFormatting.SOLID_FOREGROUND)


var fillRed = rule2.createPatternFormatting() var fillRed = rule2.createPatternFormatting()
fillRed.setFillBackgroundColor(IndexedColors.PINK.index);
fillRed.setFillBackgroundColor(IndexedColors.RED1.index);
fillRed.setFillPattern(PatternFormatting.SOLID_FOREGROUND) fillRed.setFillPattern(PatternFormatting.SOLID_FOREGROUND)


val cfRules = arrayOf(rule1, rule2) val cfRules = arrayOf(rule1, rule2)
val regions = arrayOf(CellRangeAddress.valueOf("\$J7:\$K${rowIndex + 1}"))
val regions = arrayOf(CellRangeAddress.valueOf("\$K7:\$L${rowIndex + 1}"))
sheetCF.addConditionalFormatting(regions, cfRules); sheetCF.addConditionalFormatting(regions, cfRules);


return workbook return workbook
@@ -1778,6 +1806,7 @@ open class ReportService(
+ " p.name, " + " p.name, "
+ " t.code as team, " + " t.code as team, "
+ " c.code as client, " + " c.code as client, "
+ " COALESCE(ss.name, 'N/A') as subsidiary, "
+ " p.expectedTotalFee * 0.8 as plannedBudget, " + " p.expectedTotalFee * 0.8 as plannedBudget, "
+ " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, " + " COALESCE(tns.totalBudget, 0) as actualConsumedBudget, "
+ " COALESCE(p.totalManhour, 0) as plannedManhour, " + " COALESCE(p.totalManhour, 0) as plannedManhour, "
@@ -1798,6 +1827,7 @@ open class ReportService(
+ " LEFT JOIN staff s ON p.teamLead = s.id " + " LEFT JOIN staff s ON p.teamLead = s.id "
+ " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint " + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint "
+ " LEFT JOIN customer c ON p.customerId = c.id " + " LEFT JOIN customer c ON p.customerId = c.id "
+ " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id "
+ " left join teamNormalConsumed tns on tns.project_id = p.id " + " left join teamNormalConsumed tns on tns.project_id = p.id "
+ " WHERE p.deleted = false " + " WHERE p.deleted = false "
+ " and p.status = 'On-going' " + " and p.status = 'On-going' "
@@ -1808,6 +1838,8 @@ open class ReportService(
sql.append("and t.id = :teamId") sql.append("and t.id = :teamId")
if (args.containsKey("custId")) if (args.containsKey("custId"))
sql.append("and c.id = :custId") sql.append("and c.id = :custId")
if (args.containsKey("subsidiaryId"))
sql.append("and ss.id = :subsidiaryId")
if (args.containsKey("status")) if (args.containsKey("status"))
statusFilter = when (args.get("status")) { statusFilter = when (args.get("status")) {
"Potential Overconsumption" -> "and " + "Potential Overconsumption" -> "and " +


+ 10
- 4
src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt ファイルの表示

@@ -35,6 +35,7 @@ import java.time.format.DateTimeFormatter
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.project.entity.Project
import com.ffii.tsms.modules.project.service.SubsidiaryService
import com.ffii.tsms.modules.report.web.model.* import com.ffii.tsms.modules.report.web.model.*
import com.ffii.tsms.modules.timesheet.entity.Timesheet import com.ffii.tsms.modules.timesheet.entity.Timesheet
import org.springframework.data.domain.Example import org.springframework.data.domain.Example
@@ -56,6 +57,7 @@ class ReportController(
private val leaveRepository: LeaveRepository, private val leaveRepository: LeaveRepository,
private val teamService: TeamService, private val teamService: TeamService,
private val customerService: CustomerService, private val customerService: CustomerService,
private val subsidiaryService: SubsidiaryService,
private val invoiceService: InvoiceService, private val gradeAllocationRepository: GradeAllocationRepository, private val invoiceService: InvoiceService, private val gradeAllocationRepository: GradeAllocationRepository,
private val subsidiaryRepository: SubsidiaryRepository private val subsidiaryRepository: SubsidiaryRepository
) { ) {
@@ -119,7 +121,7 @@ class ReportController(
val projectTasks = projectTaskRepository.findAllByProjectIn(projects) val projectTasks = projectTaskRepository.findAllByProjectIn(projects)
val timesheets = timesheetRepository.findAllByProjectTaskIn(projectTasks) val timesheets = timesheetRepository.findAllByProjectTaskIn(projectTasks)


val reportResult: ByteArray = excelReportService.generateProjectPotentialDelayReport(searchedTeam, searchedClient, projects, timesheets, request.numberOfDays, request.projectCompletion)
val reportResult: ByteArray = excelReportService.generateProjectPotentialDelayReport(searchedTeam, searchedClient, projects, timesheets, request.daysUntilCurrentStageEnd, request.resourceUtilizationPercentage)
return ResponseEntity.ok() return ResponseEntity.ok()
.header("filename", "Project Potential Delay Report - " + LocalDate.now() + ".xlsx") .header("filename", "Project Potential Delay Report - " + LocalDate.now() + ".xlsx")
.body(ByteArrayResource(reportResult)) .body(ByteArrayResource(reportResult))
@@ -154,7 +156,7 @@ class ReportController(
fun ProjectResourceOverconsumptionReport(@RequestBody @Valid request: ProjectResourceOverconsumptionReport): ResponseEntity<Resource> { fun ProjectResourceOverconsumptionReport(@RequestBody @Valid request: ProjectResourceOverconsumptionReport): ResponseEntity<Resource> {
val lowerLimit = request.lowerLimit val lowerLimit = request.lowerLimit
var team: String = "All" var team: String = "All"
var customer: String = "All"
var customerOrSubsidiary: String = "All"
val args: MutableMap<String, Any> = mutableMapOf( val args: MutableMap<String, Any> = mutableMapOf(
"status" to request.status, "status" to request.status,
"lowerLimit" to lowerLimit "lowerLimit" to lowerLimit
@@ -165,10 +167,14 @@ class ReportController(
} }
if (request.custId != null) { if (request.custId != null) {
args["custId"] = request.custId args["custId"] = request.custId
customer = customerService.find(request.custId).orElseThrow().name
customerOrSubsidiary = customerService.find(request.custId).orElseThrow().name
}
if (request.subsidiaryId != null) {
args["subsidiaryId"] = request.subsidiaryId
customerOrSubsidiary = subsidiaryService.find(request.subsidiaryId).orElseThrow().name
} }
val result: List<Map<String, Any>> = excelReportService.getProjectResourceOverconsumptionReport(args); val result: List<Map<String, Any>> = excelReportService.getProjectResourceOverconsumptionReport(args);
val reportResult: ByteArray = excelReportService.generateProjectResourceOverconsumptionReport(team, customer, result, lowerLimit)
val reportResult: ByteArray = excelReportService.generateProjectResourceOverconsumptionReport(team, customerOrSubsidiary, result, lowerLimit)
// val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") // val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
return ResponseEntity.ok() return ResponseEntity.ok()
// .contentType(mediaType) // .contentType(mediaType)


+ 3
- 2
src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt ファイルの表示

@@ -27,8 +27,8 @@ data class ProjectCashFlowReportRequest (
data class ProjectPotentialDelayReportRequest ( data class ProjectPotentialDelayReportRequest (
val teamId: String, val teamId: String,
val clientId: String, val clientId: String,
val numberOfDays: Int,
val projectCompletion: Int,
val daysUntilCurrentStageEnd: Int,
val resourceUtilizationPercentage: Int,
val type: String, val type: String,
) )


@@ -46,6 +46,7 @@ data class LateStartReportRequest (
data class ProjectResourceOverconsumptionReport ( data class ProjectResourceOverconsumptionReport (
val teamId: Long?, val teamId: Long?,
val custId: Long?, val custId: Long?,
val subsidiaryId: Long?,
val status: String, val status: String,
val lowerLimit: Double val lowerLimit: Double
) )


+ 11
- 0
src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt ファイルの表示

@@ -75,6 +75,17 @@ open class LeaveService(
return transformToLeaveEntryMap(leaveRepository.findAllByStaff(memberStaff)) return transformToLeaveEntryMap(leaveRepository.findAllByStaff(memberStaff))
} }


open fun deleteMemberLeaveEntry(staffId: Long, entryId: Long): Map<String, List<LeaveEntry>> {
val currentStaff = staffsService.currentStaff() ?: throw BadRequestException()
// Make sure current staff is a team lead
teamService.getMyTeamForStaff(currentStaff) ?: throw BadRequestException()

val memberStaff = staffsService.getStaff(staffId)

leaveRepository.deleteById(entryId)
return transformToLeaveEntryMap(leaveRepository.findAllByStaff(memberStaff))
}

open fun getTeamMemberLeave(): Map<Long, TeamMemberLeaveEntries> { open fun getTeamMemberLeave(): Map<Long, TeamMemberLeaveEntries> {
val currentStaff = staffsService.currentStaff() ?: return emptyMap() val currentStaff = staffsService.currentStaff() ?: return emptyMap()
// Get team where current staff is team lead // Get team where current staff is team lead


+ 11
- 0
src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt ファイルの表示

@@ -83,6 +83,17 @@ open class TimesheetsService(
return transformToTimeEntryMap(timesheetRepository.findAllByStaff(memberStaff)) return transformToTimeEntryMap(timesheetRepository.findAllByStaff(memberStaff))
} }


open fun deleteMemberTimeEntry(staffId: Long, entryId: Long): 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)

timesheetRepository.deleteById(entryId)
return transformToTimeEntryMap(timesheetRepository.findAllByStaff(memberStaff))
}

open fun getTimesheet(): Map<String, List<TimeEntry>> { open fun getTimesheet(): Map<String, List<TimeEntry>> {
// Need to be associated with a staff // Need to be associated with a staff
val currentStaff = staffsService.currentStaff() ?: return emptyMap() val currentStaff = staffsService.currentStaff() ?: return emptyMap()


+ 10
- 0
src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt ファイルの表示

@@ -66,6 +66,16 @@ class TimesheetsController(private val timesheetsService: TimesheetsService, pri
}.getOrNull()) }.getOrNull())
} }


@PostMapping("/deleteMemberEntry")
fun deleteMemberEntry(@Valid @RequestBody request: TeamEntryDelete): Map<String, List<TimeEntry>> {
return timesheetsService.deleteMemberTimeEntry(request.staffId, request.entryId)
}

@PostMapping("/deleteMemberLeave")
fun deleteMemberLeave(@Valid @RequestBody request: TeamEntryDelete): Map<String, List<LeaveEntry>> {
return leaveService.deleteMemberLeaveEntry(request.staffId, request.entryId)
}

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


+ 6
- 0
src/main/java/com/ffii/tsms/modules/timesheet/web/models/TeamEntryDelete.kt ファイルの表示

@@ -0,0 +1,6 @@
package com.ffii.tsms.modules.timesheet.web.models

data class TeamEntryDelete(
val staffId: Long,
val entryId: Long,
)

+ 3
- 2
src/main/java/com/ffii/tsms/modules/user/entity/UserRepository.java ファイルの表示

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


import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.data.jpa.repository.Query;
import com.ffii.core.support.AbstractRepository; import com.ffii.core.support.AbstractRepository;


public interface UserRepository extends AbstractRepository<User, Long> { public interface UserRepository extends AbstractRepository<User, Long> {
@@ -12,5 +12,6 @@ public interface UserRepository extends AbstractRepository<User, Long> {
List<User> findByName(@Param("name") String name); List<User> findByName(@Param("name") String name);
List<User> findAllByAndDeletedFalse(); List<User> findAllByAndDeletedFalse();


Optional<User> findByUsernameAndDeletedFalse(String username);
Optional<User> findByUsernameAndDeletedFalse(String username);

} }

+ 12
- 0
src/main/java/com/ffii/tsms/modules/user/service/GroupService.java ファイルの表示

@@ -172,6 +172,18 @@ public class GroupService extends AbstractBaseEntityService<Group, Long, GroupRe
+ " WHERE gu.groupId = :id", + " WHERE gu.groupId = :id",
Map.of(Params.ID, id)); Map.of(Params.ID, id));
} }
@Transactional(rollbackFor = Exception.class)
public String getGroupName(Map<String, Object> args) {
StringBuilder sql = new StringBuilder("select"
+ " g.name "
+ " from user u "
+ " left join user_group ug on u.id = ug.userId "
+ " left join `group`g on ug.groupId = g.id "
+ " where g.deleted = false "
+ " and u.id = :userId"
);
return jdbcDao.queryForString(sql.toString(), args);
}




@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)


+ 8
- 0
src/main/resources/db/changelog/changes/20240529_01_cyril/01_insert_authority.sql ファイルの表示

@@ -0,0 +1,8 @@
-- liquibase formatted sql
-- changeset cyril:authority, user_authority

INSERT INTO authority (authority,name)
VALUES ('DELETE_PROJECT','delete project');

INSERT INTO user_authority (userId,authId)
VALUES (1,19);

バイナリ
src/main/resources/templates/report/AR02_Delay Report v02.xlsx ファイルの表示


バイナリ
src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx ファイルの表示


バイナリ
src/main/resources/templates/report/EX02_Project Cash Flow Report.xlsx ファイルの表示


読み込み中…
キャンセル
保存